Skip to content

fix(memory): paginate memory lifecycle sweeps and stop dropping rows past the first page (salvage #772 + #776) - #785

Merged
unohee merged 1 commit into
mainfrom
salvage/memory-lifecycle
Sep 28, 2026
Merged

unohee merged 1 commit into
mainfrom
salvage/memory-lifecycle

Conversation

@unohee

@unohee unohee commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

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:

Function Before After
compactMemoryTable .limit(100_000) + hard refusal at the limit offset/limit pages via table.query()
cleanupExpired .limit(10_000) full table via fetchAllTableRows
consolidateMemories .limit(10_000) vector query full table via fetchAllTableRows
getMemoryStats .limit(10_000) vector query full table via fetchAllTableRows

compactMemoryTable's read path no longer refuses at a safety limit, because it no longer loads the whole table in one query.

shouldCompact (from #772) — uses countRows() 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) — removeDuplicates previously 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 — cosineSimilarity was declared : boolean while returning a similarity score, breaking the >= CONSOLIDATION_SIMILARITY comparison at its call site (TS2365 + TS2322 on the draft head). It is correctly typed : number here.

What was dropped

Junk / scratch — package.json + package-lock.json version churn (0.22.1 → 0.23.0, stale against main's 0.24.3); no dependency changed, so no lockfile regeneration was needed.

Dead code / wrong API (from #776's head) — getTable(tempTableName) (the real getTable() takes no argument), db.renameTable() (not on @lancedb/lancedb's Connection), and its rewrite of the table-swap path.

Main's API surface preserved (reverted from #776's head) — the deletions of withMemoryMutationLock, saveConversation, getRecentConversations, runBackgroundCognition and getMemoryStats. All are still exported by main and still called by discordCore.ts, support/chatMemory.ts and support/web.ts; the draft head would not have compiled.

Main's hardening kept — compactMemoryTable still rethrows on failure (the draft's unconditional catch → return {0,0,0,0} would have swallowed real errors), and consolidateMemories still runs under withMemoryMutationLock (the draft wrapped it in withMemoryWriteRetry, which deadlocks here — the updateMemoryRecord/deleteMemoryIds calls inside it each acquire withMemoryWriteLock).

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.ts exercises the compactMemoryTable/shouldCompact/cleanupBackupFiles consumers). Pre-commit oxlint (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.

@unohee
unohee force-pushed the salvage/memory-lifecycle branch 2 times, most recently from 0313ab7 to ca28e47 Compare September 28, 2026 06:14
…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
unohee force-pushed the salvage/memory-lifecycle branch from ca28e47 to 65d7a58 Compare September 28, 2026 06:15
@unohee
unohee merged commit 13fe051 into main Sep 28, 2026
7 checks passed
@unohee
unohee deleted the salvage/memory-lifecycle branch September 28, 2026 06:23
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant