Context
PR #12137 makes the orchestrator reap duplicate Chroma daemons (singularity). Separately, a long-lived Chroma process accumulates two distinct problems: process-level cruft (memory/fd) and on-disk HNSW index fragmentation (we observed 307 segment dirs in the unified store this session). The operator does not want a Chroma daemon running unrecycled for long stretches (e.g. a month).
The Problem
A never-recycled Chroma daemon (a) carries process-level cruft and (b) serves an increasingly fragmented on-disk index. SIGTERM does not work on chroma — empirically confirmed across hundreds of attempts — only kill -9 reliably stops it.
The Architectural Reality
- The orchestrator already supervises chroma as a continuous task (
ai/daemons/orchestrator/Orchestrator.mjs poll() + TaskDefinitions.mjs chroma task). Daemon age is trackable via taskState('chroma').lastRunAt (set at spawn; survives orchestrator restarts via persisted state).
- The recycle order MUST be kill → restart → defrag:
defragChromaDB.mjs extracts/rebuilds via the ChromaClient API and needs a LIVE daemon, so it runs against the fresh restarted process. kill+restart first heals a corrupted/hung process; defrag then compacts the index on the healthy daemon.
engines.chroma.{host,port} are the unified-store coordinates; Config.maintenance.defrag.intervalMs (7d) already exists as a maintenance-cadence precedent.
The Fix
- New config
orchestrator.chroma.maxRuntimeMs (+ env NEO_CHROMA_MAX_RUNTIME_MS; concrete default in the template, e.g. 24h).
- Orchestrator: when
now - taskState('chroma').lastRunAt > maxRuntimeMs, run the chain — kill -9 the tracked daemon → existing supervisor respawn → once the fresh daemon is chromaConnect-ready, run the safe defrag.
- Prerequisite (dependency): a unified-store-safe defrag. Today's
defragChromaDB.mjs is unsafe for the unified store — its step-6 orphan-cleanup (:471) deletes MC's segment dirs in the shared persist dir → MC data loss. The safe defrag must rebuild ALL collections (KB + MC) and never delete sibling-collection dirs; ideally shadow-swap (rebuild into a shadow collection, atomic-swap) for zero/minimal downtime — KB already has shadow-swap machinery; MC would need it for the unified rebuild.
- Gated by
chromaDaemonEnabled (off in cloud, where chroma is externally managed).
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
orchestrator.chroma.maxRuntimeMs (config) |
ai/config.template.mjs (+ NEO_CHROMA_MAX_RUNTIME_MS) |
new; concrete default ~24h |
n/a (new) |
config JSDoc |
new |
defragChromaDB.mjs unified-store safety |
ai/scripts/maintenance/defragChromaDB.mjs:471 |
step-6 orphan-cleanup must preserve ALL collections' segment dirs in a shared instance |
current behavior = MC data loss |
JSDoc |
read 2026-05-28 |
Decision Record impact
aligned-with ADR 0003 (unified Chroma topology).
Acceptance Criteria
Out of Scope
- The kill-dups singularity net (PR #12137).
- External-only mode / removing
manage_database etc. (separate ticket).
Related
- #12136 / PR #12137 (kill-dups singularity — this builds on it).
- Sibling: "KB + MC external-only Chroma; orchestrator as single source of truth".
Origin Session ID: 51ac9efd-4b5a-488e-b3dd-aa2673acbec9
Filed by @neo-opus-ada.
Context
PR #12137 makes the orchestrator reap duplicate Chroma daemons (singularity). Separately, a long-lived Chroma process accumulates two distinct problems: process-level cruft (memory/fd) and on-disk HNSW index fragmentation (we observed 307 segment dirs in the unified store this session). The operator does not want a Chroma daemon running unrecycled for long stretches (e.g. a month).
The Problem
A never-recycled Chroma daemon (a) carries process-level cruft and (b) serves an increasingly fragmented on-disk index. SIGTERM does not work on chroma — empirically confirmed across hundreds of attempts — only
kill -9reliably stops it.The Architectural Reality
ai/daemons/orchestrator/Orchestrator.mjspoll()+TaskDefinitions.mjschroma task). Daemon age is trackable viataskState('chroma').lastRunAt(set at spawn; survives orchestrator restarts via persisted state).defragChromaDB.mjsextracts/rebuilds via the ChromaClient API and needs a LIVE daemon, so it runs against the fresh restarted process. kill+restart first heals a corrupted/hung process; defrag then compacts the index on the healthy daemon.engines.chroma.{host,port}are the unified-store coordinates;Config.maintenance.defrag.intervalMs(7d) already exists as a maintenance-cadence precedent.The Fix
orchestrator.chroma.maxRuntimeMs(+ envNEO_CHROMA_MAX_RUNTIME_MS; concrete default in the template, e.g. 24h).now - taskState('chroma').lastRunAt > maxRuntimeMs, run the chain —kill -9the tracked daemon → existing supervisor respawn → once the fresh daemon ischromaConnect-ready, run the safe defrag.defragChromaDB.mjsis unsafe for the unified store — its step-6 orphan-cleanup (:471) deletes MC's segment dirs in the shared persist dir → MC data loss. The safe defrag must rebuild ALL collections (KB + MC) and never delete sibling-collection dirs; ideally shadow-swap (rebuild into a shadow collection, atomic-swap) for zero/minimal downtime — KB already has shadow-swap machinery; MC would need it for the unified rebuild.chromaDaemonEnabled(off in cloud, where chroma is externally managed).Contract Ledger
orchestrator.chroma.maxRuntimeMs(config)ai/config.template.mjs(+NEO_CHROMA_MAX_RUNTIME_MS)defragChromaDB.mjsunified-store safetyai/scripts/maintenance/defragChromaDB.mjs:471Decision Record impact
aligned-with ADR 0003(unified Chroma topology).Acceptance Criteria
orchestrator.chroma.maxRuntimeMsconfig + env binding; concrete default in the template.maxRuntimeMs, in kill→restart→defrag order, gated bychromaDaemonEnabled.kill -9(SIGTERM is not attempted — chroma ignores it).UNIT_TEST_MODE(no real process touched).maxRuntimeMsis recycled and the store is compacted afterward.Out of Scope
manage_databaseetc. (separate ticket).Related
Origin Session ID: 51ac9efd-4b5a-488e-b3dd-aa2673acbec9
Filed by @neo-opus-ada.