fix(memory): paginate memory lifecycle sweeps and stop dropping rows past the first page (salvage #772 + #776) - #785
Merged
Merged
Conversation
unohee
force-pushed
the
salvage/memory-lifecycle
branch
2 times, most recently
from
September 28, 2026 06:14
0313ab7 to
ca28e47
Compare
…past the first page Salvaged from draft PRs #772 and #776. Compaction, expiry cleanup, consolidation, and statistics all read the memory table with a single capped query, so larger stores were silently truncated: compaction refused outright at its 100k safety limit, while cleanupExpired, consolidateMemories and getMemoryStats quietly processed only the first 10k rows. They now read the complete table through memoryCore's paginated fetchAllTableRows / offset-limit pages. Deduplication is now bucket-based (stable FNV-1a hash of repo, type, derivedFrom and canonical metadata) instead of an O(n^2) pairwise scan, which matters now that the full record set is considered at once. Bucketing also guarantees duplicate pairs that straddle a page boundary meet: with the old page-by-page filtering they could not be compared. Within a bucket, records are ranked by importance then recency so the survivor is independent of page order. Also fixes the type defect in #772 where cosineSimilarity was declared : boolean while returning a similarity score, which broke the >= threshold comparison at the consolidation call site. Dropped from the drafts: getTable(tempTableName)/db.renameTable() (neither exists in the current memoryCore/lancedb API), the removals of withMemoryMutationLock/saveConversation/getRecentConversations/ runBackgroundCognition/getMemoryStats (main's API, still called by discordCore, support/chatMemory and support/web), package.json version churn, and the unconditional error-swallowing catch in compactMemoryTable that main deliberately rethrows.
unohee
force-pushed
the
salvage/memory-lifecycle
branch
from
September 28, 2026 06:15
ca28e47 to
65d7a58
Compare
This was referenced Sep 28, 2026
Merged
unohee
added a commit
that referenced
this pull request
Sep 29, 2026
…dict reuse (#794) * feat(agents): port harness agent patterns — advisor role + declarative per-role tool/effort scoping Cherry-picked onto origin/main (the original branch drifted 10 commits). Conflicts were additive on both sides (main's --harness-only path + this advisor wiring) and were resolved by keeping both. * fix(ledger): recovery is idempotent — a recomputed completion effect no longer kills the heartbeat (AGT-4518) Cherry-picked onto origin/main. Verified main still has the defect: recoverPublishedRun compares payload_json (~runLedger.ts:1071) and reconcileDurableArtifacts has no per-row isolation. * fix(taskSource): paginate the local queue instead of silently dropping past 200 (AGT-3421) Cherry-picked onto origin/main (main still has the fixed limit:200/offset:0). * fix(adapters): name protectedFiles/forbidPublication in the delegated-CLI dropped-options warning (AGT-4444) Cherry-picked onto origin/main; main warns for mcpTools/coordinationContext only. * feat: port the still-unique half of the audit remediation onto main Main advanced 10 commits while `feat/omp-agent-patterns` was open, and its salvage PRs (#785-#791) fixed several of the same issues independently — its own way. Rather than rebase 10 commits through wide conflicts, this branch was recreated from `origin/main` and only the parts main does NOT already have were ported. Every ported item was checked against main first, with the evidence recorded, and where main's version was newer or better (memory pagination, the tester prompt bound, TUI width clipping, IPv6 callbacks, pipeline embed budgets) main's version was kept and nothing was ported. Still missing on main, now ported: - `shellCommandGuard.ts` — the guard is lexical on main (`BLOCKED_COMMANDS`), so 6 of 21 destructive forms execute (`r{m,} -rf`, `$'\x72\x6d' -rf`, `r"m" -rf`, `git clean -fdx`, …) while 4 of 51 harmless mentions are falsely blocked. Replaced with a bash-faithful resolver: 72/72 verdicts correct. - `read_file` — main reads the whole file before slicing; a 512 MiB file with `limit=1` throws `RangeError: Invalid string length`. Now a bounded window. - `recoverPublishedRun` idempotency + per-row reconcile isolation (AGT-4518): main still compares `payload_json`, so a recovery of an already-enqueued completion throws and kills the heartbeat. - `taskSource` pagination (AGT-3421): main still caps the local queue at 200. - Knowledge scanning incompleteness (AGT-3490): a walk stopped by depth, timeout or size still persists a graph indistinguishable from a complete one. - `gitInfo` discovery: main misses staged-but-uncommitted files entirely and newline-splits the log query, so a path containing a newline is corrupted. Also main strips a leading `\n` from EVERY filename token, though real `git log -z` prefixes only the first — verified against real git bytes. - String-aware JSON extraction for auditor/documenter (main is brace-blind), and the documenter prompt now delimits untrusted task/worker text. - Verification-evidence log bound: main joins every failing log before capping, so one suite's output displaces another's and the total is measured post-join. - Memory: LanceDB returns the vector column as an Arrow `Vector`, which main's `normalizeRecords` zeroes — embeddings are destroyed on rewrite and cosine similarity over stored rows is NaN, so dedup matched NOTHING. Confirmed against a live store. Plus the survivor refusal bound and non-quadratic consolidation (main's is still all-pairs under the global write lock). - Automation: `fixOne` bypasses the PR lease; decomposition capacity reads a PROCESS-LOCAL counter, so two real processes are both granted a slot against a cap of 1 (reproduced with real spawned processes). - Daily reporter: main republishes every already-succeeded project on retry. - Python task-state strictness (main's own test file is currently red: 5 failed), CI-wait duration validation, verify-manifest command/runtime caps, and the notifier delegating to the shared predicate — with TEST-NET-2/3 added so delegating does not regress what main's local table rejected. - CLI output bound, tester/dashboard hardening, Retry-After HTTP-date parsing where main still parses with `parseInt`, and the delegated-CLI warning now naming `protectedFiles`/`forbidPublication` in addition to the tool list. - `advisor` role + declarative per-role `tools`/`effort` (the harness patterns), which main does not have at all. Verification: `tsc` clean, `oxlint` clean, full suite 7167 passing. Each ported item carries a test that fails against `origin/main` and passes here; several were additionally mutation-checked (removing the bound/coercion/bucket key makes the named test fail). * fix(review --max): make the audit verdict independent of --concurrency (AGT-4597) `--concurrency` was a partition input on the audit path, not just a resource knob: `balanceAreasToConcurrency` re-split the source with a smaller per-area cap until `areas.length >= concurrency`, and `reviewMaxCommand` called it. So the same files and the same reviewer were judged in 2 units at concurrency 1 and 10 units at concurrency 8 — and since `aggregateAuditResults` is worst-wins (`decision: failed ? 'reject' : worst`), a finer split could only turn an approve into a revise/reject. Reproduced deterministically (20 files in one dir, cap 12, a reviewer that judges the unit it is handed): concurrency 1 -> 2 areas -> approve; concurrency 8 -> 10 areas -> reject. No LLM call, no timing. The feature that introduced it, INT-2249 / PR #183, records its own verification as "2 dirs, 10 files: concurrency 2 -> 2 areas, concurrency 8 -> 10 areas". The audit now partitions from `--max-files-per-area` alone via `planAuditAreas`; `--concurrency` only decides how many reviewers run at once. The capability INT-2249 wanted is kept, not dropped: the finer, more parallel fan-out is reached deterministically with `--max-files-per-area <n>` (cap 2 yields the same 10 areas the concurrency-8 path fabricated), now reproducible at any concurrency. The `--fix` path keeps `balanceAreasToConcurrency` unchanged — there is no verdict there and more areas just mean more parallel fix workers. Verified on the real CLI (`--dry-run` over this repo): 478 files -> 61 areas at concurrency 1, 8 and 32 alike; cap 4 -> 134 areas at 4 and 32. The regression test drives the real command entry and fails when the partition is re-derived from concurrency. * release: 0.24.4 — durable review-verdict reuse (Tier 1) Promotes the Unreleased section to 0.24.4 and adds the durable review-verdict store: a review of content this deployment already reviewed is replayed from `(kind, base, content digest)` instead of paying for the reviewer again, and the store survives a daemon restart. Measured over the recorded history (530 records, 512 with a verdict): 68 byte-identical pairs, 15 same-mode, 10 repeating the same verdict — ~10 of 512 reviews, at ~$0.26 and 93s p50 per reviewer call. Proven across two separate processes: 27.0s / 6 model calls became a 1.58s replay with zero model calls, same verdict. The advisor pass still runs over a reused verdict, so a replay can tighten but never fail open. Also corrects the README's `review --max` claim that areas "auto-split to fill --concurrency" — the partition has been derived from --max-files-per-area alone since AGT-4597, so --concurrency no longer changes the verdict.
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.
Salvages the memory-lifecycle work from draft PRs #772 and #776 into one coherent change. Both drafts were rewritten from a stale base; only the substantive hunks were lifted onto current
origin/main, with main's API surface preserved.What was kept
Pagination (from #772) — every memory-lifecycle sweep read the table with a single capped query, so larger stores were silently truncated:
compactMemoryTable.limit(100_000)+ hard refusal at the limittable.query()cleanupExpired.limit(10_000)fetchAllTableRowsconsolidateMemories.limit(10_000)vector queryfetchAllTableRowsgetMemoryStats.limit(10_000)vector queryfetchAllTableRowscompactMemoryTable's read path no longer refuses at a safety limit, because it no longer loads the whole table in one query.shouldCompact(from #772) — usescountRows()for a bounded total and estimates waste from a single sampled page instead of loading the table; legacy v2 columns are now detected from the table schema rather than from row values, where they were only ever visible on v2 rows.Bucket dedup (from #776) —
removeDuplicatespreviously did an O(n²) pairwise scan. It now buckets records by a stable FNV-1a hash of their identity fields (repo,type,derivedFrom, canonical metadata) and compares vectors only within a bucket. This is what makes cross-page deduplication correct: with the old pairwise scan over page-filtered input, a duplicate pair split across a page boundary could never be compared. Within a bucket, records are ranked by importance then recency, so the surviving record does not depend on the order pages were read in.Type defect in #772 fixed —
cosineSimilaritywas declared: booleanwhile returning a similarity score, breaking the>= CONSOLIDATION_SIMILARITYcomparison at its call site (TS2365+TS2322on the draft head). It is correctly typed: numberhere.What was dropped
Junk / scratch —
package.json+package-lock.jsonversion churn (0.22.1 → 0.23.0, stale against main's0.24.3); no dependency changed, so no lockfile regeneration was needed.Dead code / wrong API (from #776's head) —
getTable(tempTableName)(the realgetTable()takes no argument),db.renameTable()(not on@lancedb/lancedb'sConnection), and its rewrite of the table-swap path.Main's API surface preserved (reverted from #776's head) — the deletions of
withMemoryMutationLock,saveConversation,getRecentConversations,runBackgroundCognitionandgetMemoryStats. All are still exported by main and still called bydiscordCore.ts,support/chatMemory.tsandsupport/web.ts; the draft head would not have compiled.Main's hardening kept —
compactMemoryTablestill rethrows on failure (the draft's unconditionalcatch → return {0,0,0,0}would have swallowed real errors), andconsolidateMemoriesstill runs underwithMemoryMutationLock(the draft wrapped it inwithMemoryWriteRetry, which deadlocks here — theupdateMemoryRecord/deleteMemoryIdscalls inside it each acquirewithMemoryWriteLock).Verification
npx tsc --noEmit— clean.npx vitest run src/memory/compaction.test.ts src/memory/memoryOps.test.ts src/core/service.test.ts— 57 passed (src/core/service.test.tsexercises thecompactMemoryTable/shouldCompact/cleanupBackupFilesconsumers). Pre-commitoxlint(91 rules) and type check passed.Tests added
compaction.test.ts: a straddling-pair case — a 20,001-record input whose near-duplicate pair sits across the 10,000-record page boundary — plus a case asserting records that share identity metadata but have dissimilar vectors are not merged.An additional throwaway smoke test (not committed) exercised all four sweeps against a 25k-row fake table to confirm each now sees every row and merges the cross-page pair; it was removed after passing.
Closes the salvage of #772 and #776.