QA-source: #20674 · automation.packaged-flow-disable-durable · c3
What happens
A packaged flow switched off through the ADR-0126 activation ledger comes back bound after a cold restart. Execution is still refused (no run is recorded, so the kill switch itself holds), but the trigger is armed again, every matching mutation fires it, and each firing is logged at ERROR as a failed trigger-fired run whose failure "is recorded in the flow's run history" — while the run history holds nothing.
Measured on main at 6bff748b (showcase, objectstack dev, file DB), reproduced on two consecutive cold boots over the same DB file.
Reproduction
- Boot with a file DB; as the seeded admin
POST /api/v1/automation/showcase_urgent_task_alert/toggle {"enabled":false} → 200. GET /api/v1/automation/_status → enabled:false, bound:false; the ledger row reads active: false.
- Stop the server process entirely; boot again over the same DB file (
OS_LOG_LEVEL=info shows the lines below).
GET /api/v1/automation/_status. Expected: enabled:false, bound:false. Actual: enabled:false, bound:true, stable after waiting.
- Boot-log order:
[Automation] Activation ledger: 1 packaged flow(s) are switched off for this installation and were left unbound — 'showcase_urgent_task_alert'. → [Automation] Bound 30 flow(s) from the protocol at kernel:ready → Trigger registered: record_change.
POST /api/v1/data/showcase_task with priority: 'urgent' (plus the required title, project, status): the server logs ERROR Trigger-fired run of flow 'showcase_urgent_task_alert' failed (trigger 'record_change') — no caller holds this result and nothing retries the run; the terminal failure is recorded in the flow's run history… carrying the FLOW_DISABLED sentence; GET /api/v1/automation/showcase_urgent_task_alert/runs (also ?status=failed) shows no new run.
Mechanism (read from source at 6bff748b)
AutomationEngine.registerTrigger (packages/services/service-automation/src/engine.ts) iterates every registered flow not in boundFlowTriggers and calls activateFlowTrigger(name) when its trigger type matches — without consulting isFlowEnabled(name). activateFlowTrigger has no enablement check either.
- Trigger plugins register their trigger at
kernel:ready, which fires after AutomationServicePlugin.start() pulled the flows and ran hydrateFlowActivations() (which correctly unbound the flow and logged so).
registerFlow does honour the ledger ("A ledger-disabled flow is NOT re-armed here…"); this second arming path does not. The engine pins in flow-activation-ledger.test.ts cover hydration and re-registration, not a trigger registered after hydration.
Why it matters
/_status — the Studio status badges' source — reports a disabled flow as bound after every deploy.
- An administrator's deliberate switch-off produces an ERROR line per matching event, and that line claims a run-history record that does not exist, which reads as a production failure.
- The item's P0 contract ("a restart that re-arms the flow (enabled or bound true…) is a FAIL against the whole point of ADR-0126 §7.2") is red on
main. RUNNER rule 7 applies: re-derive independently before acting.
Expected
After a restart a ledger-disabled flow stays unbound regardless of when its trigger type registers; no trigger-fired failure is logged for it.
Full evidence chain: #20674 (F-2).
Generated by Claude Code
QA-source: #20674 · automation.packaged-flow-disable-durable · c3
What happens
A packaged flow switched off through the ADR-0126 activation ledger comes back bound after a cold restart. Execution is still refused (no run is recorded, so the kill switch itself holds), but the trigger is armed again, every matching mutation fires it, and each firing is logged at ERROR as a failed trigger-fired run whose failure "is recorded in the flow's run history" — while the run history holds nothing.
Measured on
mainat6bff748b(showcase,objectstack dev, file DB), reproduced on two consecutive cold boots over the same DB file.Reproduction
POST /api/v1/automation/showcase_urgent_task_alert/toggle {"enabled":false}→ 200.GET /api/v1/automation/_status→enabled:false, bound:false; the ledger row readsactive: false.OS_LOG_LEVEL=infoshows the lines below).GET /api/v1/automation/_status. Expected:enabled:false, bound:false. Actual:enabled:false, bound:true, stable after waiting.[Automation] Activation ledger: 1 packaged flow(s) are switched off for this installation and were left unbound — 'showcase_urgent_task_alert'.→[Automation] Bound 30 flow(s) from the protocol at kernel:ready→Trigger registered: record_change.POST /api/v1/data/showcase_taskwithpriority: 'urgent'(plus the requiredtitle,project,status): the server logsERROR Trigger-fired run of flow 'showcase_urgent_task_alert' failed (trigger 'record_change') — no caller holds this result and nothing retries the run; the terminal failure is recorded in the flow's run history…carrying the FLOW_DISABLED sentence;GET /api/v1/automation/showcase_urgent_task_alert/runs(also?status=failed) shows no new run.Mechanism (read from source at
6bff748b)AutomationEngine.registerTrigger(packages/services/service-automation/src/engine.ts) iterates every registered flow not inboundFlowTriggersand callsactivateFlowTrigger(name)when its trigger type matches — without consultingisFlowEnabled(name).activateFlowTriggerhas no enablement check either.kernel:ready, which fires afterAutomationServicePlugin.start()pulled the flows and ranhydrateFlowActivations()(which correctly unbound the flow and logged so).registerFlowdoes honour the ledger ("A ledger-disabled flow is NOT re-armed here…"); this second arming path does not. The engine pins inflow-activation-ledger.test.tscover hydration and re-registration, not a trigger registered after hydration.Why it matters
/_status— the Studio status badges' source — reports a disabled flow as bound after every deploy.main. RUNNER rule 7 applies: re-derive independently before acting.Expected
After a restart a ledger-disabled flow stays unbound regardless of when its trigger type registers; no trigger-fired failure is logged for it.
Full evidence chain: #20674 (F-2).
Generated by Claude Code