feat(recall): extraction manager, scheduler, and CLI - #1183
Conversation
Adds the extraction candidate query (privacy filters, quiet period, failure backoff), idempotent bulk entry insert with deterministic ids, and a Manager that drives per-session distillation with unit-split recovery, resumable progress, and auto-activation. Adds [recall.extract] configuration (model identity, named servers, scheduling knobs, prompt and request overrides), an after-sync extraction scheduler whose backstop ticks run full passes for content-digest top-up, and daemon wiring behind enabled = true. Restructures 'recall extract' into run/status/activate/retire/doctor/ preview subcommands, keeping the legacy --session/--dry-run flags as a silent fallback for preview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
roborev: Combined Review (
|
Review fixes: eligibility now requires a clean secret scan under the current rules versions (unscanned sessions fail closed), a changed transcript rebuilds the session's generated entries instead of leaving stale ones colliding with their replacements, full passes only revisit done sessions written to since extraction, entryless generations never activate, extraction subcommands reject --server instead of silently using the local archive, and a nilaway-flagged nil flow from GetSession is checked explicitly. Adds subsystem documentation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
5fc6aba addresses the lint failure and all five review findings:
Also adds documentation: The e2e failure was a webkit-only 🤖 Generated with Claude Code |
roborev: Combined Review (
|
- Require the current full secret-scan rules version; the definite-only inline scan no longer qualifies a session for extraction. - Exclude sessions with recorded secret findings of any confidence, in the candidates SQL and the explicit single-session path. - Bracket the message read with session snapshot reads and skip on mismatch, binding eligibility to the transcript actually sent. - Stamp content_stamped_at when the unit digest is derived and gate done-session revisits on it instead of progress.updated_at, so writes landing mid-extraction are not skipped forever. - Stage building-generation entries as archived; activation promotes them and archives the retired generation's automatic entries in one transaction, and retirement archives its entries. - Add a catchup ticker running incremental passes when the backstop is disabled, so sessions whose quiet period elapses after the last sync still get extracted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Review round 2 addressed in 6570c21 — all five findings were real. Candidate credentials bypass the privacy gate (high). Eligibility now requires the last scan to be the current full Scan approval not bound to the transcript sent (high). The manager brackets Mid-extraction writes permanently skipped (medium). New Activation/retirement don't control serving (medium). Entries extracted under a building generation are staged with Disabled backstop prevents eventual extraction (medium). With Each fix landed test-first: new coverage in |
roborev: Combined Review (
|
- Read session snapshots with the full column set: the standard column list omits local_modified_at, leaving the bracket around the message load blind to metadata-only writes such as a findings replace under an unchanged rules version. - Rebuild the candidates query as a union of two indexed arms: discovery walks sessions by local_modified_at (bounded by a new ChangedSince watermark), the backlog arm walks progress rows by state. The manager watermarks incremental passes at the last completed unlimited pass minus the quiet period; full passes stay unrestricted as the recovery path. Adds idx_sessions_local_modified and a query-plan regression test asserting no full sessions scan. - Hold the offline writer lock for manual extraction commands via openWriteDB, so a multi-step pass cannot overlap another direct writer or a resync database swap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Review round 3 addressed in 8a3ff5d — all three findings were real. Snapshot guard misses concurrent changes (medium). Confirmed: Background extraction scales with the full archive (medium). The candidates query is now a union of two indexed arms: a discovery arm over sessions with no progress row, bounded by a new Manual extraction bypasses the writer lock (medium). All fixes landed test-first; suites, golangci-lint, and nilaway are clean apart from the pre-existing unrelated |
roborev: Combined Review (
|
- Invalidate secret-scan freshness atomically with every transcript mutation: bumpTranscriptRevisionTx clears secrets_rules_version in the same transaction, so appended content can never ride a stale full-scan stamp; the atomic replace path re-stamps in-transaction. The manager also requires the loaded message count to equal the session row's, closing the row-written-before-transcript window. - Require HTTPS for non-loopback extraction endpoints; allow_http on a server entry opts into plaintext explicitly, and the model client refuses redirects that leave the transport policy. - Stamp content_stamped_at with the caller's pre-read cutoff on every stable upsert, including same-digest revisits, so mid-derivation writes re-open the session and settled sessions stop re-opening on every full pass. - Bound full-pass discovery by the watermark too (ratcheted forward); unbounded reconciliation is the fresh-manager path (daemon restart or manual CLI run). - Count changed done sessions in activation-readiness and status backlog checks via IncludeDone, so a stale corpus cannot activate and status reports pending revisits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Review round 4 addressed in fe18bf6 — both highs and all three mediums were real, with one nuance on the first high. Scan freshness not bound to the transcript (high). Verified the exact window: the incremental sync path writes the session row, then the messages, then findings, then signals as separate writes, so appended content could briefly coexist with a still-current full-scan stamp. Fixed at the storage layer with the reviewer's suggested invalidation: Cleartext HTTP endpoints (high). Stamp timing (medium). Hourly full-pass scaling (medium). Full passes now bound discovery by the same ratcheted watermark; they differ from incremental passes only in revisiting changed done sessions, which the progress-state index bounds. Unbounded reconciliation is the fresh-manager path — a daemon restart or a manual Activation/status ignore changed done sessions (medium). Both All fixes landed test-first ( |
roborev: Combined Review (
|
- Drive done-session revisits from the sessions side of the join, bounded by a second watermark that ratchets to the start of each completed unlimited full pass, so the periodic backstop walks recent writes via idx_sessions_local_modified instead of every completed progress row. EXPLAIN QUERY PLAN regression asserts neither table is scanned. - Preserve content_stamped_at across full-resync state copies (empty stamps would reload the whole archive's transcripts on the next full pass); archives predating the column copy it as empty and settle on their first revisit. - Record a stable-bracket message-count mismatch as a retryable failure instead of a silent skip, so the advancing watermarks cannot permanently exclude a session whose row and transcript disagree. - Recheck eligibility (trash, automation) against the second snapshot read: those flags can flip without touching any field the snapshot comparison watches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round-5 findings addressed in a588ed6. 1. Full/backstop passes scan all completed progress rows — The done-revisit predicate moved out of the progress-driven arm into its own UNION arm that drives from the sessions side of the join, so the planner bounds it with 2. Full-resync copy drops 3. Message-count mismatch silently skipped while the watermark advances — A count mismatch under a stable snapshot bracket is no longer a silent skip: the manager upserts a progress row (which also handles digest-change corpus cleanup) and marks it failed with a descriptive error, so the session is visible in status and re-offered through the queue arm after the failure backoff — the queue arm is never gated by the discovery watermarks. The transcript still never reaches the model. An unstable bracket keeps the silent skip, since the concurrent write re-surfaces the session by itself. 4. Second snapshot comparison misses concurrent ineligibility — The bracket recheck now goes through
|
roborev: Combined Review (
|
- Handle a stable-bracket message-count mismatch before the completed-row short-circuit: a same-digest upsert preserves done and settles the coverage stamp, which would claim the inconsistent state as covered forever. MarkExtractProgressFailed gains ReopenDone, which waives only the non-done guard (digest and cursor checks still apply) and resets the reopened row's cursor to zero — the completed-units claim was judged against the inconsistent session, and the strictly monotonic cursor could otherwise never reach done again. - Synchronize session context (project, cwd, git branch, agent) onto a session's generated entries during same-digest revisits: entries copy those fields at insert time, and a metadata-only session update keeps the digest unchanged, so the revisit would otherwise settle the stamp while the corpus kept matching Recall filters for the old context. Human-touched entries stay as they were, mirroring the delete path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round-6 findings addressed in fd5bc69. 1. Completed rows bypass message-count mismatch handling — Verified: with an unchanged digest, the progress upsert preserves 2. Metadata-only updates leave generated entries stale — Fixed by synchronizing rather than by widening the reconciliation identity: folding metadata into the digest would trigger full model re-extraction on every project or branch change. A new One test-harness note: the bare
|
roborev: Combined Review (
|
- Re-validate eligibility (snapshot, predicates, secret findings) before every model call and before persisting each call's output. Losing it mid-extraction fails closed: generated entries are discarded and the progress row reopens at cursor zero as a retryable failure. - Restrict extraction redirects to the configured endpoint origin (scheme, host, port): a 307/308 replays the POST with transcript content to whatever destination the endpoint names, including loopback services that trust local callers. - Preserve updated_at on same-digest upserts of failed rows so a retry cancelled mid-way does not restart the failure backoff. - Synchronize entry context before the upsert that settles the coverage stamp, so a failed sync leaves the session re-openable instead of stamped covered with stale metadata. - Revisit legacy rows (NULL local_modified_at) only while their coverage stamp is empty: every write path records a local write, so a stamped legacy row cannot have changed and must not reload every full pass. - Complete zero-unit rows on same-digest upserts whatever state they held: the extraction loop runs zero iterations for them, so no cursor advance would ever promote a reopened zero-unit failure. - Add the extraction tables to the read-only schema compatibility probe so status on an unmigrated archive reports upgrade guidance instead of raw missing-table errors. Rename ReopenDone to Reopen; the reset now applies to non-done rows too, matching the discard path's needs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round-7 findings addressed in d089fd2. High — eligibility can change during extraction — The bracket no longer ends when distillation starts. A new Cross-origin redirects (SSRF) — Retry setup resets failure backoff — the upsert now preserves Stamp settles before context sync — the sync moved before the upsert that stamps coverage. The regression test ( Legacy NULL rows reprocessed every full pass — the done-revisit predicate now treats Zero-unit failed retries never return to done — a same-digest upsert now completes a zero-unit row whatever state it held (they are done by construction; the loop runs zero iterations, so no cursor advance would promote them). Read-only status compatibility — Also in this commit: Docs updated ( |
roborev: Combined Review (
|
- Commit each distilled unit through a single guarded transaction (CommitExtractedUnit): the session snapshot, eligibility predicates, and absence of secret findings are re-verified atomically with the entry insert and cursor advance, so output distilled from a stale or newly ineligible view can never land. The out-of-band recheck now only saves a wasted model call; the commit guard is the enforcement point. - Bind evidence provenance at commit time through the recall evidence-window APIs: each cited range is rebuilt as a host-authorized window inside the commit transaction and its content digest and stable endpoint UUIDs are stamped onto the evidence rows, so the evidence reconciler re-verifies provenance instead of revoking it for a missing digest on the first relevant transcript write or resync. - Reconcile eligibility loss after extraction on full passes: sessions since trashed, flagged automated, or carrying secret findings get their unreviewed_auto entries deleted and progress rows removed, so an excluded session's corpus stops serving and a lingering pending or partial row cannot block activation forever. Stale scan versions are deliberately transient and do not qualify. Bounded by the full watermark; a fresh manager reconciles unbounded. - Index recall_entries by (source_run_id, source_session_id, review_state): the per-pass stats count and the per-session delete and context-sync paths all filter on those columns and previously scanned the whole corpus. EXPLAIN QUERY PLAN regression added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round-8 findings addressed in 50e5858. High — recheck and insert not atomic — Verified: a write could land between the out-of-band recheck and the entry insert. Each unit is now persisted through Post-extraction eligibility loss never reconciled — Verified, including the activation-blocking claim: Provenance revoked for missing digest — Verified against the reconciler: it revokes No index leading with Docs updated ( |
roborev: Combined Review (
|
- Make eligibility retraction generation-independent: ineligible sessions' unreviewed_auto entries are deleted across every registered generation (a retired generation keeps serving until the next activation) and their progress rows removed across generations. - Run retraction on every scheduled pass, bounded by its own watermark that advances on completed unlimited scans: with the backstop disabled only incremental passes run, and privacy retraction must not be schedulable away. - Re-verify eligibility inside the activation transaction: promotion leaves entries of sessions trashed, flagged automated, or scanned into findings after staging in the archived state for the retraction pass to delete, instead of serving them. - Discard mid-extraction output atomically: the entry delete and the guarded cursor reset commit in one transaction, so a failure between them can no longer leave a cursor pointing past deleted entries. - Make both retraction deletes set-based instead of materializing session ids into IN clauses, so retraction cannot be blocked by SQLite's host-parameter limit. - Sanitize the doctor probe error before it reaches the terminal: an endpoint's HTTP error body is embedded in the error and could carry OSC/CSI sequences. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round-9 findings addressed in 369ab33. High — retraction misses other generations — High — retraction schedulable away — Retraction now runs on every scheduled pass, not just full ones, bounded by its own High — activation promotes ineligible sessions' entries — The promotion UPDATE inside Entry deletion and cursor reset non-atomic — New IN-clause parameter limit — Both retraction deletes are now set-based, with the ineligible-session SELECT inlined as a subquery; no per-session host parameters exist at all, so no limit to hit. Terminal-control injection via doctor — The probe error (which embeds up to 200 bytes of the endpoint's HTTP error body) now passes through Docs updated ( |
roborev: Combined Review (
|
Activation re-verifies its gates inside the transaction: pending or partial coverage, completed sessions with writes past their coverage stamp or a stale scan stamp, and zero servable entries after promotion all abort with ErrExtractActivationBlocked instead of retiring the served corpus around them. Extraction endpoints no longer follow any redirect: a name-based same-origin allowance cannot survive hostname re-resolution. TurnsV1 splits action runs at ordinal discontinuities so no evidence range spans a row ingest filtering dropped, and a commit refusal against an unchanged session is recorded as a failure so the backoff applies instead of repeating the model call every pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 28 review response (as of 200edfd)Verified all four findings against the code. Three fixed, one declined with rationale. 1. Endpoint outages handled per session (manager.go) — fixed. Confirmed: a dead endpoint made every queued session burn its own full retry ladder, so a large backlog's pass could stall for hours re-probing the same outage. 2. Quiet-period timestamps compared lexicographically (recall_extract.go:981) — declined, with the substance addressed by fix 3. The premise (mixed offsets breaking string comparison) doesn't hold here: every 3. Quiet-period eligibility can become stale (manager.go:508) — fixed. Confirmed: the backlog is materialized at pass start, so a session re-ended while queued (or during a long pass) could be extracted mid-settling. 4. Partial secret-scan commits don't notify (huma_routes_secrets.go:101) — fixed. Confirmed, plus a latent nil-deref: Verification: extract/db/config/server/service suites green; |
roborev: Combined Review (
|
…d scans Two review findings fixed. With the backstop disabled, a failed startup full pass left its carry stranded: catchup ticks always ran incremental passes and never consumed pendingFull, and the debounce only re-arms on sync activity, so changed completed sessions could remain unrevisited indefinitely. Periodic ticks now run with Full when a carry is pending, and the carry clears only once a full pass both starts and succeeds. A side effect pinned by the updated catchup test: the first catchup tick now consumes the startup full carry instead of waiting for the startup debounce. The secret-scan summary undercounted on cancellation: scanOneSession persists through a non-context-aware write, and the loop returned on ctx.Err() before counting the session it had just committed. A cancellation landing during the first session's scan therefore reported Scanned == 0 with committed work, and the HTTP route's Scanned > 0 gate skipped the extraction notification, delaying retraction. The loop now counts a persisted session before observing cancellation, keeping the summary truthful for the notification gate. The regression test drives the exact window with a context whose Err() flips to Canceled the moment the session's rules version commits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 29 review response (as of 27fd967)Both findings verified against the code and fixed. 1. Stranded full-pass carry with the backstop disabled (extract_scheduler.go) — fixed. Confirmed: catchup ticks ran 2. Partial scan committing without incrementing Scanned (huma_routes_secrets.go / sync engine) — fixed at the engine. Confirmed the exact window: Verification: cmd/agentsview, internal/sync, internal/server, internal/service, internal/recall/extract suites run; only the pre-existing failures remain (1 in cmd/agentsview, 9 Note on the test placement: the new engine test lives in |
roborev: Combined Review (
|
Activation checked post-coverage session writes only for done rows. A failed partial row keeps its staged entries behind the failure backoff, and its session — still fully eligible — could be written after the coverage stamp: a content change, or a remap to another project, cwd, or branch. Nothing gated those entries, so activation promoted stale context that only the retry's same-digest refresh knows how to repair. The activation transaction now refuses failed rows like stale done coverage, scoped to what can actually promote stale output: the session must be fully eligible (anything less is deleted by the ineligible cleanup in the same transaction, preserving the transient-flux reset contract) and the row must hold staged entries (an outage-failed row with nothing staged still never blocks). The retry unblocks it: a same-digest revisit refreshes entry context and re-stamps, a changed digest discards and re-extracts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 30 review response (as of 7440393)Finding verified and fixed. Stale failed-row staged output can promote (recall_extract.go) — fixed. Confirmed: the activation transaction's stale gate filtered Of the suggested options I took block-over-refresh: refreshing context at promotion time would still promote entries distilled from content written after the stamp (the same gate hole covers both), while blocking mirrors the existing done-stale semantics exactly and self-heals the same way — the retry either refreshes and re-stamps (same digest) or discards and re-extracts (changed digest), after which activation proceeds. The new gate is scoped to what can actually promote stale output, as its own query using the full eligibility predicate:
Tests, both written red-first: Verification: internal/db full suite green (including all prior activation-gate tests), internal/recall/extract green, cmd/agentsview only its known pre-existing failure; |
roborev: Combined Review (
|
…status Transport errors from the HTTP client were wrapped verbatim. Their text can quote bytes the server chose: a malformed redirect Location header is parsed before the CheckRedirect policy runs and echoed raw by Go's parse error, and a malformed HTTP response is quoted verbatim. A server that knows the endpoint credential could reflect it there, bypassing the redirect-target and response-body redaction on the way to CLI output, scheduler logs, and persisted failure rows. Transport-error detail now follows the response-body policy: withheld for credentialed endpoints, control-stripped and bounded at 200 runes otherwise. Context cancellation and deadline errors pass through — their text is fixed by the runtime and says why the request ended. Redirect refusals are unchanged: RefuseRedirects already redacts its target, and its sentinel stays on the chain for endpoint-scoped classification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 31 review response (as of 8641b26)Finding verified and fixed. Credential reflection through transport errors (client.go) — fixed. Confirmed, and the red test displayed the leak verbatim: Transport-error detail now follows the established response-body policy via a new
Classification is unaffected: everything on this path except a redirect refusal was and remains transient, so dropping the foreign cause from the chain loses nothing any caller inspects (the retry loop watches Tests, red-first with the reflected token visible in the failure output: Verification: internal/recall/extract full suite green (including all prior redirect, withholding, and known-API-path diagnostic tests); |
roborev: Combined Review (
|
… rows
Two review findings fixed.
Body-read errors were wrapped verbatim, and Go quotes raw wire bytes in
them: a malformed chunked trailer line is echoed whole ('malformed MIME
header: missing colon: "<raw line>"'), carrying whatever the server —
or a broken proxy — put on the wire into doctor output, scheduler logs,
and persisted failure rows. Read errors now follow the same policy as
transport errors: withheld for credentialed endpoints, control-stripped
and bounded otherwise, context errors passing through.
Pending and partial rows for transiently ineligible sessions could
block activation forever: candidate selection skips such sessions,
reconciliation clears only hard-ineligible rows, and both maybeActivate
(raw pending/partial stats) and the activation transaction's unfinished
gate (not-hard-ineligible filter) counted them — so a session that
reopened and never ended again stalled activation with no path out.
The unfinished gate now counts only fully eligible sessions, letting
the in-transaction cleanup delete unfinishable rows with their staged
output for rediscovery once the session settles, and maybeActivate
defers to the eligibility-aware backlog probe and the transactional
gates instead of refusing on raw counts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 32 review response (as of 8efab41)Both findings verified and fixed, with a threat-model note on the first. 1. Body-read errors quote raw wire bytes (client.go) — fixed, with the rationale reframed. The mechanism is real and I confirmed it empirically before writing the test: Go's chunked reader echoes a malformed trailer line whole — 2. Transient ineligibility can block activation indefinitely (manager.go / recall_extract.go) — fixed. Confirmed on all three legs: candidate selection applies full eligibility (skips the session), Verification: internal/db, internal/recall/extract, internal/server, internal/service suites green; cmd/agentsview only its known pre-existing failure; |
roborev: Combined Review (
|
…ze ended_at
Three review findings fixed, plus a latent hole the second one exposed.
Secret rescans ran per message, but units join adjacent assistant
messages before they are sent: a PEM block whose BEGIN and END land in
different rows matches no per-message scan — stored findings and the
extraction-time rescan alike — while the joined text the model would
receive contains the whole key. The manager now also scans every
constructed unit text, exactly the payloads a request would carry, and
fails the session closed before any model call.
A refused redirect's error named its target, and a redirect can put the
endpoint credential in the target hostname, which RedactedEndpoint
preserves. Credentialed endpoints now keep only the refusal sentinel;
credential-free ones keep the redacted target, bounded at 200 runes.
Fixing this exposed that the no-redirect policy lived solely in the
daemon's wiring: a Client built without an explicit HTTPClient followed
redirects, replaying the extraction POST wherever the endpoint pointed.
httpClient() now installs RefuseRedirects on its fallback and fills a
missing CheckRedirect on caller-provided clients (on a copy).
Quiet-period eligibility compared ended_at strings lexically against a
fixed-millisecond cutoff, but RFC3339Nano trims trailing zeros: a
trimmed value in the cutoff's second sorts after it ('Z' beats any
digit), so an eligible session was skipped while the discovery
watermark — advanced to pass start minus the quiet period — could pass
its last write, stranding it until a daemon restart resets watermarks.
The shared eligibility predicate now normalizes ended_at to fixed
milliseconds via strftime before comparing; the activation cleanup's
second format pass over the rendered predicate became concatenation so
the strftime verbs survive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 33 review response (as of 3e9560b)All three findings verified and fixed. The second one exposed a latent hole worth its own mention, and the third overturns a round-28 decline. 1. Unit-straddling secrets evade per-message scans (manager.go) — fixed. Confirmed, and this one needs no hostile endpoint — it's the privacy boundary against accidental leakage, squarely in the threat model: units join adjacent assistant messages with 2. Redirect target hostname can carry the credential (client.go) — fixed, and the policy hole it sat on too. Confirmed: 3. Mixed-precision ended_at comparison (recall_extract.go) — fixed, reversing my round-28 decline. Round 28's version of this finding lacked the consequence that makes it matter; this round's has it. The anomaly is real but sub-second (RFC3339Nano trims trailing zeros, and a trimmed value in the cutoff's second sorts after it — Verification: internal/db, internal/recall/extract, internal/server, internal/service green; cmd/agentsview only its known pre-existing failure; |
roborev: Combined Review (
|
…plits Three review findings fixed. The extraction-time secret rescan ran per message and per unit, but a secret whose structure spans them — a private-key block split across adjacent messages, or across separate units the endpoint receives and can correlate — matched neither. Scanning the formatted unit texts does not close it: the interposed unit formatting pushes a straddling key under the scanner's base64 payload-purity gate. The rescan now runs over the raw message contents concatenated in transcript order, which subsumes the per-message and per-unit checks and reconstructs a split key cleanly. The session fails closed on a match. Retraction of generated entries was gated on extraction being enabled: a generation activated while [recall.extract] was on keeps serving after it is turned off, so a source session later trashed, flagged automated, or found to carry secrets kept serving its entries with no reconciliation. The daemon now runs a reconcile-only scheduler when extraction is disabled but a generation exists (nil when none does), driven by the same startup pass, session-mutation notifications, and periodic ticks, using a new Reconciler that needs no model client. Overflow recovery split an oversized unit recursively with no bound: a single message far larger than any window — user content is never packed, so its length is unbounded — could fan out one model call per split leaf and hold every leaf's entries in memory. distillSplit now shares a call counter across the recursion and fails the session closed with ErrSplitBudgetExceeded once it reaches the budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 34 review response (as of 35c6d95)All three findings verified and fixed. The first two were real gaps; the third I scoped to the concrete harm and declined the provenance-changing part with rationale. 1. Secret spanning units bypasses the outbound gate (high) — fixed, and the obvious fix doesn't work. Confirmed the round-33 per-unit scan still misses a key split across separate units (two user messages → two intent units → the endpoint correlates them). The natural fix — scan the concatenated unit texts — I implemented and it failed the red test: the scanner's private-key rule has a base64 payload-purity gate (≥99%), and the interposed unit formatting ( 2. Disabling extraction disables retraction (medium) — fixed via independent retraction. Confirmed: 3. Unbounded overflow-recovery work (medium) — bounded; message-splitting declined. Confirmed the concrete harm: one oversized message becomes one unit (user content is never packed, so unbounded in length), and Verification: internal/recall/extract, internal/db green; cmd/agentsview only its known pre-existing failure; On the round count: this slice has taken many rounds, and the remaining findings are getting narrower (spanning-secret variants, precision edges, lifecycle corners). I still think the branch is merge-worthy and would benefit from the squash before merge we discussed. Happy to keep going as long as findings keep landing. |
roborev: Combined Review (
|
…ation Two review findings fixed. The aggregate outbound secret scan joined messages with a newline, so a single-token credential split mid-token across adjacent messages stayed undetected — the newline breaks the token for a regex that needs contiguous characters, while both fragments still reach the model endpoint, which can reconstruct them. The scan now also checks a separator-free join, which reconstructs the token; the newline join is kept for multi-line secrets whose structure it preserves. Activation promoted every archived unreviewed_auto entry, including one a reviewed replacement had superseded (archived with a superseded_by link). Reactivating the generation therefore served both the obsolete entry and its replacement. Promotion and the servable-count gate now exclude entries carrying a superseded_by link. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 35 review response (as of 9cbd2dc)Both findings verified and fixed. 1. Mid-token credential split across messages (high) — fixed. Confirmed: round 34's aggregate joined messages with 2. Reactivation promotes superseded entries (medium) — fixed. Confirmed the mechanism end to end: On the finding's "and related extraction lifecycle operations": I scoped the exclusion to promotion and the servable gate, which are where a superseded entry would re-enter service. Reconciliation and the stale gates operate on ineligibility and coverage staleness, not supersession, and deleting/reopening a superseded entry's row there would be out of their remit — supersession is a serving-state concern, and the two enforcement points above cover it. Happy to widen if you had a specific operation in mind. Verification: internal/recall/extract, internal/db green (all prior activation-gate and secret-scan tests included); |
roborev: Combined Review (
|
Two review findings fixed. The outbound aggregate secret scan concatenated the raw transcript rows, but the segmenter drops system messages and unsupported roles and trims each message before the model sees it. A credential split across user messages with an intervening system row, or with boundary whitespace, was therefore broken in the scan's aggregate while the endpoint still received the fragments contiguously and could reconstruct them. The scan now aggregates exactly the model-visible contents via a shared VisibleContents filter that the segmenter's Units also routes through, so the two cannot disagree about what is sent, and still scans both the newline-preserving and separator-free joins. A negative --limit passed CLI validation and reached the candidate query's "<= 0 means all" rule, scanning the entire eligible archive and causing unexpected model usage. The run command now rejects a negative limit at the boundary; zero remains the documented unlimited value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 36 review response (as of 9dc7739)Both findings verified and fixed. 1. Aggregate diverges from the model-visible sequence (high) — fixed. Confirmed the gap round 35 left: 2. Negative Verification: internal/recall/extract, internal/db green; cmd/agentsview only its known pre-existing failure; |
roborev: Combined Review (
|
Guards the decision not to scan the formatted unit text in the outbound secret gate. TurnsV1 prepends "ASSISTANT:\n", which the high-entropy-assignment rule reads as an assignment key, so scanning the formatted payload would flag any assistant message starting with a bare 20+ char high-entropy token — a git SHA, a hash, a base64 blob — as a secret and fail the session. Those are not secrets and are ubiquitous in coding transcripts. The gate scans the raw model-visible content, consistent with sync-time scanning; this test pins that a benign high-entropy token extracts normally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 37 review response (as of 6f6d249) — finding declined with evidenceScanning the formatted The
Git SHAs, hashes, base64 blobs, and UUIDs are ubiquitous as the first line of assistant messages in coding transcripts. Scanning the formatted payload would flag every such message as a secret and fail the session — a high-impact false-positive regression. This is also the exact failure mode round 34 moved away from (formatting polluting the scan), here in the false-positive direction. On the leak premise: a bare high-entropy token is not treated as a secret by design. The There's also a consistency argument: sync-time scanning, the I added If the real concern is bare unprefixed secrets, the right place to address it is the shared secret rules (so sync, search, and extraction stay consistent), not extraction-only scanning of text we formatted — happy to open that separately if you want it. No production change this round. On the broader thread: I agree we're into diminishing returns — the last several rounds have been narrowing variants of the same secret-gate and lifecycle areas, and this round's finding was a false-positive trap rather than a leak. I'd suggest this is a good point to squash and merge; I'll keep responding to any genuinely new findings. |
Builds on #1159's extraction store and client with the manager layer that drives model-backed recall extraction end to end.
Manager (
internal/recall/extract)ExtractCandidatesselects eligible sessions in SQL: ended past the quiet period, not automated, not trashed, zero secret findings, scanned under the current rules versions, and non-empty. The privacy predicates are not configurable, and the same checks guard explicit single-session runs so no path can feed an excluded session to the model. Failed sessions retry behind an updated_at-indexed backoff arm.Manager.RunPassdistills each candidate unit by unit, checkpointing a resumable cursor after every unit. Unit output commits under an in-transaction guard that re-verifies the session snapshot, eligibility, and absence of secret findings atomically with the insert; every Go-side check is advisory over that guard.sha256of generation fingerprint, session, unit, entry position), and the bulk insert skips existing ids, so replays after crashes or digest resets dedupe instead of duplicating.source_run_id, andunreviewed_autoreview state. While a generation is building, entries stage asarchivedand never serve.Activation
A generation auto-activates once everything eligible is done and it has produced entries; explicit
Activaterefuses an empty generation. The activation transaction re-verifies coverage — no eligible session pending/partial, unextracted, or with coverage stamped before its latest transcript write or under superseded scan rules — and aborts with a typed error rather than retiring the served corpus around a gap. It clears the staged output and progress of any session no longer fully eligible (trashed, reopened, awaiting rescan, or gone) so nothing stale serves and nothing strands archived, then promotes the rest atomically while retiring the previous generation.Credential handling
Endpoint URLs can carry credentials in userinfo, query values, bare query tokens, fragments, and path segments.
config.RedactedEndpointmasks all of these fail-closed (onlyapi-versionand known API-surface path vocabulary stay visible) for every error, log line, and stored failure row. Response bodies are attacker-influenced and can reflect the request: when the endpoint URL carries any credential material, all endpoint-provided diagnostic detail is withheld rather than scrubbed (re-encodings defeat literal replacement); credential-free endpoints keep a control-stripped, length-capped excerpt. Redirects are never followed — a redirect would replay transcript content to an attacker-chosen destination — and a refused redirect aborts the pass.Secrets integration
Transcript mutations revoke the session's secret-scan stamp in-write (
rulesAlgorithmVersion7), so appended content cannot ride an older scan's approval. The manager additionally rescans outbound text against the full ruleset before sending and fails closed on any match despite a current stamp.Config (
[recall.extract])Model identity (
model,deployment), named servers (transport only — moving a deployment to a new address does not orphan the corpus),quiet_period,backstop_interval,failure_backoff,max_window_chars,max_tokens, prompt profile/override-dir selection, and request-shape overrides. Validated at load, and the resolved request shape is validated at manager construction, so a bad profile fails setup before any progress rows exist. Disabled section stays inert.Daemon scheduler
Mirrors the embed scheduler: sync completions debounce into incremental passes; backstop ticks run full passes that revisit done sessions so grown transcripts are topped up via content-digest reset; with the backstop disabled a catchup ticker keeps incremental passes running. Every daemon lifetime starts with a full pass (deferred work survives daemon restarts), the pending startup pass and every running pass hold an idle-work lease so a detached daemon neither reaps itself mid-pass nor before its first pass, and no pass starts once the daemon is draining. Session-mutating server routes (trash, restore, delete, empty-trash, secret scan) notify the scheduler so eligibility changes are picked up without waiting for sync activity.
CLI
recall extractbecomes a parent command:run [--session <id>] [--full] [--limit N]— one pass;--sessionbypasses the quiet period but never the privacy filtersstatus— coverage per state, unit progress, entry count, generation listactivate/retire <fingerprint> [--force]— with the store's refusal guardsdoctor— prints the resolved model/server/profile/fingerprint and makes one probe call whose deadline derives from the configured server timeoutpreview --session <id>— the previous--dry-runchunk preview; the legacyextract --session --dry-runflags still work as a silent fallbackManual write commands refuse while a daemon owns the archive, since an enabled daemon runs passes itself.
Design notes live in
docs/internal/recall-extraction.md.Note:
TestDoSyncConfiguredFullUnifiedHTTPUsesManifestDeltaAndOrderedProgressfails on this machine on cleanorigin/mainas well; unrelated to this change.🤖 Generated with Claude Code