feat(cli): lazy background worker — daemon-less consolidation, metered and detached - #3
Open
rabbiveesh wants to merge 6 commits into
Open
feat(cli): lazy background worker — daemon-less consolidation, metered and detached#3rabbiveesh wants to merge 6 commits into
rabbiveesh wants to merge 6 commits into
Conversation
…dation Ship the docs/design/lazy-background-work.md first cut (memory-cli/src/worker.rs): - writes (remember / promote / onboard --commit) count toward a due-check (threshold + min/max interval, work-first); when it trips, the CLI re-execs itself as a detached hidden `mem __worker` (own process group, stdio -> .worker.log) that consolidates the pending backlog, bounded by worker.max_targets; dream piggybacks weekly on a healthy pass - pidfile lock (zero-dep, stale-steal on dead holder or 6h age) serializes workers; idempotent edge commits make the rare double-worker harmless - state sidecar .worker-state.json: epoch u64, atomic replace, writes_since drained by subtraction so mid-run writes stay pending; last_run advances on failed passes too (min_interval throttles retries), only a killed run re-triggers immediately; first-ever write anchors intervals (no surprise LLM pass on write #1) - recall/ask surface a one-line '🛠 background …' summary exactly once; `mem worker` is the status surface, `mem worker --now` a forced pass; MEM_NO_BG=1 / worker.enabled=false switch the tier off - consolidate/dream cores extracted as consolidate_pass/dream_pass, shared by the commands and the worker (no behavior change) - scope decision (spec open question): the worker inherits the triggering invocation's cwd and consolidates the SAME active scope, not store-wide Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WMTLGECmHZB6HKXADbWzsD
…guard Adversarial review of the first cut surfaced 8 findings; all addressed: - [high] a failed/empty pass no longer drains writes_since — the backlog is drained only when the pass actually consolidated targets, so a down backend retries (min_interval-throttled) and out-of-scope writes catch up when the user is next active in their scope - [med] state-file read-modify-writes (foreground count, surfacing, worker's final update) are serialized by a .worker-state.lock micro-lock: µs hold, ~250ms spin then proceed-unlocked so the foreground can never hang; fixes the lost-update race that could drop a run's summary, revert last_run, and fork a redundant second LLM pass - [med] worker.max_targets=0 no longer panics (clamp on an inverted range); pass_limit() treats 0 as 1 - [low] cmd_consolidate/cmd_dream --limit 0 report as before the pass extraction (no false 'nothing in scope'/'need 2 beliefs'; review nudge kept) - [low] a lock with a FUTURE mtime (clock step, VM snapshot) falls through to the pid-liveness check instead of blocking background work for hours - [low] .worker.log rotates past ~1MB (one old generation kept) - [low] documented that multi-word worker knobs are config.toml-only (the MEM_* env layer's _ separator can't address them) Design doc updated to match (drain semantics, state lock, rotation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WMTLGECmHZB6HKXADbWzsD
Split the state-lock test: exclusion/free-on-drop stays portable, and a new Linux-only test ages the lock file past the 10s abandonment bar (touch -d) to prove an abandoned .worker-state.lock is stolen rather than spun on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WMTLGECmHZB6HKXADbWzsD
…teal Round-2 verification of the review fixes surfaced two residuals: - a capped pass (backlog > max_targets) drains the whole counter but only consolidates the newest chunk, and no cursor exists to catch the tail up later — instead of a false 'catches up across passes' promise, the run summary now names the tail and the command that covers it: 'capped at 12; cover the rest: mem consolidate --limit 40' (docs aligned) - .worker-state.lock abandonment now treats a FUTURE mtime (clock stepped back, restored VM) as abandoned, mirroring the WorkerLock fix — otherwise a crashed holder converts the µs micro-lock into a 250ms per-command spin for hours Accepted as documented: --limit 0 cosmetic divergences in degenerate scopes; the one-generation log-rotation race. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WMTLGECmHZB6HKXADbWzsD
…idate Instrumentation: every consolidate/dream pass — background or foreground — appends one line to $MEMORY_DIR/.worker-metrics.jsonl: what happened (job, trigger, scopes, pending, targets, new_edges, review, probes, bridges, ok, error) and what it cost (duration_ms, chat_calls, chat_ms, embed_texts). Costs come from new process-local atomic counters in memory-embed — the one choke point all LLM traffic flows through — snapshotted before/after each pass; attempts count even on failure. mem worker prints lifetime totals per job plus the recent tail; the ledger is the substrate for deeper analysis. Backgrounding: mem consolidate now DETACHES by default, re-execing the same hidden __worker body with the caller's --limit (trigger=cli, no dream piggyback) — identical lock, log, state-drain, surfacing, and metrics as a due-check kick. --fg (or a disabled tier: MEM_NO_BG / worker.enabled=false) keeps the synchronous path, which is now also metered and drains the backlog it covers. mem dream stays foreground but is metered, and a deliberate dream resets the weekly piggyback cadence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WMTLGECmHZB6HKXADbWzsD
…ordance Review round on the instrumentation commit: - [med] a detached mem consolidate that loses the worker lock no longer vanishes after telling the user it's running: the skip is surfaced on the next recall/ask with the exact rerun command, and recorded as an ok:false ledger line (a skipped due-check kick stays silent — that work is fungible) - [low] the inline --fg path now prints the same capped-backlog affordance as the worker path when it drains more writes than it consolidated - [low] the two memory-embed counter tests merged into one sequential test — they raced each other's process-global atomics under parallel test runs - sanitize also strips \r (terminal-garble nit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WMTLGECmHZB6HKXADbWzsD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships the lazy-background-work proposal: the CLI is its own scheduler — no daemon, no cron. Two features plus two adversarial review rounds each.
The worker tier (
crates/memory-cli/src/worker.rs)remember/promote/onboard --commitcount toward a due-check (default: 5 writes + 10-min throttle, 24-h ceiling). When it trips, the CLI re-execs itself as a detached hiddenmem __worker(own process group, stdio →.worker.log, rotated past ~1 MB).worker.max_targets);dreampiggybacks weekly. Scope = the triggering invocation's active scope (the spec's "trigger is the user already being in the right place"), never store-wide..worker-state.lockmicro-lock serializes state read-modify-writes (µs hold, ~250 ms spin, then proceed-unlocked — the foreground can never hang).capped at 12; cover the rest: mem consolidate --limit 40);recall/asksurface a one-line🛠 background …summary exactly once.mem worker(status),mem worker --now(forced pass),MEM_NO_BG=1/worker.enabled=false(kill switch), knobs under[worker]in config.toml.Instrumentation + backgrounded
mem consolidate.worker-metrics.jsonl: one JSON line per pass — what happened (job, trigger, pending, targets, new edges, review flags, probes/bridges, ok/error) and what it cost (wall ms, chat calls + chat ms, embed texts). Costs come from new atomic counters inmemory-embed, the single choke point all LLM traffic flows through; attempts count even on failure.mem workershows per-job totals + the recent tail.mem consolidatedetaches by default, running the same locked/metered worker body with the caller's--limit(trigger=cli, no dream piggyback).--fg(or a disabled tier) keeps the synchronous path — also metered, drains the backlog it covers. A detached consolidate that loses the lock to an in-flight worker surfacesconsolidate SKIPPED … rerun: mem consolidate --limit Ninstead of vanishing.mem dreamstays foreground but is metered, and resets the weekly piggyback cadence.Review rounds (write→review→fix loop)
Each feature commit was followed by an adversarial review + verification round; the fix commits document what they caught, including: a failed pass permanently draining the retry backlog, a lost-update race on the state file, a
clamppanic onmax_targets=0,--limit 0refactor regressions, a clock-skew hole in stale-lock detection, counter-test races, and the silent lock-held drop above. Accepted tradeoffs (store-global counter imprecision, double-steal race, log-rotation race) are documented in the design doc's "As built" section.Testing
--fgconsolidate, lock-held skip surfacing.consolidate/dreammachinery unchanged, but a realmem remember×5 on a machine with ollama is the last-mile check.🤖 Generated with Claude Code
https://claude.ai/code/session_01WMTLGECmHZB6HKXADbWzsD
Generated by Claude Code