Scope Correction
Intake classified the original ticket as needs-narrowing. It combined two independently owned contracts:
- the Orchestrator must not record a safety-blocked child as ordinary success; and
- the deployment must give the compactor authoritative durable watermarks for every consumer class.
Current source and prior art make the first a coherent one-PR leaf. The second requires a separate ownership decision for mcp-notifications and a2a-webhook cursors. This corrected body owns scheduler outcome truth only. It must not synthesize or weaken consumer watermarks to make compaction green.
Context
#12329 established the fail-closed GraphLog compaction mechanism, and #12394 scheduled it as a recurring Orchestrator lane. Both predecessors remain resolved for the behavior they shipped.
At 2026-08-08T10:54:35Z, the canonical Docker Orchestrator started scheduled graphlog-compaction. One second later it recorded:
GraphLog compaction completed successfully.
The production compactor functions, run read-only against the Orchestrator's mounted volumes, returned a different maintenance outcome:
{
"canApply": false,
"cutoffLogId": 0,
"reason": "unknown-consumer-watermark"
}The same census measured 3,200,403 GraphLog rows and 13 active wake subscriptions. That row count is operational context, not a causal claim about #16677.
The Problem
runGraphLogCompaction() returns the fail-closed plan, but the CLI prints only a human-readable report and exits zero. The graphlog-compaction task definition does not capture structured stdout, and ProcessSupervisorService therefore advances the task through its ordinary completed path.
The green receipt means only that the child did not throw. It does not mean the maintenance contract reached an applicable cutoff, deleted rows, or established that no deletion was needed. This erases the exact safety-blocked state an operator needs to repair.
Architectural Reality
ai/scripts/maintenance/compactGraphLog.mjs already returns the complete plan and deletion counts. No second planner is needed.
- The CLI's default human-readable mode is useful for operators and must remain available.
ai/daemons/orchestrator/taskDefinitions.mjs opts memory-summary-backfill and kbSync into bounded stdout JSON capture, but not graphlog-compaction.
ai/daemons/orchestrator/services/ProcessSupervisorService.mjs already maps a structured boolean {deferred: true, reason} child result to markSkipped() for selected tasks. markSkipped() does not refresh lastSuccessAt.
graphlog-compaction is classified container-plane by taskAuthority.mjs, aligned with ADR 0014. This ticket does not change that authority.
The Fix
- Give the compaction CLI an explicit structured-output mode that emits exactly one bounded JSON outcome on stdout while preserving the existing human-readable default.
- Derive that outcome from the returned plan and counts, distinguishing at least:
applied — rows were safely deleted;
up-to-date — the authoritative plan was computable and no deletion was required; and
safety-blocked — an authoritative cutoff could not be computed.
- Opt the scheduled
graphlog-compaction task into stdout JSON capture.
- Map a
safety-blocked outcome to the existing skipped/non-success task-state path, retaining the plan reason and bounded counts in task health details.
- Preserve parse/telemetry fail-soft behavior and the compactor's fail-closed deletion invariant.
Contract Ledger
| Target Surface |
Authority |
Required Behavior |
Failure Posture |
Evidence |
| Compaction CLI output |
compactGraphLog.mjs |
Default human report remains; explicit structured mode emits one JSON outcome derived from the returned result |
Invalid invocation fails; a blocked plan is valid structured output, not an exception |
CLI subprocess tests |
| Scheduled child definition |
taskDefinitions.mjs |
graphlog-compaction requests structured capture |
Missing/malformed stdout remains observable and fail-soft |
Task-definition test |
| Child outcome classification |
ProcessSupervisorService |
Boolean deferred/safety-blocked outcome records skipped, never completed |
Does not refresh lastSuccessAt; reason and counts remain visible |
Supervisor exact-outcome tests |
| GraphLog deletion |
#12329 compaction plan |
No change to cutoff or delete authority |
Unknown consumer still blocks every deletion |
Existing compaction guard suite |
Decision Record Impact
Aligned with ADR 0014. The scheduled lane remains container-plane; this change makes its existing task outcome truthful and introduces no new authority class.
Acceptance Criteria
Out of Scope
- Creating, relocating, or synthesizing durable consumer watermarks.
- Deciding whether webhook routes share a service-level cursor or own per-route cursors.
- Changing Docker mounts or WakeSubscriptionService cursor ownership.
- Deleting the current 3.2M-row GraphLog as part of this implementation.
- Claiming GraphLog size caused #16677.
- Reopening #12329 or #12394.
Avoided Traps
- Child exit zero means maintenance succeeded. It proves only that the process did not throw.
- Safety-blocked should exit nonzero. The plan is a valid fail-closed result; the scheduler needs structured outcome semantics, not an invented process crash.
- Human logs are structured stdout. Mixing prose and JSON defeats the existing bounded parser.
- Make the ticket green by weakening the plan. Consumer-watermark uncertainty must continue to block deletion.
- A large GraphLog proves the current liveness root cause. It is adjacent retention evidence only.
Related
- Predecessor mechanism: #12329
- Predecessor scheduler: #12394
- Deferred-as-completed precedent: #13755
- Adjacent live liveness investigation: #16677
- ADR authority:
learn/agentos/decisions/0014-cloud-deployment-topology-and-scheduler-task-taxonomy.md
Duplicate and Currency Sweep
- Live issue/PR searches found no active duplicate or merged close-link.
- Knowledge Base prior art identified #13755 as the existing stdout-outcome/skip precedent and separated scheduler observability from watermark topology.
- Three Memory Core prior-art probes found no competing implementation lane for this exact compaction outcome.
- The self-authored drift probe found zero changes to the declared compactor, task-definition, supervisor, scheduler, or watermark surfaces since this issue was filed.
Origin Session ID: 019fe0b3-53bc-7ef2-8665-41a0ef3f7b62
Retrieval Hint: "GraphLog compaction safety-blocked child exits zero orchestrator completed stdout JSON skipped outcome"
Scope Correction
Intake classified the original ticket as
needs-narrowing. It combined two independently owned contracts:Current source and prior art make the first a coherent one-PR leaf. The second requires a separate ownership decision for
mcp-notificationsanda2a-webhookcursors. This corrected body owns scheduler outcome truth only. It must not synthesize or weaken consumer watermarks to make compaction green.Context
#12329 established the fail-closed GraphLog compaction mechanism, and #12394 scheduled it as a recurring Orchestrator lane. Both predecessors remain resolved for the behavior they shipped.
At
2026-08-08T10:54:35Z, the canonical Docker Orchestrator started scheduledgraphlog-compaction. One second later it recorded:The production compactor functions, run read-only against the Orchestrator's mounted volumes, returned a different maintenance outcome:
{ "canApply": false, "cutoffLogId": 0, "reason": "unknown-consumer-watermark" }The same census measured 3,200,403 GraphLog rows and 13 active wake subscriptions. That row count is operational context, not a causal claim about #16677.
The Problem
runGraphLogCompaction()returns the fail-closed plan, but the CLI prints only a human-readable report and exits zero. Thegraphlog-compactiontask definition does not capture structured stdout, andProcessSupervisorServicetherefore advances the task through its ordinary completed path.The green receipt means only that the child did not throw. It does not mean the maintenance contract reached an applicable cutoff, deleted rows, or established that no deletion was needed. This erases the exact safety-blocked state an operator needs to repair.
Architectural Reality
ai/scripts/maintenance/compactGraphLog.mjsalready returns the complete plan and deletion counts. No second planner is needed.ai/daemons/orchestrator/taskDefinitions.mjsoptsmemory-summary-backfillandkbSyncinto bounded stdout JSON capture, but notgraphlog-compaction.ai/daemons/orchestrator/services/ProcessSupervisorService.mjsalready maps a structured boolean{deferred: true, reason}child result tomarkSkipped()for selected tasks.markSkipped()does not refreshlastSuccessAt.graphlog-compactionis classifiedcontainer-planebytaskAuthority.mjs, aligned with ADR 0014. This ticket does not change that authority.The Fix
applied— rows were safely deleted;up-to-date— the authoritative plan was computable and no deletion was required; andsafety-blocked— an authoritative cutoff could not be computed.graphlog-compactiontask into stdout JSON capture.safety-blockedoutcome to the existing skipped/non-success task-state path, retaining the plan reason and bounded counts in task health details.Contract Ledger
compactGraphLog.mjstaskDefinitions.mjsgraphlog-compactionrequests structured captureProcessSupervisorServiceskipped, nevercompletedlastSuccessAt; reason and counts remain visibleDecision Record Impact
Aligned with ADR 0014. The scheduled lane remains
container-plane; this change makes its existing task outcome truthful and introduces no new authority class.Acceptance Criteria
unknown-consumer-watermarkapply run exits without deleting rows and recordsskipped/safety-blocked, notcompleted.lastSuccessAt; its reason, before/after row counts, cutoff, and deleted-row count are present in bounded task details.Out of Scope
Avoided Traps
Related
learn/agentos/decisions/0014-cloud-deployment-topology-and-scheduler-task-taxonomy.mdDuplicate and Currency Sweep
Origin Session ID: 019fe0b3-53bc-7ef2-8665-41a0ef3f7b62
Retrieval Hint: "GraphLog compaction safety-blocked child exits zero orchestrator completed stdout JSON skipped outcome"