Context
During the 2026-06-18 Chroma integrity follow-up, the malformed FTS5 index investigation surfaced a separate safety issue in ai/scripts/maintenance/defragChromaDB.mjs: the script buffers collection data, deletes canonical collections, recreates them, and reloads the buffered rows. A normal run can complete successfully, but a hard kill or process crash in the delete/recreate/reload window can leave a canonical collection missing or only partially restored until an operator rebuilds/restores it.
This is not the same bug as #13467. #13467 tracks the malformed Chroma internal FTS5 index and explicitly keeps defrag redesign out of scope. #13469 adds only the copy-first diagnostic script.
Live latest-open sweep: checked the latest 20 open GitHub issues at 2026-06-18T13:54:02Z; adjacent tickets #13466, #13467, and #13469 exist, but no equivalent defrag-interruption hardening ticket was found.
A2A in-flight claim sweep: checked latest 30 messages via list_messages({status:'all', box:'all', limit:30}); only my related #13466/#13467/#13469 Chroma lane messages were present, with no competing [lane-claim] or [lane-intent] for defrag interruption hardening.
Local exact sweep: searched resources/content/issues, resources/content/discussions, ai, learn, and .agents for defrag hard kill, delete/recreate, canonical collection absent, shadow-swap defrag, and defragChromaDB; found existing defrag references and adjacent architecture docs, but no ticket for this failure mode.
KB semantic sweep: attempted ask_knowledge_base for the defrag/shadow-swap pattern, but the local KB MCP was stale/unavailable (Failed to access collections: The requested resource could not be found, runtimeFreshness.openApiDigest=true). I did not rely on KB output for this ticket.
Release classification: post-release operational hardening by default; boardless unless the operator promotes Chroma maintenance safety to release-blocking.
The Problem
defragChromaDB.mjs currently implements a "Nuke and Pave" collection rewrite:
- It creates a physical pre-nuke snapshot of the unified Chroma persist directory.
- It extracts each target collection into an in-memory buffer.
- It deletes the canonical collection(s) through the raw Chroma client.
- It recreates each canonical collection and reloads the buffered data.
- It cleans orphaned segment dirs and runs SQLite
VACUUM.
The hazardous window is between canonical delete and successful full reload. If the process is killed there, the snapshot exists under dist/chromadb-backups/<target>/, but the live Chroma daemon can remain without the canonical collection or with an incomplete replacement. Nothing writes a durable phase marker, nothing resumes or rolls back automatically, and a later healthcheck may only report that the collection is missing.
The risk is sharper in local Neo than it first appears because the orchestrator can automatically run the KB defrag after recycling Chroma. AiConfig.orchestrator.chroma.maxRuntimeMs defaults to a nonzero value, Orchestrator.poll() sets _chromaDefragPending after Chroma recycle, and the chromaDefrag task invokes defragChromaDB.mjs --target knowledge-base after the fresh daemon is connection-ready.
The Architectural Reality
Relevant anchors:
ai/scripts/maintenance/defragChromaDB.mjs lines 361-373 create a defrag-private physical snapshot, not the canonical JSONL backup.
ai/scripts/maintenance/defragChromaDB.mjs lines 472-502 delete canonical collections, then recreate them under the same names.
ai/scripts/maintenance/defragChromaDB.mjs lines 535-549 clean orphaned segment dirs and run VACUUM after the rewrite.
learn/agentos/decisions/0017-chroma-single-flat-unified-store.md defines one flat chroma/unified store shared by KB and MC; recovery must be collection-scoped, not folder deletion.
learn/agentos/tooling/RestorationRunbook.md states KB is a rebuildable cache but MC is an irreplaceable store; both live inside the same unified Chroma store.
ai/services/knowledge-base/VectorService.mjs already has a safer KB full-corpus pattern: build a shadow collection, rename live to parking, rename shadow to canonical, and keep the parked live collection as a rollback artifact.
ai/services/knowledge-base/ChromaManager.mjs blocks cold-cache empty canonical recreation while active shadow/parking collections exist.
ai/services/shared/vector/chromaClientPrimitives.mjs has a guarded delete wrapper for canonical collection deletes, but defragChromaDB.mjs currently uses the raw client.deleteCollection() path directly.
ai/daemons/orchestrator/taskDefinitions.mjs defines the one-shot chromaDefrag task as defragChromaDB.mjs --target knowledge-base.
This means the correct fix is not a folder-level restore or a generic SQLite repair. It is a collection-scoped interruption-safe rewrite strategy, with different treatment for KB-as-cache and MC-as-store.
The Fix
Harden defrag so canonical collections are never absent for the duration of a full extraction/reload.
Expected shape:
- For
--target knowledge-base, replace delete/recreate with a shadow/parking promotion model: load extracted data into a process-unique shadow collection, validate count/sample reads, then perform the bounded rename promote (live -> parking, shadow -> canonical) already used by KB shadow-swap semantics.
- For
--target memory-core, either implement a safe multi-collection shadow/parking set with all collection counts validated before promotion, or fail closed behind an explicit operator-gated recovery mode until such a collection-set promotion exists. Do not delete neo-agent-memory, neo-agent-sessions, or neo-native-graph before replacements are fully loaded and validated.
- Add durable phase metadata for any non-atomic promotion window: target, source counts, shadow names, parking names, snapshot path, phase, and timestamp. A subsequent invocation should detect an incomplete prior run and either refuse with exact recovery instructions or continue/rollback only when the state is unambiguous.
- Ensure orchestrator-triggered
chromaDefrag uses the hardened path and does not auto-launch the old delete/recreate behavior.
- Keep #13467's FTS5 repair separate; this ticket does not assume defrag fixes SQLite
embedding_fulltext_search corruption.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
npm run ai:defrag-kb |
package.json + defragChromaDB.mjs |
Rebuild KB through shadow/parking promotion, not canonical delete/recreate |
If promotion cannot be proven safe, abort before touching canonical |
Script JSDoc / maintenance notes |
Unit test proving canonical delete is not called before replacement validation |
npm run ai:defrag-memory |
package.json + Restoration Runbook MC-as-store model |
Protect MC collections from partial delete/reload loss |
Fail closed with operator guidance until multi-collection promotion is safe |
Restoration Runbook or script warning |
Unit test covering fail-closed or atomic set promotion behavior |
Orchestrator chromaDefrag task |
ai/daemons/orchestrator/taskDefinitions.mjs + Orchestrator.poll() |
Auto-defrag only invokes the hardened KB path |
Disable auto-defrag when safety preflight fails |
AiConfig/orchestrator comments if behavior changes |
Existing orchestrator recycle tests updated to assert safe task wiring |
| Defrag recovery state |
New or existing maintenance-state file under .neo-ai-data |
Detect incomplete prior promotion and surface exact recovery path |
Refuse rather than silently recreating empty canonical collections |
Script output + optional runbook note |
Crash-window unit coverage with fake Chroma registry |
| FTS5 malformed-index repair |
#13467 / #13469 |
Remains separate from defrag interruption hardening |
Use copy-first diagnostic and repair ticket flow |
#13467/#13469 |
No FTS repair claims in this ticket's PR |
Decision Record impact
aligned-with ADR 0017. The fix must preserve the single flat unified Chroma store and strengthen collection-scoped recovery inside it.
Acceptance Criteria
Out of Scope
- Repairing
malformed inverted index for FTS5 table main.embedding_fulltext_search; that remains #13467.
- Adding more healthcheck work to KB/MC hot paths.
- Folder-level deletion or restore of
.neo-ai-data/chroma/unified; the unified store also contains MC data.
- Reworking Chroma's internal SQLite schema.
Avoided Traps
- Do not treat the defrag-private physical snapshot as a canonical backup substitute;
backup.mjs and restore.mjs own portable JSONL backup/restore.
- Do not fix KB only and assume the MC target is safe; MC is the irreplaceable store and has stricter loss semantics.
- Do not run
REINDEX, VACUUM, or manual SQLite repair as part of this ticket's proof. That belongs to #13467 after copy-first validation.
- Do not use raw
client.deleteCollection() against canonical production collections unless the new design proves the failure windows are safe.
Related
Related: #13466
Related: #13467
Related: #13469
Related: PR #13470
Origin Session ID
Origin Session ID: 4ce60429-2986-4543-be2d-741957c75b6c
Handoff Retrieval Hints
query_raw_memories: defragChromaDB nuke pave delete recreate collection hard kill restore window
query_raw_memories: shadow swap knowledge base canonical collection live parked rollback Chroma rebuild
query_raw_memories: Chroma FTS5 malformed inverted index quick_check repair defrag Error finding id
Context
During the 2026-06-18 Chroma integrity follow-up, the malformed FTS5 index investigation surfaced a separate safety issue in
ai/scripts/maintenance/defragChromaDB.mjs: the script buffers collection data, deletes canonical collections, recreates them, and reloads the buffered rows. A normal run can complete successfully, but a hard kill or process crash in the delete/recreate/reload window can leave a canonical collection missing or only partially restored until an operator rebuilds/restores it.This is not the same bug as #13467. #13467 tracks the malformed Chroma internal FTS5 index and explicitly keeps defrag redesign out of scope. #13469 adds only the copy-first diagnostic script.
Live latest-open sweep: checked the latest 20 open GitHub issues at
2026-06-18T13:54:02Z; adjacent tickets #13466, #13467, and #13469 exist, but no equivalent defrag-interruption hardening ticket was found.A2A in-flight claim sweep: checked latest 30 messages via
list_messages({status:'all', box:'all', limit:30}); only my related #13466/#13467/#13469 Chroma lane messages were present, with no competing[lane-claim]or[lane-intent]for defrag interruption hardening.Local exact sweep: searched
resources/content/issues,resources/content/discussions,ai,learn, and.agentsfordefrag hard kill,delete/recreate,canonical collection absent,shadow-swap defrag, anddefragChromaDB; found existing defrag references and adjacent architecture docs, but no ticket for this failure mode.KB semantic sweep: attempted
ask_knowledge_basefor the defrag/shadow-swap pattern, but the local KB MCP was stale/unavailable (Failed to access collections: The requested resource could not be found,runtimeFreshness.openApiDigest=true). I did not rely on KB output for this ticket.Release classification: post-release operational hardening by default; boardless unless the operator promotes Chroma maintenance safety to release-blocking.
The Problem
defragChromaDB.mjscurrently implements a "Nuke and Pave" collection rewrite:VACUUM.The hazardous window is between canonical delete and successful full reload. If the process is killed there, the snapshot exists under
dist/chromadb-backups/<target>/, but the live Chroma daemon can remain without the canonical collection or with an incomplete replacement. Nothing writes a durable phase marker, nothing resumes or rolls back automatically, and a later healthcheck may only report that the collection is missing.The risk is sharper in local Neo than it first appears because the orchestrator can automatically run the KB defrag after recycling Chroma.
AiConfig.orchestrator.chroma.maxRuntimeMsdefaults to a nonzero value,Orchestrator.poll()sets_chromaDefragPendingafter Chroma recycle, and thechromaDefragtask invokesdefragChromaDB.mjs --target knowledge-baseafter the fresh daemon is connection-ready.The Architectural Reality
Relevant anchors:
ai/scripts/maintenance/defragChromaDB.mjslines 361-373 create a defrag-private physical snapshot, not the canonical JSONL backup.ai/scripts/maintenance/defragChromaDB.mjslines 472-502 delete canonical collections, then recreate them under the same names.ai/scripts/maintenance/defragChromaDB.mjslines 535-549 clean orphaned segment dirs and runVACUUMafter the rewrite.learn/agentos/decisions/0017-chroma-single-flat-unified-store.mddefines one flatchroma/unifiedstore shared by KB and MC; recovery must be collection-scoped, not folder deletion.learn/agentos/tooling/RestorationRunbook.mdstates KB is a rebuildable cache but MC is an irreplaceable store; both live inside the same unified Chroma store.ai/services/knowledge-base/VectorService.mjsalready has a safer KB full-corpus pattern: build a shadow collection, rename live to parking, rename shadow to canonical, and keep the parked live collection as a rollback artifact.ai/services/knowledge-base/ChromaManager.mjsblocks cold-cache empty canonical recreation while active shadow/parking collections exist.ai/services/shared/vector/chromaClientPrimitives.mjshas a guarded delete wrapper for canonical collection deletes, butdefragChromaDB.mjscurrently uses the rawclient.deleteCollection()path directly.ai/daemons/orchestrator/taskDefinitions.mjsdefines the one-shotchromaDefragtask asdefragChromaDB.mjs --target knowledge-base.This means the correct fix is not a folder-level restore or a generic SQLite repair. It is a collection-scoped interruption-safe rewrite strategy, with different treatment for KB-as-cache and MC-as-store.
The Fix
Harden defrag so canonical collections are never absent for the duration of a full extraction/reload.
Expected shape:
--target knowledge-base, replace delete/recreate with a shadow/parking promotion model: load extracted data into a process-unique shadow collection, validate count/sample reads, then perform the bounded rename promote (live -> parking,shadow -> canonical) already used by KB shadow-swap semantics.--target memory-core, either implement a safe multi-collection shadow/parking set with all collection counts validated before promotion, or fail closed behind an explicit operator-gated recovery mode until such a collection-set promotion exists. Do not deleteneo-agent-memory,neo-agent-sessions, orneo-native-graphbefore replacements are fully loaded and validated.chromaDefraguses the hardened path and does not auto-launch the old delete/recreate behavior.embedding_fulltext_searchcorruption.Contract Ledger Matrix
npm run ai:defrag-kbpackage.json+defragChromaDB.mjsnpm run ai:defrag-memorypackage.json+ Restoration Runbook MC-as-store modelchromaDefragtaskai/daemons/orchestrator/taskDefinitions.mjs+Orchestrator.poll().neo-ai-dataDecision Record impact
aligned-with ADR 0017. The fix must preserve the single flat unified Chroma store and strengthen collection-scoped recovery inside it.
Acceptance Criteria
defragChromaDB.mjs --target knowledge-baseno longer deletes the canonical KB collection before a replacement collection is fully loaded and validated.chromaDefragcannot auto-run the old unsafe delete/recreate behavior.Out of Scope
malformed inverted index for FTS5 table main.embedding_fulltext_search; that remains #13467..neo-ai-data/chroma/unified; the unified store also contains MC data.Avoided Traps
backup.mjsandrestore.mjsown portable JSONL backup/restore.REINDEX,VACUUM, or manual SQLite repair as part of this ticket's proof. That belongs to #13467 after copy-first validation.client.deleteCollection()against canonical production collections unless the new design proves the failure windows are safe.Related
Related: #13466 Related: #13467 Related: #13469 Related: PR #13470
Origin Session ID
Origin Session ID: 4ce60429-2986-4543-be2d-741957c75b6c
Handoff Retrieval Hints
query_raw_memories:defragChromaDB nuke pave delete recreate collection hard kill restore windowquery_raw_memories:shadow swap knowledge base canonical collection live parked rollback Chroma rebuildquery_raw_memories:Chroma FTS5 malformed inverted index quick_check repair defrag Error finding id