Context
#14030 AC1 (alert on backup failure/overdue) converged with @neo-gpt (https://github.com/neomjs/neo/issues/14030#issuecomment-4806173915): a sibling task-diagnostics producer (NOT bolted into ContainerHealthDiagnosisService, NOT a pageable recovery-actuator target) that emits a recovery-diagnosis event with targetIdentity:{kind:'supervised-task', id} + details.actionClass:'escalate', routed through a narrow diagnosis-escalation sink.
The Fix (this slice — the producer-core)
Pure functions in ai/daemons/orchestrator/services/taskOutcomeDiagnosis.mjs:
detectTaskOverdue({lastRunAt, intervalMs, graceMs, now}) → {overdue, overdueByMs} (interval<=0 disables).
buildSupervisedTaskDiagnosis({taskName, outcome, observedAt, details}) → a recovery-diagnosis event (via the existing createRecoveryDiagnosisEvent contract): failed→crash, overdue→ambiguous recovery class; supervised-task targetIdentity; actionClass:'escalate'. Never restarts/retries — detection only.
Acceptance Criteria
Out of Scope (the next AC1 slice — live wiring)
- The
ProcessSupervisorService.recordTaskOutcome('<task>','failed', …) hook (line 412 etc.) calling the producer for the alert-on-failure task set.
- The scheduling-loop overdue check (
scheduling/backup.mjs lastRunAt+interval) calling detectTaskOverdue.
- The narrow
escalateDiagnosis(diagnosisEvent) sink in RecoveryActuatorService (NOT apply('backup','page') — page is deploy-target-only).
Contract Ledger Matrix
| Consumed surface |
Field |
Type / values |
Change |
Consumer impact |
Fallback / semantics |
detectTaskOverdue() return (exported from taskOutcomeDiagnosis.mjs) |
verdict |
overdue: Boolean · overdueByMs: Number (≥0) |
New exported function (additive) |
Intended consumer: the scheduling-loop overdue check (next slice) → feeds buildSupervisedTaskDiagnosis. |
Pure; intervalMs <= 0 ⇒ {overdue:false, overdueByMs:0} (periodic disabled); absent lastRunAt treated as 0. |
buildSupervisedTaskDiagnosis() return |
recovery-diagnosis event |
conforms to createRecoveryDiagnosisEvent: type:'recovery-diagnosis' · recoveryClass: 'crash' (failed) / 'ambiguous' (overdue) · targetIdentity:{kind:'supervised-task', id} · confidence:1 · details.actionClass:'escalate' + details.outcome |
New exported function (additive); reuses the existing recovery-diagnosis wire contract |
Intended consumer: the escalateDiagnosis sink (next slice). Producer-only — emits the event; never restarts/retries. |
Throws TypeError on invalid outcome (not failed/overdue) or missing taskName. |
- Docs: both functions' JSDoc documents the verdict/event shapes + the detection-only (no restart/retry) guarantee.
- Evidence: L2 unit —
detectTaskOverdue verdicts + the failed→crash / overdue→ambiguous event shapes + invalid-input rejection.
Related
Authored-by: @neo-opus-vega (Vega, Claude Opus 4.8)
Context
#14030 AC1 (alert on backup failure/overdue) converged with @neo-gpt (https://github.com/neomjs/neo/issues/14030#issuecomment-4806173915): a sibling task-diagnostics producer (NOT bolted into
ContainerHealthDiagnosisService, NOT a pageable recovery-actuator target) that emits arecovery-diagnosisevent withtargetIdentity:{kind:'supervised-task', id}+details.actionClass:'escalate', routed through a narrow diagnosis-escalation sink.The Fix (this slice — the producer-core)
Pure functions in
ai/daemons/orchestrator/services/taskOutcomeDiagnosis.mjs:detectTaskOverdue({lastRunAt, intervalMs, graceMs, now})→{overdue, overdueByMs}(interval<=0 disables).buildSupervisedTaskDiagnosis({taskName, outcome, observedAt, details})→ arecovery-diagnosisevent (via the existingcreateRecoveryDiagnosisEventcontract):failed→crash,overdue→ambiguousrecovery class;supervised-tasktargetIdentity;actionClass:'escalate'. Never restarts/retries — detection only.Acceptance Criteria
detectTaskOverduereturns the overdue verdict (pastlastRunAt+intervalMs+graceMs);<=0interval never overdue.buildSupervisedTaskDiagnosisemits a contract-validrecovery-diagnosisevent (supervised-taskidentity,escalateactionClass,crash/ambiguousclass) and rejects invalid outcomes/missing taskName.Out of Scope (the next AC1 slice — live wiring)
ProcessSupervisorService.recordTaskOutcome('<task>','failed', …)hook (line 412 etc.) calling the producer for the alert-on-failure task set.scheduling/backup.mjslastRunAt+interval) callingdetectTaskOverdue.escalateDiagnosis(diagnosisEvent)sink inRecoveryActuatorService(NOTapply('backup','page')—pageisdeploy-target-only).Contract Ledger Matrix
detectTaskOverdue()return (exported fromtaskOutcomeDiagnosis.mjs)overdue: Boolean·overdueByMs: Number(≥0)buildSupervisedTaskDiagnosis.intervalMs <= 0⇒{overdue:false, overdueByMs:0}(periodic disabled); absentlastRunAttreated as 0.buildSupervisedTaskDiagnosis()returnrecovery-diagnosiseventcreateRecoveryDiagnosisEvent:type:'recovery-diagnosis'·recoveryClass: 'crash'(failed) /'ambiguous'(overdue) ·targetIdentity:{kind:'supervised-task', id}·confidence:1·details.actionClass:'escalate'+details.outcomerecovery-diagnosiswire contractescalateDiagnosissink (next slice). Producer-only — emits the event; never restarts/retries.TypeErroron invalidoutcome(notfailed/overdue) or missingtaskName.detectTaskOverdueverdicts + thefailed→crash/overdue→ambiguousevent shapes + invalid-input rejection.Related
Authored-by: @neo-opus-vega (Vega, Claude Opus 4.8)