Context
Operator report on 2026-06-28: after pulling dev and restarting the local orchestrator, the log showed the supervisor adopting an existing Chroma process that was still present at the OS process layer but functionally dead. The orchestrator then froze until Chroma was manually killed. This happened immediately after the recent v13.1 self-healing/orchestrator fixes, including #14291/#14292 (singleton-port defer adoption) and #14293/#14294 (Chroma recycle coordination with kbSync).
Fresh V-B-A from this session:
git log --oneline -8 shows #14294 and #14292 as the last relevant merged PRs before the report.
ps -axo pid,ppid,stat,command after the manual kill/restart showed one live orchestrator and one Chroma listener, so the current process table is no longer wedged.
lsof -nP -iTCP:8000 -sTCP:LISTEN showed Chroma listening on IPv6 [::1]:8000.
- Fetch probes verified the live daemon responds on
localhost / [::1] for /api/v2/heartbeat and /api/v2/healthcheck, while 127.0.0.1 fails for the current bind.
- Code inspection shows Chroma has
singletonPort and default SIGKILL duplicate reaping, but no livenessProbe or healthProbe in ai/daemons/orchestrator/taskDefinitions.mjs.
- Code inspection shows
ProcessSupervisorService.recoverTask() adopts a PID-file match using only process.kill(pid, 0) plus processCommand(pid).includes(expectedCommand); it does not prove the Chroma HTTP service is usable before setting running=true.
Live latest-open sweep: checked latest 20 open issues on 2026-06-28 before filing; no equivalent narrow ticket found. Closest open parent is #14039 (Agent OS Stability & Self-Healing). Recent A2A sweep (list_messages({status:'all', limit:30})) showed this is a follow-up to #14293/#14294, not a competing peer claim.
The Problem
The Chroma single-daemon machinery currently distinguishes duplicate processes, but not process-alive/service-dead adoption.
ProcessSupervisorService.recoverTask() treats a persisted chroma.pid as healthy when the PID exists and the command line contains chroma. For Chroma, that is insufficient: a hung or half-dead Chroma process can still satisfy process.kill(pid, 0) and match the command, while its HTTP API is unavailable or wedged. Once adopted, TaskStateService marks the task running. ProcessSupervisorService.superviseTask() then returns early for running tasks unless the task defines a healthProbe. Chroma defines none, so the supervisor never recycles the service-dead process.
This is exactly the worst failure mode for self-healing: the orchestrator believes it healed/adopted the daemon, but actually preserves the bad process and blocks the system until a human kills it.
The Architectural Reality
ai/daemons/orchestrator/services/ProcessSupervisorService.mjs:135-156 - PID-file recovery adopts any PID whose command includes expectedCommand; no readiness or health proof.
ai/daemons/orchestrator/services/ProcessSupervisorService.mjs:1120-1132 - killProcess() intentionally uses SIGKILL; this must stay true for Chroma because SIGTERM is unreliable.
ai/daemons/orchestrator/services/ProcessSupervisorService.mjs:1135-1149 - killTask() recycles a tracked task by SIGKILL + markRecycled; this is the right actuator for an adopted-but-dead Chroma process.
ai/daemons/orchestrator/taskDefinitions.mjs:153-165 - the Chroma task has command, args, pidFileName, expectedCommand, and singletonPort, but no livenessProbe or healthProbe.
- Closed
#12136 originally considered probe/host-pin for Chroma singularity, but the delivered fix pivoted to active duplicate reaping. That closed the duplicate-writer class, not the adopted service-dead class.
- Closed
#12138 added max-runtime Chroma recycle and preserved the invariant that Chroma recycle uses SIGKILL.
- Open
#14291 / merged #14292 added defer-policy adoption for externally owned port holders. Chroma must not become defer; it is orchestrator-owned and should keep authoritative SIGKILL reaping.
The Fix
Add Chroma service-health proof to the existing supervisor contract without reviving host-pin or defer-policy semantics.
Concrete target shape:
- Add a Chroma HTTP readiness/health helper in the orchestrator task-definition layer, using the v2 Chroma endpoint that the live process serves (
/api/v2/heartbeat or /api/v2/healthcheck) and host semantics that work for the current daemon (localhost/configured host, not hardcoded 127.0.0.1).
- Wire the Chroma task with a
healthProbe so an adopted/running-but-unhealthy Chroma process is recycled by the existing gateRecycleOnHealthProbe() -> killTask('chroma', 'supervisor-health-recycle') path.
- Consider adding a
livenessProbe for the not-running path only if it does not weaken the authoritative Chroma ownership model. A live matching Chroma listener with no tracked canonical PID should still be reconciled by the default policy, not silently treated like shared external infra.
- Add unit coverage for an adopted Chroma state (
running=true, pid set) whose health probe returns false: the supervisor must call killTask, which uses SIGKILL via killProcess, marks recycled, and allows the next poll to spawn fresh Chroma.
Decision Record impact
none - this is a behavior fix inside the existing orchestrator-owned Chroma lifecycle. It preserves the decisions from #12136 (authoritative duplicate reaping) and #12138 (SIGKILL recycle), and does not amend ADR authority.
Acceptance Criteria
Out of Scope
- Reverting
#14292 defer-policy adoption for dev-server/Neural Link Bridge.
- Reverting
#14294 heavy-maintenance lease coordination.
- Changing Chroma from orchestrator-owned to externally owned/shared-infra semantics.
- Re-architecting the full Chroma recycle/defrag cadence.
Avoided Traps
- Switching Chroma to graceful SIGTERM - rejected. Closed
#12138 and live operator evidence both say Chroma does not shut down reliably via SIGTERM; SIGKILL is the safety primitive.
- Making Chroma
defer because defer adoption was just added - rejected. defer is for shared external infra; Chroma writes the unified persist dir and must remain authoritative.
- Only checking TCP connect - insufficient. A listener can exist while the Chroma service is wedged. Prefer a Chroma HTTP heartbeat/healthcheck response.
- Reviving the old host-pin as the primary fix - rejected. Closed
#12136 pivoted away from host-pin; this ticket is about service-health after PID adoption.
Related
- Parent epic:
#14039 - Agent OS Stability & Self-Healing (v13.1).
#12136 - Chroma single-daemon guarantee; duplicate reaping delivered, service-dead adoption gap remains.
#12138 - Chroma max-runtime recycle; SIGKILL invariant.
#14291/#14292 - defer-policy port adoption for shared infra; not the Chroma ownership model.
#14293/#14294 - Chroma recycle coordination with kbSync; this is the next Chroma lifecycle regression.
Origin Session ID: 019f0f79-7af4-7a70-893a-dd58e03337d6
Handoff Retrieval Hint: "Chroma PID adoption process-alive service-dead healthProbe ProcessSupervisor recoverTask SIGKILL recycle"
Context
Operator report on 2026-06-28: after pulling
devand restarting the local orchestrator, the log showed the supervisor adopting an existing Chroma process that was still present at the OS process layer but functionally dead. The orchestrator then froze until Chroma was manually killed. This happened immediately after the recent v13.1 self-healing/orchestrator fixes, including#14291/#14292(singleton-port defer adoption) and#14293/#14294(Chroma recycle coordination with kbSync).Fresh V-B-A from this session:
git log --oneline -8shows#14294and#14292as the last relevant merged PRs before the report.ps -axo pid,ppid,stat,commandafter the manual kill/restart showed one live orchestrator and one Chroma listener, so the current process table is no longer wedged.lsof -nP -iTCP:8000 -sTCP:LISTENshowed Chroma listening on IPv6[::1]:8000.localhost/[::1]for/api/v2/heartbeatand/api/v2/healthcheck, while127.0.0.1fails for the current bind.singletonPortand default SIGKILL duplicate reaping, but nolivenessProbeorhealthProbeinai/daemons/orchestrator/taskDefinitions.mjs.ProcessSupervisorService.recoverTask()adopts a PID-file match using onlyprocess.kill(pid, 0)plusprocessCommand(pid).includes(expectedCommand); it does not prove the Chroma HTTP service is usable before settingrunning=true.Live latest-open sweep: checked latest 20 open issues on 2026-06-28 before filing; no equivalent narrow ticket found. Closest open parent is
#14039(Agent OS Stability & Self-Healing). Recent A2A sweep (list_messages({status:'all', limit:30})) showed this is a follow-up to#14293/#14294, not a competing peer claim.The Problem
The Chroma single-daemon machinery currently distinguishes duplicate processes, but not process-alive/service-dead adoption.
ProcessSupervisorService.recoverTask()treats a persistedchroma.pidas healthy when the PID exists and the command line containschroma. For Chroma, that is insufficient: a hung or half-dead Chroma process can still satisfyprocess.kill(pid, 0)and match the command, while its HTTP API is unavailable or wedged. Once adopted,TaskStateServicemarks the task running.ProcessSupervisorService.superviseTask()then returns early for running tasks unless the task defines ahealthProbe. Chroma defines none, so the supervisor never recycles the service-dead process.This is exactly the worst failure mode for self-healing: the orchestrator believes it healed/adopted the daemon, but actually preserves the bad process and blocks the system until a human kills it.
The Architectural Reality
ai/daemons/orchestrator/services/ProcessSupervisorService.mjs:135-156- PID-file recovery adopts any PID whose command includesexpectedCommand; no readiness or health proof.ai/daemons/orchestrator/services/ProcessSupervisorService.mjs:1120-1132-killProcess()intentionally usesSIGKILL; this must stay true for Chroma becauseSIGTERMis unreliable.ai/daemons/orchestrator/services/ProcessSupervisorService.mjs:1135-1149-killTask()recycles a tracked task by SIGKILL +markRecycled; this is the right actuator for an adopted-but-dead Chroma process.ai/daemons/orchestrator/taskDefinitions.mjs:153-165- the Chroma task hascommand,args,pidFileName,expectedCommand, andsingletonPort, but nolivenessProbeorhealthProbe.#12136originally considered probe/host-pin for Chroma singularity, but the delivered fix pivoted to active duplicate reaping. That closed the duplicate-writer class, not the adopted service-dead class.#12138added max-runtime Chroma recycle and preserved the invariant that Chroma recycle uses SIGKILL.#14291/ merged#14292addeddefer-policy adoption for externally owned port holders. Chroma must not becomedefer; it is orchestrator-owned and should keep authoritative SIGKILL reaping.The Fix
Add Chroma service-health proof to the existing supervisor contract without reviving host-pin or defer-policy semantics.
Concrete target shape:
/api/v2/heartbeator/api/v2/healthcheck) and host semantics that work for the current daemon (localhost/configured host, not hardcoded127.0.0.1).healthProbeso an adopted/running-but-unhealthy Chroma process is recycled by the existinggateRecycleOnHealthProbe()->killTask('chroma', 'supervisor-health-recycle')path.livenessProbefor the not-running path only if it does not weaken the authoritative Chroma ownership model. A live matching Chroma listener with no tracked canonical PID should still be reconciled by the default policy, not silently treated like shared external infra.running=true, pid set) whose health probe returns false: the supervisor must callkillTask, which uses SIGKILL viakillProcess, marks recycled, and allows the next poll to spawn fresh Chroma.Decision Record impact
none- this is a behavior fix inside the existing orchestrator-owned Chroma lifecycle. It preserves the decisions from#12136(authoritative duplicate reaping) and#12138(SIGKILL recycle), and does not amend ADR authority.Acceptance Criteria
killTask('chroma', ...)path; the stalerunning=truestate must not persist indefinitely.duplicateListenerPolicy: 'defer'.ProcessSupervisorService.spec.mjsand/orOrchestrator.spec.mjscovers running/adopted Chroma health failure -> recycle -> next-poll restart eligibility.localhost/[::1]succeeds;127.0.0.1may fail when Chroma binds IPv6-only).Out of Scope
#14292defer-policy adoption for dev-server/Neural Link Bridge.#14294heavy-maintenance lease coordination.Avoided Traps
#12138and live operator evidence both say Chroma does not shut down reliably via SIGTERM; SIGKILL is the safety primitive.deferbecause defer adoption was just added - rejected.deferis for shared external infra; Chroma writes the unified persist dir and must remain authoritative.#12136pivoted away from host-pin; this ticket is about service-health after PID adoption.Related
#14039- Agent OS Stability & Self-Healing (v13.1).#12136- Chroma single-daemon guarantee; duplicate reaping delivered, service-dead adoption gap remains.#12138- Chroma max-runtime recycle; SIGKILL invariant.#14291/#14292- defer-policy port adoption for shared infra; not the Chroma ownership model.#14293/#14294- Chroma recycle coordination with kbSync; this is the next Chroma lifecycle regression.Origin Session ID: 019f0f79-7af4-7a70-893a-dd58e03337d6
Handoff Retrieval Hint: "Chroma PID adoption process-alive service-dead healthProbe ProcessSupervisor recoverTask SIGKILL recycle"