@moretea-labs/forge 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (528) hide show
  1. package/AGENTS.md +70 -7
  2. package/CHANGELOG.md +14 -0
  3. package/CLAUDE.md +17 -2
  4. package/CONTRIBUTING.md +3 -2
  5. package/THIRD_PARTY_NOTICES.md +1 -1
  6. package/adapters/chatgpt/browser-delivery-host.ts +7 -1
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +529 -56
  8. package/adapters/chatgpt/controller-binding-store.ts +101 -22
  9. package/adapters/chatgpt/controller-round-host.ts +15 -25
  10. package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
  11. package/adapters/chatgpt/provider-delivery.ts +228 -1
  12. package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
  13. package/adapters/computer/chatgpt-extension-target.ts +463 -0
  14. package/adapters/computer/chatgpt-target-router.ts +103 -0
  15. package/adapters/computer/chrome-extension/background.js +385 -0
  16. package/adapters/computer/chrome-extension/content.js +378 -0
  17. package/adapters/computer/chrome-extension/core.js +24 -0
  18. package/adapters/computer/chrome-extension/manifest.json +30 -0
  19. package/adapters/computer/desktop-operator-discovery.ts +11 -31
  20. package/adapters/computer/interaction-target-authority.ts +142 -18
  21. package/adapters/mcp/auth.ts +0 -1
  22. package/adapters/mcp/instructions.ts +5 -4
  23. package/adapters/mcp/multi-repository.ts +3 -19
  24. package/adapters/mcp/oauth.ts +73 -0
  25. package/adapters/mcp/paths.ts +4 -1
  26. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
  27. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  28. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  29. package/adapters/mcp/runtime-gateway/context-adapter.ts +411 -73
  30. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
  31. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
  32. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  33. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
  34. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  35. package/adapters/mcp/runtime-gateway/execution-tools.ts +8 -42
  36. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
  37. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
  38. package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
  39. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
  40. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
  41. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
  42. package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
  43. package/adapters/mcp/runtime-gateway/router.ts +18 -504
  44. package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
  45. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
  46. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  47. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
  48. package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
  49. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
  50. package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
  51. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
  52. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
  53. package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
  54. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  55. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  56. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
  57. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  58. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  59. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  60. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  61. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  62. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
  63. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  64. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
  65. package/adapters/mcp/server.ts +67 -23
  66. package/adapters/mcp/setup.ts +1 -3
  67. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
  68. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
  69. package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
  70. package/adapters/mcp/toolset-names.ts +5 -0
  71. package/adapters/mcp/toolset.ts +6 -5
  72. package/adapters/mcp/transports/http-observation.ts +365 -0
  73. package/adapters/mcp/transports/http.ts +154 -677
  74. package/adapters/mcp/transports/oauth-http.ts +322 -0
  75. package/adapters/mcp/transports/session-registry.ts +6 -0
  76. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
  77. package/assets/plugin-registry.v1.json +6 -4
  78. package/assets/reference-configs/agentic-development-flow.md +25 -0
  79. package/assets/reference-configs/global-working-rules.md +19 -0
  80. package/assets/skill-version.json +2 -2
  81. package/docs/architecture/CURRENT.md +111 -41
  82. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  83. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  84. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
  85. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
  86. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  87. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  88. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
  89. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  90. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
  91. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
  92. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  93. package/docs/architecture/domains/verification.md +1 -0
  94. package/docs/architecture/domains/workflow-engine.md +6 -0
  95. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  96. package/docs/forge-plugin-management.md +2 -2
  97. package/docs/operations/controller-performance-and-502.md +17 -0
  98. package/docs/operations/controller-reliability-runbook.md +2 -2
  99. package/docs/operations/features.md +1 -1
  100. package/docs/operations/features.zh-CN.md +1 -1
  101. package/docs/operations/homebrew.md +2 -2
  102. package/docs/operations/mcp-tool-exposure.md +2 -0
  103. package/docs/operations/releasing.md +2 -0
  104. package/docs/operations/standalone-disaster-recovery.md +14 -6
  105. package/docs/wiki/Implementation.md +1 -1
  106. package/package.json +25 -14
  107. package/packages/kernel/cognition/api/index.ts +6 -0
  108. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  109. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  110. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  111. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  112. package/packages/kernel/cognition/domain/learning.ts +106 -0
  113. package/packages/kernel/cognition/domain/memory.ts +254 -0
  114. package/packages/kernel/controller/api/index.ts +1 -0
  115. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  116. package/packages/kernel/controller/application/controller-round-service.ts +12 -0
  117. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
  118. package/packages/kernel/controller/domain/controller-round.ts +4 -1
  119. package/packages/kernel/controller/domain/execution-quality.ts +3 -1
  120. package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
  121. package/packages/kernel/controller/infrastructure/controller-session-store.ts +297 -71
  122. package/packages/kernel/controller/ports/controller-host.ts +2 -0
  123. package/packages/kernel/identity/api/index.ts +10 -0
  124. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  125. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  126. package/packages/kernel/identity/domain/grant.ts +72 -0
  127. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  128. package/packages/kernel/identity/domain/user-request.ts +93 -0
  129. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
  130. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  131. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  132. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  133. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  134. package/packages/kernel/scheduler/api/index.ts +0 -2
  135. package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
  136. package/packages/kernel/scheduler/application/settlement.ts +0 -15
  137. package/packages/kernel/scheduler/domain/schedule.ts +87 -0
  138. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +153 -133
  139. package/packages/kernel/work/api/index.ts +3 -2
  140. package/packages/kernel/work/application/work-service.ts +17 -42
  141. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  142. package/packages/kernel/work/domain/engineering-design.ts +10 -5
  143. package/packages/kernel/work/domain/engineering-profile.ts +8 -12
  144. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  145. package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
  146. package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
  147. package/packages/kernel/work/domain/implementation-review.ts +14 -331
  148. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  149. package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
  150. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  151. package/packages/kernel/work/domain/state-machine.ts +25 -63
  152. package/packages/kernel/work/domain/types.ts +57 -51
  153. package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
  154. package/packages/kernel/work/ports/work-contract-store.ts +5 -2
  155. package/packages/plugin-runtime/browser/session-authority.ts +2 -1
  156. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  157. package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
  158. package/packages/plugin-runtime/computer/index.ts +2 -0
  159. package/packages/plugin-runtime/computer/target-authority.ts +26 -1
  160. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
  161. package/packages/protocols/computer/contract.ts +27 -1
  162. package/scripts/activate-source-baseline.ts +7 -9
  163. package/scripts/canary-controller-semantic-wait.ts +12 -24
  164. package/scripts/check-mcp-compatibility.ts +48 -136
  165. package/scripts/check-runtime-architecture.mjs +622 -217
  166. package/scripts/forge-local-recovery-helper.mjs +93 -15
  167. package/scripts/lib/project-init-lib.sh +3 -0
  168. package/scripts/open-source-audit-allowlist.txt +1 -2
  169. package/scripts/run-bounded-command.mjs +5 -0
  170. package/scripts/run-governed-gate.ts +0 -1
  171. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  172. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  173. package/scripts/smoke-mcp-http-runtime.ts +5 -0
  174. package/scripts/smoke-runtime-recovery.ts +12 -43
  175. package/scripts/smoke-schedule-engine.ts +23 -15
  176. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  177. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  178. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
  179. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  180. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  181. package/scripts/smoke-workflow-supervisor.ts +103 -0
  182. package/scripts/verify-forge-runtime.sh +2 -1
  183. package/src/cli/agent-jobs/executor-health.ts +11 -34
  184. package/src/cli/agent-jobs/integration.ts +18 -49
  185. package/src/cli/agent-jobs/job-manager.ts +7 -3
  186. package/src/cli/commands/brain-assistant.ts +27 -13
  187. package/src/cli/commands/brain.ts +13 -15
  188. package/src/cli/commands/chatgpt.ts +1 -163
  189. package/src/cli/commands/recovery.ts +135 -121
  190. package/src/cli/commands/runtime.ts +3 -3
  191. package/src/cli/commands/setup-profile.ts +8 -0
  192. package/src/cli/controller/check-runner.ts +13 -2
  193. package/src/cli/controller/completion-backlog.ts +3 -3
  194. package/src/cli/controller/completion-orchestrator.ts +4 -43
  195. package/src/cli/controller/completion-target.ts +3 -72
  196. package/src/cli/controller/context/source-materializer.ts +81 -100
  197. package/src/cli/controller/context-pack.ts +10 -8
  198. package/src/cli/controller/execution-completion.ts +4 -7
  199. package/src/cli/controller/execution-policy.ts +36 -153
  200. package/src/cli/controller/governance.ts +2 -3
  201. package/src/cli/controller/issue-store.ts +4 -99
  202. package/src/cli/controller/launch-agents.ts +59 -18
  203. package/src/cli/controller/operational-plan.ts +24 -51
  204. package/src/cli/controller/runtime-config.ts +5 -1
  205. package/src/cli/controller/stuck-state-migration.ts +2 -3
  206. package/src/cli/controller/systemd-user.ts +39 -1
  207. package/src/cli/editing/edit-session.ts +1 -7
  208. package/src/cli/local-bridge/console-automations.ts +18 -13
  209. package/src/cli/local-bridge/console-view-models.ts +3 -12
  210. package/src/cli/local-bridge/facade-api.ts +64 -191
  211. package/src/cli/local-bridge/server.ts +24 -147
  212. package/src/cli/local-bridge/types.ts +0 -1
  213. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  214. package/src/cli/repositories/command-executor.ts +29 -23
  215. package/src/cli/repositories/command-scope.ts +35 -16
  216. package/src/cli/repositories/controller-home.ts +48 -0
  217. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  218. package/src/cli/repositories/git-command-executor.ts +2 -2
  219. package/src/cli/repositories/registry.ts +77 -11
  220. package/src/cli/repositories/structured-git.ts +62 -2
  221. package/src/cli/repository/session-cache.ts +92 -6
  222. package/src/runtime/context/assistant-context.ts +20 -7
  223. package/src/runtime/context/assistant-work-context.ts +89 -27
  224. package/src/runtime/context/automatic-learning.ts +565 -0
  225. package/src/runtime/context/cognitive-attention.ts +173 -0
  226. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  227. package/src/runtime/context/context-closure.ts +46 -9
  228. package/src/runtime/context/context-pack-process.ts +103 -0
  229. package/src/runtime/context/context-plane.ts +1 -9
  230. package/src/runtime/context/direct-controller-learning.ts +298 -0
  231. package/src/runtime/context/semantic-navigation-contract.ts +9 -0
  232. package/src/runtime/context/semantic-navigation.ts +6 -34
  233. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  234. package/src/runtime/context/typescript-navigation.ts +89 -3
  235. package/src/runtime/control-plane/cleanup-artifact-retention.ts +212 -9
  236. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
  237. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
  238. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
  239. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  240. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  241. package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
  242. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  243. package/src/runtime/control-plane/execution/validation.ts +6 -8
  244. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
  245. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  246. package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
  247. package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
  248. package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
  249. package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
  250. package/src/runtime/control-plane/execution/work-handle-authority.ts +188 -109
  251. package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
  252. package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
  253. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
  254. package/src/runtime/control-plane/execution/work-operation-service.ts +43 -27
  255. package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
  256. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  257. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  258. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +173 -186
  259. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
  260. package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
  261. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  262. package/src/runtime/control-plane/facade/capability-registry.ts +362 -37
  263. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  264. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  265. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  266. package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
  267. package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
  268. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
  269. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
  270. package/src/runtime/control-plane/facade/index.ts +0 -1
  271. package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
  272. package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
  273. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  274. package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
  275. package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
  276. package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
  277. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
  278. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  279. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
  280. package/src/runtime/control-plane/facade/types.ts +49 -108
  281. package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
  282. package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
  283. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  284. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
  285. package/src/runtime/control-plane/global-scheduler/maintenance.ts +239 -112
  286. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
  287. package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
  288. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  289. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  290. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  291. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
  292. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -300
  293. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  294. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
  295. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
  296. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  297. package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
  298. package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
  299. package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
  300. package/src/runtime/control-plane/release-retention.ts +135 -47
  301. package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
  302. package/src/runtime/control-plane/runtime-cleanup.ts +290 -21
  303. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  304. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
  305. package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
  306. package/src/runtime/diagnostics/incident-repair.ts +313 -146
  307. package/src/runtime/diagnostics/mcp-timing.ts +114 -0
  308. package/src/runtime/evidence/artifact-store.ts +158 -40
  309. package/src/runtime/evidence/evidence-store.ts +112 -8
  310. package/src/runtime/evidence/output-handle-index.ts +225 -0
  311. package/src/runtime/evidence/result-store.ts +158 -44
  312. package/src/runtime/execution/jobs/store.ts +3 -4
  313. package/src/runtime/execution/managed-workspace.ts +57 -0
  314. package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
  315. package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
  316. package/src/runtime/execution/process-runtime/check-result.ts +105 -1
  317. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
  318. package/src/runtime/execution/process-runtime/command-facade.ts +79 -27
  319. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  320. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  321. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  322. package/src/runtime/execution/process-runtime/index.ts +3 -0
  323. package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
  324. package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
  325. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  326. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  327. package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
  328. package/src/runtime/execution/process-runtime/runtime.ts +35 -2
  329. package/src/runtime/execution/process-runtime/store.ts +30 -2
  330. package/src/runtime/execution/process-runtime/types.ts +6 -1
  331. package/src/runtime/execution/workers/executor.ts +2 -6
  332. package/src/runtime/platform/service-inventory.ts +130 -0
  333. package/src/runtime/platform/service-manager.ts +8 -0
  334. package/src/runtime/plugins/action-application.ts +85 -28
  335. package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
  336. package/src/runtime/plugins/browser-adapter.ts +757 -34
  337. package/src/runtime/plugins/browser-automation-service.ts +23 -3
  338. package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
  339. package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
  340. package/src/runtime/plugins/browser-native-session-service.ts +63 -13
  341. package/src/runtime/plugins/browser-registration.ts +16 -0
  342. package/src/runtime/plugins/browser-runtime.ts +14 -1
  343. package/src/runtime/plugins/browser-session-authority.ts +2 -2
  344. package/src/runtime/plugins/browser-session-store.ts +46 -76
  345. package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
  346. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
  347. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
  348. package/src/runtime/plugins/computer-registration.ts +222 -11
  349. package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
  350. package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
  351. package/src/runtime/plugins/external-adapter.ts +42 -6
  352. package/src/runtime/plugins/external-registration.ts +21 -1
  353. package/src/runtime/plugins/gmail-adapter.ts +23 -2
  354. package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
  355. package/src/runtime/plugins/google-shared.ts +14 -7
  356. package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
  357. package/src/runtime/plugins/ios-adapter.ts +1 -1
  358. package/src/runtime/plugins/lightweight-action.ts +208 -2
  359. package/src/runtime/plugins/local-system-adapter.ts +12 -2
  360. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  361. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  362. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  363. package/src/runtime/plugins/store.ts +602 -428
  364. package/src/runtime/plugins/types.ts +20 -0
  365. package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
  366. package/src/runtime/projections/invalidation.ts +7 -3
  367. package/src/runtime/projections/materialized-view.ts +80 -36
  368. package/src/runtime/recovery/actions.ts +3 -3
  369. package/src/runtime/recovery/capability-action-application.ts +46 -45
  370. package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
  371. package/src/runtime/recovery/maintenance-executor.ts +374 -123
  372. package/src/runtime/recovery/store.ts +29 -11
  373. package/src/runtime/release/release-coordinator.ts +296 -0
  374. package/src/runtime/release/release-session.ts +716 -0
  375. package/src/runtime/release/release-state-migration.ts +25 -0
  376. package/src/runtime/resources/leases/store.ts +56 -25
  377. package/src/runtime/root/computer-composition.ts +191 -6
  378. package/src/runtime/root/controller-progression-composition.ts +192 -0
  379. package/src/runtime/root/controller-round-composition.ts +20 -1
  380. package/src/runtime/root/deployment-topology.ts +106 -0
  381. package/src/runtime/root/entry.ts +4 -3
  382. package/src/runtime/root/known-good-recovery.ts +184 -0
  383. package/src/runtime/root/local-bridge.ts +0 -1
  384. package/src/runtime/root/mcp-transport.ts +111 -18
  385. package/src/runtime/root/ownership.ts +261 -9
  386. package/src/runtime/root/package-connector-service.ts +105 -19
  387. package/src/runtime/root/package-runtime-release.ts +117 -18
  388. package/src/runtime/root/package-runtime-service.ts +17 -11
  389. package/src/runtime/root/release-execution-canary.ts +44 -12
  390. package/src/runtime/root/release-loader.ts +90 -8
  391. package/src/runtime/root/release-manifest.ts +80 -6
  392. package/src/runtime/root/release-materialize.ts +372 -28
  393. package/src/runtime/root/release-store.ts +395 -36
  394. package/src/runtime/root/runtime-lane.ts +191 -0
  395. package/src/runtime/root/runtime.ts +159 -27
  396. package/src/runtime/root/scheduled-controller-composition.ts +28 -3
  397. package/src/runtime/root/service-runner.ts +2 -2
  398. package/src/runtime/root/service.ts +36 -33
  399. package/src/runtime/root/status.ts +48 -0
  400. package/src/runtime/root/types.ts +32 -3
  401. package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
  402. package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
  403. package/src/runtime/root/write-fence.ts +50 -2
  404. package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
  405. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  406. package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
  407. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  408. package/src/runtime/shared/process-environment.ts +20 -9
  409. package/src/runtime/shared/response-budget.ts +1 -3
  410. package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
  411. package/src/runtime/standalone-recovery/core.ts +3702 -342
  412. package/src/runtime/standalone-recovery/entry.ts +469 -39
  413. package/src/runtime/standalone-recovery/installer.ts +262 -148
  414. package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
  415. package/src/runtime/standalone-recovery/operation-lock.ts +1 -1
  416. package/src/runtime/standalone-recovery/performance.ts +54 -16
  417. package/src/runtime/standalone-recovery/release.ts +53 -5
  418. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  419. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  420. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  421. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +10 -3
  422. package/src/runtime/workflow/schedules/engine.ts +138 -172
  423. package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
  424. package/src/runtime/workflows/runtime.ts +15 -19
  425. package/src/testing/test-governance.ts +5 -1
  426. package/supervisor/chatgpt-conversation.ts +23 -0
  427. package/supervisor/client.ts +201 -0
  428. package/supervisor/control-plane.ts +868 -0
  429. package/supervisor/entry.ts +15 -0
  430. package/supervisor/forge-validators.ts +80 -0
  431. package/supervisor/index.ts +5 -0
  432. package/supervisor/native-browser-adapter.ts +786 -0
  433. package/supervisor/native-messaging/host.ts +39 -0
  434. package/supervisor/native-messaging/manifest.ts +26 -0
  435. package/supervisor/native-messaging/protocol.ts +27 -0
  436. package/supervisor/paths.ts +18 -0
  437. package/supervisor/protocol.ts +202 -0
  438. package/supervisor/server.ts +404 -0
  439. package/supervisor/store.ts +1451 -0
  440. package/supervisor/tsconfig.json +8 -0
  441. package/supervisor/types.ts +255 -0
  442. package/adapters/chatgpt/controller-host.ts +0 -119
  443. package/adapters/mcp/controller-round-compatibility.ts +0 -184
  444. package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
  445. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
  446. package/packages/kernel/progression/api/index.ts +0 -2
  447. package/packages/kernel/progression/application/projector.ts +0 -211
  448. package/packages/kernel/progression/domain/types.ts +0 -116
  449. package/packages/kernel/progression/index.ts +0 -1
  450. package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
  451. package/packages/kernel/scheduler/domain/continuation.ts +0 -38
  452. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
  453. package/packages/kernel/work/domain/admission-policy.ts +0 -34
  454. package/scripts/benchmark-background-check-overlap.ts +0 -228
  455. package/scripts/benchmark-computer-hotpath.ts +0 -255
  456. package/scripts/benchmark-controller-hotpath.ts +0 -212
  457. package/scripts/benchmark-controller-round2.ts +0 -191
  458. package/scripts/benchmark-ios-agent-device.ts +0 -511
  459. package/scripts/benchmark-mcp-host.sh +0 -117
  460. package/scripts/benchmark-mcp-transport.ts +0 -169
  461. package/scripts/benchmark-operational-memory-activation.ts +0 -235
  462. package/scripts/benchmark-operational-shadow.ts +0 -28
  463. package/scripts/benchmark-quality-harness.ts +0 -146
  464. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  465. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  466. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  467. package/scripts/benchmark-thin-harness.ts +0 -251
  468. package/scripts/benchmark-typescript-navigation.ts +0 -186
  469. package/scripts/benchmark-work-efficiency.ts +0 -334
  470. package/scripts/build-controller-ui.ts +0 -28
  471. package/scripts/check-npm-release.sh +0 -32
  472. package/scripts/check-public-release-surface.sh +0 -19
  473. package/scripts/check-release-published.sh +0 -57
  474. package/scripts/check-release-readiness.sh +0 -37
  475. package/scripts/check-release-version.mjs +0 -74
  476. package/scripts/check-tarball-install-smoke.sh +0 -150
  477. package/scripts/package-source-archive.sh +0 -147
  478. package/scripts/public-release-files.txt +0 -39
  479. package/scripts/public-release.ts +0 -174
  480. package/scripts/publish-release-tarball.sh +0 -27
  481. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  482. package/scripts/stage-runtime-release.ts +0 -44
  483. package/src/cli/controller/progress.ts +0 -656
  484. package/src/cli/controller/work-mode.ts +0 -181
  485. package/src/cli/local-bridge/console-assets.ts +0 -17
  486. package/src/cli/local-bridge/dashboard.ts +0 -17
  487. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  488. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  489. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  490. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  491. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  492. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  493. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  494. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  495. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  496. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  497. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  498. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  499. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  500. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  501. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  502. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  503. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  504. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  505. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  506. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  507. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  508. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  509. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  510. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  511. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  512. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  513. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  514. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  515. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
  516. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
  517. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
  518. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
  519. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  520. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
  521. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
  522. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
  523. package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
  524. package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
  525. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  526. package/src/runtime/control-plane/routing/index.ts +0 -1
  527. package/src/runtime/control-plane/routing/route-policy.ts +0 -401
  528. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
@@ -0,0 +1,1451 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createRequire } from 'node:module';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { resolveWorkflowSupervisorForgeHome, workflowSupervisorDatabasePathValue, workflowSupervisorRootPath } from './paths';
5
+ import type { WorkflowEffectKind, WorkflowEffectOutcome, WorkflowSupervisorCompletion, WorkflowSupervisorContinuationProof, WorkflowSupervisorDiscoverySnapshot, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTaskStall, WorkflowSupervisorTerminalState } from './types';
6
+ export type { WorkflowSupervisorTaskStall } from './types';
7
+
8
+ interface Statement { get(...params: unknown[]): unknown; all(...params: unknown[]): unknown[]; run(...params: unknown[]): unknown; finalize?(): void }
9
+ interface Database { exec(sql: string): void; prepare(sql: string): Statement; close(): void }
10
+ const require = createRequire(import.meta.url);
11
+
12
+ function databaseConstructor(): new (path: string) => Database {
13
+ const module = require(process.versions.bun ? 'bun:sqlite' : 'node:sqlite') as { Database?: new (path: string) => Database; DatabaseSync?: new (path: string) => Database };
14
+ const Constructor = module.Database ?? module.DatabaseSync;
15
+ if (!Constructor) throw new Error('WORKFLOW_SUPERVISOR_SQLITE_UNAVAILABLE');
16
+ return Constructor;
17
+ }
18
+
19
+ function statement<T>(db: Database, sql: string, fn: (statement: Statement) => T): T {
20
+ const prepared = db.prepare(sql);
21
+ try { return fn(prepared); } finally { prepared.finalize?.(); }
22
+ }
23
+ function now(): string { return new Date().toISOString(); }
24
+ function objectiveSha256(value: string): string { return createHash('sha256').update(value).digest('hex'); }
25
+ function projectMetadataFromConversationUrl(value: string, observedTitle?: string, observedUrl?: string): { projectTitle?: string; projectUrl: string } | undefined {
26
+ try {
27
+ const parsed = new URL(value);
28
+ const match = /^\/g\/(g-p-[a-z0-9]+)(?:-([^/]+))?\/c\/[a-z0-9-]+\/?$/i.exec(parsed.pathname);
29
+ const slug = match?.[2]?.trim();
30
+ if (parsed.protocol !== 'https:' || parsed.hostname !== 'chatgpt.com' || !match?.[1]) return undefined;
31
+ let observedProjectId: string | undefined;
32
+ try {
33
+ const observed = new URL(observedUrl ?? '');
34
+ if (observed.origin === parsed.origin) observedProjectId = /^\/g\/(g-p-[a-z0-9]+)(?:-[^/]+)?\/project\/?$/i.exec(observed.pathname)?.[1];
35
+ } catch { /* An unproven title cannot override the canonical route. */ }
36
+ const projectTitle = observedProjectId?.toLowerCase() === match[1].toLowerCase()
37
+ ? observedTitle ?? slug?.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim()
38
+ : slug?.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim();
39
+ return {
40
+ ...(projectTitle ? { projectTitle } : {}),
41
+ projectUrl: `https://chatgpt.com/g/${match[1]}/project`,
42
+ };
43
+ } catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ function json(value: unknown): string {
48
+ if (Array.isArray(value)) return `[${value.map((item) => json(item)).join(',')}]`;
49
+ if (value && typeof value === 'object') {
50
+ const entries = Object.entries(value as Record<string, unknown>).sort(([a],[b]) => a.localeCompare(b));
51
+ return `{${entries.map(([key,item]) => `${JSON.stringify(key)}:${json(item)}`).join(',')}}`;
52
+ }
53
+ return JSON.stringify(value);
54
+ }
55
+
56
+ export { resolveWorkflowSupervisorForgeHome } from './paths';
57
+ export function workflowSupervisorRoot(forgeHome?: string): string {
58
+ const root = workflowSupervisorRootPath(forgeHome);
59
+ mkdirSync(root, { recursive: true, mode: 0o700 });
60
+ return root;
61
+ }
62
+ export function workflowSupervisorDatabasePath(forgeHome?: string): string {
63
+ workflowSupervisorRoot(forgeHome);
64
+ return workflowSupervisorDatabasePathValue(forgeHome);
65
+ }
66
+
67
+ function openDatabase(forgeHome?: string): Database {
68
+ const Constructor = databaseConstructor();
69
+ const db = new Constructor(workflowSupervisorDatabasePath(forgeHome));
70
+ db.exec('PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;');
71
+ db.exec(`
72
+ CREATE TABLE IF NOT EXISTS supervisor_schema (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL);
73
+ CREATE TABLE IF NOT EXISTS tasks (
74
+ task_id TEXT PRIMARY KEY, conversation_id TEXT NOT NULL UNIQUE, conversation_url TEXT NOT NULL, objective TEXT NOT NULL,
75
+ completion_contract_json TEXT NOT NULL, continuation_policy_json TEXT NOT NULL, user_blocker_policy_json TEXT NOT NULL, created_at TEXT NOT NULL
76
+ );
77
+ CREATE TABLE IF NOT EXISTS completions (
78
+ completion_fingerprint TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(task_id), source_effect_id TEXT NOT NULL,
79
+ action TEXT NOT NULL, response_sha256 TEXT NOT NULL, control_block_sha256 TEXT NOT NULL, proposal_json TEXT NOT NULL, committed_at TEXT NOT NULL,
80
+ UNIQUE(task_id, source_effect_id, response_sha256, control_block_sha256)
81
+ );
82
+ CREATE UNIQUE INDEX IF NOT EXISTS completions_one_per_source_effect
83
+ ON completions(task_id, source_effect_id);
84
+ CREATE TABLE IF NOT EXISTS effects (
85
+ effect_id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(task_id), kind TEXT NOT NULL, origin_key TEXT NOT NULL UNIQUE,
86
+ source_completion_fingerprint TEXT, prompt_text TEXT NOT NULL, created_at TEXT NOT NULL,
87
+ UNIQUE(task_id, source_completion_fingerprint)
88
+ );
89
+ CREATE TABLE IF NOT EXISTS events (
90
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL REFERENCES tasks(task_id), event_key TEXT NOT NULL UNIQUE,
91
+ kind TEXT NOT NULL, effect_id TEXT, completion_fingerprint TEXT, payload_json TEXT NOT NULL, occurred_at TEXT NOT NULL
92
+ );
93
+ CREATE INDEX IF NOT EXISTS events_task_order ON events(task_id, event_id);
94
+ CREATE INDEX IF NOT EXISTS events_effect_kind_order ON events(effect_id, kind, event_id);
95
+ CREATE TABLE IF NOT EXISTS discovered_conversations (
96
+ source TEXT NOT NULL, conversation_id TEXT NOT NULL, canonical_url TEXT NOT NULL, title TEXT,
97
+ project_title TEXT, project_url TEXT, observed_at TEXT NOT NULL,
98
+ PRIMARY KEY(source, conversation_id)
99
+ );
100
+ CREATE INDEX IF NOT EXISTS discovered_conversations_observed ON discovered_conversations(observed_at, conversation_id);
101
+ INSERT OR IGNORE INTO supervisor_schema(version, applied_at) VALUES (1, datetime('now'));
102
+ `);
103
+ const schema = statement(db, 'SELECT MAX(version) AS version FROM supervisor_schema', (s) => s.get()) as { version?: number } | undefined;
104
+ if (schema?.version !== 1) { db.close(); throw new Error(`WORKFLOW_SUPERVISOR_SCHEMA_UNSUPPORTED:${String(schema?.version ?? 'missing')}`); }
105
+ return db;
106
+ }
107
+
108
+ function taskFromRow(row: Record<string, unknown>): WorkflowSupervisorTask {
109
+ return {
110
+ taskId: String(row.task_id), conversationId: String(row.conversation_id), conversationUrl: String(row.conversation_url), objective: String(row.objective),
111
+ completionContract: JSON.parse(String(row.completion_contract_json)) as Record<string, unknown>, continuationPolicy: JSON.parse(String(row.continuation_policy_json)) as Record<string, unknown>,
112
+ userBlockerPolicy: JSON.parse(String(row.user_blocker_policy_json)) as Record<string, unknown>, createdAt: String(row.created_at),
113
+ };
114
+ }
115
+
116
+ function boundedText(value: unknown): string | undefined {
117
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
118
+ }
119
+
120
+ function explicitRequirementTaskUpgrade(input: WorkflowSupervisorTaskInput): { requirementId: string; repoId: string } | undefined {
121
+ if (input.completionContract.kind !== 'forge_requirement_done'
122
+ || input.continuationPolicy.kind !== 'forge_goal_outer_turn'
123
+ || input.userBlockerPolicy.kind !== 'forge_requirement_waiting_for_user') return undefined;
124
+ const requirementId = boundedText(input.completionContract.requirement_id);
125
+ const blockerRequirementId = boundedText(input.userBlockerPolicy.requirement_id);
126
+ const repoId = boundedText(input.completionContract.repo_id);
127
+ const blockerRepoId = boundedText(input.userBlockerPolicy.repo_id);
128
+ if (!requirementId || blockerRequirementId !== requirementId || !repoId || blockerRepoId !== repoId) return undefined;
129
+ if (boundedText(input.continuationPolicy.exact_conversation_id) !== input.conversationId
130
+ || boundedText(input.continuationPolicy.exact_conversation_url) !== input.conversationUrl) return undefined;
131
+ return { requirementId, repoId };
132
+ }
133
+ function effectFromRow(row: Record<string, unknown>): WorkflowSupervisorEffect {
134
+ return { effectId: String(row.effect_id), taskId: String(row.task_id), kind: String(row.kind) as WorkflowEffectKind,
135
+ ...(row.source_completion_fingerprint ? { sourceCompletionFingerprint: String(row.source_completion_fingerprint) } : {}), prompt: String(row.prompt_text), createdAt: String(row.created_at) };
136
+ }
137
+
138
+ function completionFromRow(row: Record<string, unknown>): WorkflowSupervisorCompletion {
139
+ return {
140
+ completionFingerprint: String(row.completion_fingerprint), taskId: String(row.task_id), sourceEffectId: String(row.source_effect_id),
141
+ action: String(row.action) as WorkflowSupervisorCompletion['action'], responseSha256: String(row.response_sha256), controlBlockSha256: String(row.control_block_sha256),
142
+ proposal: JSON.parse(String(row.proposal_json)) as WorkflowSupervisorCompletion['proposal'], committedAt: String(row.committed_at),
143
+ };
144
+ }
145
+ function parsedObject(value: unknown): Record<string, unknown> {
146
+ try { const parsed = JSON.parse(String(value)); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {}; }
147
+ catch { return {}; }
148
+ }
149
+ function storedGeneration(value: unknown): number {
150
+ const generation = Number(parsedObject(value).generation);
151
+ return Number.isInteger(generation) && generation > 0 ? generation : 1;
152
+ }
153
+ function latestNotAppliedProofEventId(db: Database, effectId: string): number {
154
+ const notApplied = statement(db, "SELECT event_id FROM events WHERE effect_id = ? AND kind = 'effect_not_applied' ORDER BY event_id DESC LIMIT 1", (s) => s.get(effectId)) as { event_id?: number } | undefined;
155
+ return Number(notApplied?.event_id ?? 0);
156
+ }
157
+
158
+ /** A durable operator supersession retires an effect from the causal obligation; it is never replayed. */
159
+ function effectSuperseded(db: Database, effectId: string): boolean {
160
+ return Boolean(statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_outcome_unknown_superseded' LIMIT 1", (s) => s.get(effectId)));
161
+ }
162
+
163
+ function providerResumeExhaustedWithin(db: Database, effectId: string): boolean {
164
+ return Boolean(statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'assistant_recovery_exhausted' LIMIT 1", (s) => s.get(effectId)));
165
+ }
166
+
167
+ function providerRecoveryDepthWithin(db: Database, effectId: string): number {
168
+ let currentEffectId = effectId;
169
+ let depth = 0;
170
+ const seen = new Set<string>();
171
+ while (true) {
172
+ if (seen.has(currentEffectId)) throw new Error('WORKFLOW_SUPERVISOR_PROVIDER_RECOVERY_CHAIN_CORRUPT');
173
+ seen.add(currentEffectId);
174
+ const row = statement(db, 'SELECT kind,origin_key FROM effects WHERE effect_id = ?', (s) => s.get(currentEffectId)) as { kind?: string; origin_key?: string } | undefined;
175
+ if (!row) throw new Error('WORKFLOW_SUPERVISOR_PROVIDER_RECOVERY_CHAIN_CORRUPT');
176
+ const originKey = String(row.origin_key ?? '');
177
+ if (row.kind !== 'recovery' || !originKey.startsWith('provider-recovery:')) return depth;
178
+ depth += 1;
179
+ currentEffectId = originKey.slice('provider-recovery:'.length).trim();
180
+ if (!currentEffectId) throw new Error('WORKFLOW_SUPERVISOR_PROVIDER_RECOVERY_CHAIN_CORRUPT');
181
+ }
182
+ }
183
+
184
+ function operatorAuthorizedRefundCount(db: Database, effectId: string): number {
185
+ const row = statement(db, "SELECT COUNT(*) AS total FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_budget_refunded' AND json_extract(payload_json,'$.reason') = 'operator_authorized_retry'", (s) => s.get(effectId)) as { total?: number } | undefined;
186
+ return Number(row?.total ?? 0);
187
+ }
188
+
189
+ function oldestUnappliedEffect(db: Database, taskId: string): WorkflowSupervisorEffect | undefined {
190
+ const row = statement(db, `SELECT e.* FROM effects e
191
+ WHERE e.task_id = ? AND NOT EXISTS (
192
+ SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied'
193
+ ) AND NOT EXISTS (
194
+ SELECT 1 FROM events superseded WHERE superseded.effect_id = e.effect_id AND superseded.kind = 'effect_outcome_unknown_superseded'
195
+ ) ORDER BY e.created_at, e.effect_id LIMIT 1`, (s) => s.get(taskId)) as Record<string, unknown> | undefined;
196
+ return row ? effectFromRow(row) : undefined;
197
+ }
198
+
199
+ /**
200
+ * Mechanical ceiling on how many times one un-applied Supervisor effect may be
201
+ * submitted to the provider. A proven non-application may legitimately be
202
+ * retried, but ChatGPT is one rate-limited shared resource: without a ceiling a
203
+ * single stuck effect minted an unbounded generation chain (live evidence:
204
+ * single effects were re-sent 130-166 times over hours), which is provider
205
+ * request volume that no Work actually asked for. Three total attempts is the
206
+ * hard ceiling: one normal submission plus at most two causally authorized retries.
207
+ */
208
+ export const WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS = 3;
209
+ /** A broken preparation cannot silently poll forever across Runtime restarts. */
210
+ export const WORKFLOW_SUPERVISOR_MAX_PREFLIGHT_RELEASE_ATTEMPTS = 3;
211
+
212
+ /**
213
+ * Once a provider has accepted a Supervisor effect, recovery never replays that
214
+ * source mutation. It may emit at most two separately identified recovery turns,
215
+ * matching the repository-wide three-provider-attempt ceiling (initial turn plus
216
+ * two causal retries). The depth is derived from effect origin links, not stored
217
+ * as another lifecycle state.
218
+ */
219
+ export const WORKFLOW_SUPERVISOR_MAX_PROVIDER_RECOVERY_TURNS = 2;
220
+ export const WORKFLOW_SUPERVISOR_DISPATCH_RETRY_BASE_MS = 30_000;
221
+ export const WORKFLOW_SUPERVISOR_DISPATCH_RETRY_MAX_MS = 10 * 60_000;
222
+ export const WORKFLOW_SUPERVISOR_UNKNOWN_OBSERVATION_BASE_MS = 5_000;
223
+ export const WORKFLOW_SUPERVISOR_UNKNOWN_OBSERVATION_MAX_MS = 60_000;
224
+ /**
225
+ * An explicit operator recovery may refund the mechanical retry budget for an
226
+ * effect whose latest dispatch carries a mechanical pre-send negative proof.
227
+ * That is a safety statement about *this* effect, not a reset of provider
228
+ * budgets, so it stays bounded: exceeding it requires a new explicit decision
229
+ * rather than another silent attempt.
230
+ */
231
+ export const WORKFLOW_SUPERVISOR_MAX_OPERATOR_REFUNDS = 2;
232
+ /**
233
+ * A standalone Supervisor task's conversation is its delivery surface. If that
234
+ * exact conversation becomes permanently unusable, an explicit operator may
235
+ * move the task to a replacement conversation. The budget keeps a transient
236
+ * provider problem from turning into a conversation-hopping loop.
237
+ */
238
+ export const WORKFLOW_SUPERVISOR_MAX_CONVERSATION_MIGRATIONS = 2;
239
+ export const WORKFLOW_SUPERVISOR_MIN_TURN_INTERVAL_MS = 3 * 60_000;
240
+
241
+ /** Spaced retries keep a persistent local obstacle from becoming a request storm. */
242
+ export function workflowSupervisorDispatchRetryDelayMs(generation: number): number {
243
+ const exponent = Math.max(0, Math.min(8, Math.trunc(generation) - 1));
244
+ return Math.min(WORKFLOW_SUPERVISOR_DISPATCH_RETRY_MAX_MS, WORKFLOW_SUPERVISOR_DISPATCH_RETRY_BASE_MS * 2 ** exponent);
245
+ }
246
+
247
+ export interface WorkflowSupervisorEffectDispatchBudget {
248
+ effectId: string;
249
+ generations: number;
250
+ maxGenerations: number;
251
+ lastDispatchedAtMs?: number;
252
+ retryDelayMs?: number;
253
+ exhausted: boolean;
254
+ }
255
+
256
+
257
+ interface EffectDispatchLedger {
258
+ generations: number;
259
+ budgetRefunds: number;
260
+ lastEventId: number;
261
+ lastGeneration: number;
262
+ lastOccurredAtMs: number;
263
+ }
264
+
265
+ interface EffectUnknownObservationLedger {
266
+ sameFingerprintCount: number;
267
+ lastOccurredAtMs: number;
268
+ fingerprint?: string;
269
+ }
270
+
271
+ function unknownObservationFingerprintFromObject(payload: Record<string, unknown>): string | undefined {
272
+ const explicit = boundedText(payload.observation_fingerprint);
273
+ if (explicit) return `explicit:${explicit}`;
274
+ // A transport may fail before a Browser snapshot exists. The canonical
275
+ // ledger still has to collapse repeated identical transport failures from
276
+ // native messaging, extensions, or older adapters. A changed reason/surface
277
+ // is materially new evidence and receives a fresh bounded budget.
278
+ const reason = boundedText(payload.reason) ?? 'unknown';
279
+ const surface = boundedText(payload.surface) ?? '';
280
+ const reconciliation = payload.reconciliation === true ? 'reconcile' : 'observe';
281
+ return `fallback:${surface}:${reconciliation}:${reason}`;
282
+ }
283
+
284
+ function unknownObservationFingerprint(payloadJson: unknown): string | undefined {
285
+ return unknownObservationFingerprintFromObject(parsedObject(payloadJson));
286
+ }
287
+
288
+ function effectUnknownObservationLedger(db: Database, effectId: string, afterEventId: number): EffectUnknownObservationLedger {
289
+ const rows = statement(db, "SELECT event_id,payload_json,occurred_at FROM events WHERE effect_id = ? AND kind = 'effect_unknown' AND event_id > ? ORDER BY event_id DESC LIMIT 32", (s) => s.all(effectId, afterEventId)) as Array<{ event_id?: number; payload_json?: string; occurred_at?: string }>;
290
+ const latest = rows[0];
291
+ if (!latest) return { sameFingerprintCount: 0, lastOccurredAtMs: 0 };
292
+ const fingerprint = unknownObservationFingerprint(latest.payload_json);
293
+ if (!fingerprint) return { sameFingerprintCount: 0, lastOccurredAtMs: 0 };
294
+ let sameFingerprintCount = 0;
295
+ for (const row of rows) {
296
+ if (unknownObservationFingerprint(row.payload_json) !== fingerprint) break;
297
+ sameFingerprintCount += 1;
298
+ }
299
+ const occurredAtMs = Date.parse(String(latest.occurred_at ?? ''));
300
+ return {
301
+ sameFingerprintCount,
302
+ lastOccurredAtMs: Number.isFinite(occurredAtMs) ? occurredAtMs : 0,
303
+ fingerprint,
304
+ };
305
+ }
306
+
307
+ function unknownObservationDelayMs(count: number): number {
308
+ const exponent = Math.max(0, Math.min(8, Math.trunc(count) - 1));
309
+ return Math.min(
310
+ WORKFLOW_SUPERVISOR_UNKNOWN_OBSERVATION_MAX_MS,
311
+ WORKFLOW_SUPERVISOR_UNKNOWN_OBSERVATION_BASE_MS * 2 ** exponent,
312
+ );
313
+ }
314
+
315
+ function effectDispatchLedger(db: Database, effectId: string): EffectDispatchLedger {
316
+ const rows = statement(db, "SELECT event_id,payload_json,occurred_at FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_started' ORDER BY event_id", (s) => s.all(effectId)) as Array<{ event_id?: number; payload_json?: string; occurred_at?: string }>;
317
+ const last = rows[rows.length - 1];
318
+ const refundRows = statement(db, "SELECT event_id FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_budget_refunded'", (s) => s.all(effectId)) as Array<{ event_id?: number }>;
319
+ const payload = parsedObject(last?.payload_json);
320
+ const payloadMs = Number(payload.dispatched_at_ms);
321
+ const lastOccurredAtMs = Number.isFinite(payloadMs) && payloadMs > 0
322
+ ? payloadMs
323
+ : Date.parse(String(last?.occurred_at ?? ''));
324
+ return {
325
+ generations: rows.length,
326
+ budgetRefunds: refundRows.length,
327
+ lastEventId: Number(last?.event_id ?? 0),
328
+ lastGeneration: last?.event_id ? storedGeneration(last.payload_json) : 0,
329
+ lastOccurredAtMs: Number.isFinite(lastOccurredAtMs) ? lastOccurredAtMs : 0,
330
+ };
331
+ }
332
+ /** Derived entirely from the Supervisor effect event log; no second retry authority. */
333
+ function effectPreflightFailureReleaseIds(db: Database, effectId: string): string[] {
334
+ const rows = statement(db, "SELECT payload_json FROM events WHERE effect_id = ? AND kind = 'effect_preflight_rejected' ORDER BY event_id", (s) => s.all(effectId)) as Array<{ payload_json?: string }>;
335
+ return rows.map((row) => String(parsedObject(row.payload_json).active_release_id ?? 'unidentified-runtime'));
336
+ }
337
+
338
+ export class WorkflowSupervisorStore {
339
+ private readonly db: Database;
340
+ private closed = false;
341
+ /** Mechanical clock for provider retry spacing. Durable event timestamps keep wall-clock ISO. */
342
+ private readonly clockMs: () => number;
343
+ private readonly activeReleaseId: () => string | undefined;
344
+
345
+ constructor(readonly forgeHome?: string, options: { now?: () => number; activeReleaseId?: () => string | undefined } = {}) {
346
+ this.db = openDatabase(forgeHome);
347
+ this.clockMs = options.now ?? Date.now;
348
+ this.activeReleaseId = options.activeReleaseId ?? (() => undefined);
349
+ }
350
+
351
+ private preflightBlocked(db: Database, effectId: string): boolean {
352
+ const tried = effectPreflightFailureReleaseIds(db, effectId);
353
+ return tried.length >= WORKFLOW_SUPERVISOR_MAX_PREFLIGHT_RELEASE_ATTEMPTS
354
+ || tried.includes(this.activeReleaseId()?.trim() || 'unidentified-runtime');
355
+ }
356
+
357
+ close(): void {
358
+ if (this.closed) return;
359
+ this.closed = true;
360
+ this.db.close();
361
+ }
362
+
363
+ private database(): Database {
364
+ if (this.closed) throw new Error('WORKFLOW_SUPERVISOR_STORE_CLOSED');
365
+ return this.db;
366
+ }
367
+
368
+ private transaction<T>(fn: (db: Database) => T): T {
369
+ const db = this.database();
370
+ db.exec('BEGIN IMMEDIATE');
371
+ try { const result = fn(db); db.exec('COMMIT'); return result; } catch (error) { db.exec('ROLLBACK'); throw error; }
372
+ }
373
+ private read<T>(fn: (db: Database) => T): T { return fn(this.database()); }
374
+
375
+ recordDiscovery(source: string, conversations: readonly WorkflowSupervisorDiscoveredConversation[]): WorkflowSupervisorDiscoverySnapshot {
376
+ const normalizedSource = source.trim();
377
+ if (!/^[a-z0-9][a-z0-9._:-]{0,127}$/i.test(normalizedSource)) throw new Error('WORKFLOW_SUPERVISOR_DISCOVERY_SOURCE_INVALID');
378
+ const observedAt = now();
379
+ this.transaction((db) => {
380
+ for (const conversation of conversations) {
381
+ const project = projectMetadataFromConversationUrl(conversation.canonicalUrl, conversation.projectTitle, conversation.projectUrl)
382
+ ?? { projectTitle: conversation.projectTitle, projectUrl: conversation.projectUrl };
383
+ statement(db, `INSERT INTO discovered_conversations(source,conversation_id,canonical_url,title,project_title,project_url,observed_at)
384
+ VALUES (?,?,?,?,?,?,?)
385
+ ON CONFLICT(source,conversation_id) DO UPDATE SET canonical_url=excluded.canonical_url,title=excluded.title,
386
+ project_title=CASE WHEN excluded.project_url IS NOT NULL AND discovered_conversations.project_url IS NOT NULL
387
+ AND excluded.project_url<>discovered_conversations.project_url THEN excluded.project_title
388
+ ELSE COALESCE(excluded.project_title,discovered_conversations.project_title) END,
389
+ project_url=COALESCE(excluded.project_url,discovered_conversations.project_url),observed_at=excluded.observed_at`,
390
+ (s) => s.run(normalizedSource, conversation.conversationId, conversation.canonicalUrl, conversation.title ?? null, project.projectTitle ?? null, project.projectUrl ?? null, observedAt));
391
+ }
392
+ });
393
+ return this.discoverySnapshot();
394
+ }
395
+
396
+ discoverySnapshot(): WorkflowSupervisorDiscoverySnapshot {
397
+ return this.read((db) => {
398
+ const rows = statement(db, 'SELECT * FROM discovered_conversations ORDER BY observed_at DESC, conversation_id, source', (s) => s.all()) as Record<string, unknown>[];
399
+ const byConversation = new Map<string, WorkflowSupervisorDiscoveredConversation>();
400
+ let observedAt = '';
401
+ for (const row of rows) {
402
+ const conversationId = String(row.conversation_id ?? '');
403
+ const canonicalUrl = String(row.canonical_url ?? '');
404
+ // Validate retained observations too: an older adapter may have tagged
405
+ // every sidebar conversation with the project page it happened to scan.
406
+ const project = projectMetadataFromConversationUrl(canonicalUrl, row.project_title ? String(row.project_title) : undefined, row.project_url ? String(row.project_url) : undefined)
407
+ ?? { projectTitle: row.project_title ? String(row.project_title) : undefined, projectUrl: row.project_url ? String(row.project_url) : undefined };
408
+ const rowObservedAt = String(row.observed_at ?? '');
409
+ if (rowObservedAt > observedAt) observedAt = rowObservedAt;
410
+ const current = byConversation.get(conversationId);
411
+ const candidate: WorkflowSupervisorDiscoveredConversation = {
412
+ conversationId,
413
+ canonicalUrl,
414
+ ...(row.title ? { title: String(row.title) } : {}),
415
+ ...(project.projectTitle ? { projectTitle: project.projectTitle } : {}),
416
+ ...(project.projectUrl ? { projectUrl: project.projectUrl } : {}),
417
+ };
418
+ if (!current) byConversation.set(conversationId, candidate);
419
+ else if (!current.projectTitle && candidate.projectTitle && (!current.projectUrl || current.projectUrl === candidate.projectUrl)) byConversation.set(conversationId, { ...current, projectTitle: candidate.projectTitle, ...(candidate.projectUrl ? { projectUrl: candidate.projectUrl } : {}) });
420
+ }
421
+ return { observedAt, conversations: [...byConversation.values()] };
422
+ });
423
+ }
424
+
425
+ registerTask(input: WorkflowSupervisorTaskInput): WorkflowSupervisorTask {
426
+ return this.transaction((db) => {
427
+ const createdAt = now();
428
+ statement(db, 'INSERT OR IGNORE INTO tasks VALUES (?, ?, ?, ?, ?, ?, ?, ?)', (s) => s.run(input.taskId, input.conversationId, input.conversationUrl, input.objective, json(input.completionContract), json(input.continuationPolicy), json(input.userBlockerPolicy), createdAt));
429
+ let row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
430
+ if (!row) {
431
+ row = statement(db, 'SELECT * FROM tasks WHERE conversation_id = ?', (s) => s.get(input.conversationId)) as Record<string, unknown> | undefined;
432
+ if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
433
+ const existing = taskFromRow(row);
434
+ if (existing.conversationUrl !== input.conversationUrl) throw new Error('WORKFLOW_SUPERVISOR_TASK_CONVERSATION_CONFLICT');
435
+ const existingRepo = typeof existing.completionContract.repo_id === 'string' ? existing.completionContract.repo_id : existing.continuationPolicy.repo_id;
436
+ const incomingRepo = typeof input.completionContract.repo_id === 'string' ? input.completionContract.repo_id : input.continuationPolicy.repo_id;
437
+ if (typeof existingRepo === 'string' && typeof incomingRepo === 'string' && existingRepo !== incomingRepo) throw new Error('WORKFLOW_SUPERVISOR_TASK_REPOSITORY_CONFLICT');
438
+ return existing;
439
+ }
440
+ const task = taskFromRow(row);
441
+ const taskRepo = typeof task.completionContract.repo_id === 'string' ? task.completionContract.repo_id : task.continuationPolicy.repo_id;
442
+ const inputRepo = typeof input.completionContract.repo_id === 'string' ? input.completionContract.repo_id : input.continuationPolicy.repo_id;
443
+ const { checkout_id: incomingCheckout, ...incomingCompletion } = input.completionContract;
444
+ const { checkout_id: selectedCheckout, ...selectedCompletion } = task.completionContract;
445
+ // Registration may fill an initially missing execution hint. It cannot
446
+ // replace a selected checkout or change task identity/semantic obligations.
447
+ const sameExecutionContextOwner = typeof taskRepo === 'string' && taskRepo === inputRepo
448
+ && task.conversationId === input.conversationId && task.conversationUrl === input.conversationUrl
449
+ && task.objective === input.objective
450
+ && json(selectedCompletion) === json(incomingCompletion)
451
+ && json(task.continuationPolicy) === json(input.continuationPolicy)
452
+ && json(task.userBlockerPolicy) === json(input.userBlockerPolicy);
453
+ // A retry of the original registration cannot erase the selected hint.
454
+ if (sameExecutionContextOwner && selectedCheckout !== undefined && incomingCheckout === undefined) return task;
455
+ const initialCheckoutContext = sameExecutionContextOwner && selectedCheckout === undefined
456
+ && task.continuationPolicy.checkout_id === undefined
457
+ && typeof incomingCheckout === 'string' && /^checkout_[a-zA-Z0-9_-]{8,120}$/.test(incomingCheckout);
458
+ if (initialCheckoutContext) {
459
+ statement(db, 'UPDATE tasks SET completion_contract_json = ? WHERE task_id = ?', s => s.run(json(input.completionContract), task.taskId));
460
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', s => s.run(
461
+ task.taskId, `task-checkout-context:${task.taskId}`, 'task_execution_context_enriched', json({ repo_id: taskRepo, checkout_id: incomingCheckout }), createdAt,
462
+ ));
463
+ return { ...task, completionContract: input.completionContract };
464
+ }
465
+ const bootstrapUpgrade = task.continuationPolicy.bootstrap === true
466
+ && input.continuationPolicy.bootstrap !== true
467
+ && taskRepo === inputRepo
468
+ && task.conversationId === input.conversationId
469
+ && task.conversationUrl === input.conversationUrl;
470
+ if (bootstrapUpgrade) {
471
+ statement(db, `UPDATE tasks
472
+ SET objective = ?, completion_contract_json = ?, continuation_policy_json = ?, user_blocker_policy_json = ?
473
+ WHERE task_id = ?`, (s) => s.run(
474
+ input.objective,
475
+ json(input.completionContract),
476
+ json(input.continuationPolicy),
477
+ json(input.userBlockerPolicy),
478
+ input.taskId,
479
+ ));
480
+ const upgraded = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
481
+ if (!upgraded) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
482
+ return taskFromRow(upgraded);
483
+ }
484
+ const projectBootstrap = task.continuationPolicy.kind === 'forge_project_conversation_outer_turn';
485
+ const sameProjectBootstrapIdentity = projectBootstrap
486
+ && task.conversationId === input.conversationId
487
+ && task.conversationUrl === input.conversationUrl
488
+ && (!taskRepo || !inputRepo || taskRepo === inputRepo);
489
+ if (sameProjectBootstrapIdentity) {
490
+ const explicitRequirementUpgrade = explicitRequirementTaskUpgrade(input);
491
+ if (input.continuationPolicy.kind === 'forge_goal_outer_turn') {
492
+ if (!explicitRequirementUpgrade || !taskRepo || taskRepo !== explicitRequirementUpgrade.repoId) {
493
+ throw new Error('WORKFLOW_SUPERVISOR_TASK_ID_CONFLICT');
494
+ }
495
+ statement(db, `UPDATE tasks
496
+ SET objective = ?, completion_contract_json = ?, continuation_policy_json = ?, user_blocker_policy_json = ?
497
+ WHERE task_id = ?`, (s) => s.run(
498
+ input.objective,
499
+ json(input.completionContract),
500
+ json(input.continuationPolicy),
501
+ json(input.userBlockerPolicy),
502
+ input.taskId,
503
+ ));
504
+ const upgraded = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
505
+ if (!upgraded) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
506
+ return taskFromRow(upgraded);
507
+ }
508
+ return task;
509
+ }
510
+ if (task.conversationId !== input.conversationId
511
+ || task.conversationUrl !== input.conversationUrl
512
+ || task.objective !== input.objective
513
+ || json(task.completionContract) !== json(input.completionContract)
514
+ || json(task.continuationPolicy) !== json(input.continuationPolicy)
515
+ || json(task.userBlockerPolicy) !== json(input.userBlockerPolicy)) {
516
+ throw new Error('WORKFLOW_SUPERVISOR_TASK_ID_CONFLICT');
517
+ }
518
+ return task;
519
+ });
520
+ }
521
+ reviseTaskObjective(input: {
522
+ taskId: string;
523
+ objective: string;
524
+ expectedObjectiveSha256: string;
525
+ requestId: string;
526
+ authorizedBy: string;
527
+ refreshEffect?: { effectId: string; expectedPrompt: string; prompt: string };
528
+ }): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; refreshedEffect?: WorkflowSupervisorEffect } {
529
+ return this.transaction((db) => {
530
+ const objective = input.objective.trim();
531
+ if (!objective || objective.length > 8_000) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_INVALID');
532
+ if (!/^[0-9a-f]{64}$/i.test(input.expectedObjectiveSha256)) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_EXPECTED_SHA_INVALID');
533
+ if (!input.requestId.trim()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_REQUIRED');
534
+ const targetSha = objectiveSha256(objective);
535
+ const eventKey = `task-objective-revise:${input.taskId}:${input.requestId}`;
536
+ const existingEvent = statement(db, 'SELECT payload_json FROM events WHERE event_key = ?', (s) => s.get(eventKey)) as { payload_json?: string } | undefined;
537
+ if (existingEvent) {
538
+ const payload = parsedObject(existingEvent.payload_json);
539
+ if (boundedText(payload.objective_sha256) !== targetSha) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_CONFLICT');
540
+ const taskRow = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
541
+ if (!taskRow) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
542
+ const refreshedEffectId = boundedText(payload.refreshed_effect_id);
543
+ const refreshedRow = refreshedEffectId
544
+ ? statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(refreshedEffectId)) as Record<string, unknown> | undefined
545
+ : undefined;
546
+ return {
547
+ task: taskFromRow(taskRow),
548
+ changed: payload.changed === true,
549
+ deduplicated: true,
550
+ objectiveSha256: targetSha,
551
+ ...(refreshedRow ? { refreshedEffect: effectFromRow(refreshedRow) } : {}),
552
+ };
553
+ }
554
+ const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
555
+ if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
556
+ const terminal = statement(db, "SELECT kind FROM events WHERE task_id = ? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') ORDER BY event_id DESC LIMIT 1", (s) => s.get(input.taskId)) as { kind?: string } | undefined;
557
+ if (terminal?.kind) throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
558
+ const task = taskFromRow(row);
559
+ const currentSha = objectiveSha256(task.objective);
560
+ if (currentSha !== input.expectedObjectiveSha256.toLowerCase()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_CONFLICT');
561
+
562
+ let refreshedEffect: WorkflowSupervisorEffect | undefined;
563
+ if (input.refreshEffect) {
564
+ const effectRow = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(input.refreshEffect!.effectId)) as Record<string, unknown> | undefined;
565
+ if (!effectRow) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
566
+ const effect = effectFromRow(effectRow);
567
+ if (effect.taskId !== input.taskId) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT');
568
+ if (effect.prompt !== input.refreshEffect.expectedPrompt) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_PROMPT_CONFLICT');
569
+ const dispatched = effectDispatchLedger(db, effect.effectId).generations > 0;
570
+ const applied = Boolean(statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effect.effectId)));
571
+ const superseded = effectSuperseded(db, effect.effectId);
572
+ if (!dispatched && !applied && !superseded) {
573
+ statement(db, 'UPDATE effects SET prompt_text = ? WHERE effect_id = ?', (s) => s.run(input.refreshEffect!.prompt, effect.effectId));
574
+ const updatedEffect = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(effect.effectId)) as Record<string, unknown> | undefined;
575
+ if (!updatedEffect) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
576
+ refreshedEffect = effectFromRow(updatedEffect);
577
+ }
578
+ }
579
+
580
+ const changed = task.objective !== objective;
581
+ if (changed) statement(db, 'UPDATE tasks SET objective = ? WHERE task_id = ?', (s) => s.run(objective, input.taskId));
582
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
583
+ input.taskId,
584
+ eventKey,
585
+ 'task_objective_revised',
586
+ json({
587
+ request_id: input.requestId,
588
+ authorized_by: input.authorizedBy,
589
+ previous_objective_sha256: currentSha,
590
+ objective_sha256: targetSha,
591
+ changed,
592
+ ...(refreshedEffect ? { refreshed_effect_id: refreshedEffect.effectId } : {}),
593
+ }),
594
+ now(),
595
+ ));
596
+ const updatedTask = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
597
+ if (!updatedTask) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
598
+ return { task: taskFromRow(updatedTask), changed, deduplicated: false, objectiveSha256: targetSha, ...(refreshedEffect ? { refreshedEffect } : {}) };
599
+ });
600
+ }
601
+ getTask(taskId: string): WorkflowSupervisorTask | undefined { return this.read((db) => { const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(taskId)); return row ? taskFromRow(row as Record<string, unknown>) : undefined; }); }
602
+ bindBootstrapConversation(taskId: string, conversationId: string, conversationUrl: string): WorkflowSupervisorTask {
603
+ return this.transaction((db) => {
604
+ const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(taskId)) as Record<string, unknown> | undefined;
605
+ if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
606
+ const task = taskFromRow(row);
607
+ if (!task.conversationId.startsWith('bootstrap:')) {
608
+ if (task.conversationId === conversationId && task.conversationUrl === conversationUrl) return task;
609
+ throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_ALREADY_BOUND');
610
+ }
611
+ // The migration event ledger is the sole authority for previously used
612
+ // source conversations. Do not rebind any retired source, including one
613
+ // from an earlier 15-turn rotation (not only the immediately prior chat).
614
+ const retired = statement(db, `SELECT 1 AS ok FROM events
615
+ WHERE task_id=? AND kind='conversation_migrated'
616
+ AND json_extract(payload_json,'$.from_conversation_id')=? LIMIT 1`,
617
+ (s) => s.get(taskId, conversationId));
618
+ if (retired) throw new Error('WORKFLOW_SUPERVISOR_FRESH_CONVERSATION_REBOUND_SOURCE');
619
+ const conflict = statement(db, 'SELECT task_id FROM tasks WHERE conversation_id = ? AND task_id <> ?', (s) => s.get(conversationId, taskId)) as { task_id?: string } | undefined;
620
+ if (conflict) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_CONVERSATION_CONFLICT');
621
+ statement(db, 'UPDATE tasks SET conversation_id = ?, conversation_url = ? WHERE task_id = ?', (s) => s.run(conversationId, conversationUrl, taskId));
622
+ const bound = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(taskId)) as Record<string, unknown> | undefined;
623
+ if (!bound) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
624
+ return taskFromRow(bound);
625
+ });
626
+ }
627
+ getTaskByConversationId(conversationId: string): WorkflowSupervisorTask | undefined { return this.read((db) => { const row = statement(db, 'SELECT * FROM tasks WHERE conversation_id = ?', (s) => s.get(conversationId)); return row ? taskFromRow(row as Record<string, unknown>) : undefined; }); }
628
+ listTasks(): WorkflowSupervisorTask[] { return this.read((db) => statement(db, 'SELECT * FROM tasks ORDER BY created_at, task_id', (s) => s.all()).map((row) => taskFromRow(row as Record<string, unknown>))); }
629
+ listTasksWithoutCausalObligation(limit = 16): WorkflowSupervisorTask[] {
630
+ const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 128));
631
+ return this.read((db) => statement(db, `SELECT t.* FROM tasks t
632
+ WHERE NOT EXISTS (
633
+ SELECT 1 FROM events terminal
634
+ WHERE terminal.task_id = t.task_id
635
+ AND terminal.kind IN ('terminal_done','terminal_needs_user','terminal_stopped')
636
+ )
637
+ AND NOT EXISTS (
638
+ SELECT 1 FROM effects e
639
+ WHERE e.task_id = t.task_id
640
+ AND NOT EXISTS (
641
+ SELECT 1 FROM events applied
642
+ WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied'
643
+ )
644
+ )
645
+ AND NOT EXISTS (
646
+ SELECT 1 FROM effects e
647
+ WHERE e.task_id = t.task_id
648
+ AND EXISTS (
649
+ SELECT 1 FROM events applied
650
+ WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied'
651
+ )
652
+ AND NOT EXISTS (
653
+ SELECT 1 FROM completions c
654
+ WHERE c.task_id = e.task_id AND c.source_effect_id = e.effect_id
655
+ )
656
+ )
657
+ ORDER BY t.created_at, t.task_id
658
+ LIMIT ?`, (s) => s.all(boundedLimit))
659
+ .map((row) => taskFromRow(row as Record<string, unknown>)));
660
+ }
661
+ getEffect(effectId: string): WorkflowSupervisorEffect | undefined { return this.read((db) => { const row = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(effectId)); return row ? effectFromRow(row as Record<string, unknown>) : undefined; }); }
662
+ currentUnappliedEffect(taskId: string): WorkflowSupervisorEffect | undefined { return this.read((db) => oldestUnappliedEffect(db, taskId)); }
663
+ getEffectByOriginKey(originKey: string): WorkflowSupervisorEffect | undefined { return this.read((db) => { const row = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(originKey)); return row ? effectFromRow(row as Record<string, unknown>) : undefined; }); }
664
+ getCompletion(completionFingerprint: string): WorkflowSupervisorCompletion | undefined { return this.read((db) => { const row = statement(db, 'SELECT * FROM completions WHERE completion_fingerprint = ?', (s) => s.get(completionFingerprint)); return row ? completionFromRow(row as Record<string, unknown>) : undefined; }); }
665
+ getCompletionBySourceEffectId(taskId: string, sourceEffectId: string): WorkflowSupervisorCompletion | undefined {
666
+ return this.read((db) => {
667
+ const row = statement(db, 'SELECT * FROM completions WHERE task_id = ? AND source_effect_id = ? LIMIT 1', (s) => s.get(taskId, sourceEffectId)) as Record<string, unknown> | undefined;
668
+ return row ? completionFromRow(row) : undefined;
669
+ });
670
+ }
671
+ listContinueCompletionsAwaitingSuccessor(limit = 16): WorkflowSupervisorCompletion[] {
672
+ const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 128));
673
+ return this.read((db) => statement(db, `SELECT c.* FROM completions c
674
+ WHERE c.action = 'CONTINUE'
675
+ AND NOT EXISTS (SELECT 1 FROM effects successor WHERE successor.origin_key = 'completion:' || c.completion_fingerprint)
676
+ AND NOT EXISTS (SELECT 1 FROM events terminal WHERE terminal.task_id = c.task_id AND terminal.kind IN ('terminal_done','terminal_needs_user','terminal_stopped'))
677
+ ORDER BY c.committed_at DESC, c.completion_fingerprint DESC LIMIT ?`, (s) => s.all(boundedLimit))
678
+ .map((row) => completionFromRow(row as Record<string, unknown>)));
679
+ }
680
+ getCompletionByResponseSha256(taskId: string, responseSha256: string): WorkflowSupervisorCompletion | undefined {
681
+ return this.read((db) => {
682
+ const rows = statement(db, 'SELECT * FROM completions WHERE task_id = ? AND response_sha256 = ? ORDER BY committed_at, completion_fingerprint LIMIT 2', (s) => s.all(taskId, responseSha256)) as Record<string, unknown>[];
683
+ if (rows.length > 1) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_COMPLETION_AMBIGUOUS');
684
+ return rows[0] ? completionFromRow(rows[0]) : undefined;
685
+ });
686
+ }
687
+ /** The only replay candidate when no applied effect is awaiting a receipt. */
688
+ getLatestCompletion(taskId: string): WorkflowSupervisorCompletion | undefined {
689
+ return this.read((db) => {
690
+ const row = statement(db, 'SELECT * FROM completions WHERE task_id = ? ORDER BY committed_at DESC, completion_fingerprint DESC LIMIT 1', (s) => s.get(taskId)) as Record<string, unknown> | undefined;
691
+ return row ? completionFromRow(row) : undefined;
692
+ });
693
+ }
694
+ continuationProof(input: { repoId?: string; activeReleaseId: string; notBefore: string }): WorkflowSupervisorContinuationProof | undefined {
695
+ if (!Number.isFinite(Date.parse(input.notBefore))) throw new Error('WORKFLOW_SUPERVISOR_PROOF_BOUNDARY_INVALID');
696
+ const activeReleaseId = boundedText(input.activeReleaseId);
697
+ if (!activeReleaseId) throw new Error('WORKFLOW_SUPERVISOR_PROOF_RELEASE_REQUIRED');
698
+ return this.read((db) => {
699
+ const taskRows = statement(db, 'SELECT * FROM tasks ORDER BY created_at DESC, task_id DESC', (s) => s.all()) as Record<string, unknown>[];
700
+ for (const taskRow of taskRows) {
701
+ const task = taskFromRow(taskRow);
702
+ const taskRepoId = boundedText(task.completionContract.repo_id) ?? boundedText(task.continuationPolicy.repo_id);
703
+ if (input.repoId?.trim() && taskRepoId !== input.repoId.trim()) continue;
704
+ const completionRows = statement(db, 'SELECT * FROM completions WHERE task_id = ? AND committed_at >= ? ORDER BY committed_at, completion_fingerprint', (s) => s.all(task.taskId, input.notBefore)) as Record<string, unknown>[];
705
+ const completions = completionRows.map(completionFromRow);
706
+ // Release acceptance proves continuation liveness, not semantic task completion.
707
+ // Requiring DONE here deadlocks a long-running Supervisor goal against its own known-good gate.
708
+ for (let index = 0; index + 1 < completions.length; index += 1) {
709
+ const first = completions[index]!;
710
+ const second = completions[index + 1]!;
711
+ if (first.action !== 'CONTINUE' || second.action !== 'CONTINUE') continue;
712
+ const rows = [first, second].map((completion) => statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(completion.sourceEffectId)) as Record<string, unknown> | undefined);
713
+ if (rows.some((row) => !row)) continue;
714
+ const [firstEffect, secondEffect] = rows.map((row) => effectFromRow(row!)) as [WorkflowSupervisorEffect, WorkflowSupervisorEffect];
715
+ if (secondEffect.sourceCompletionFingerprint !== first.completionFingerprint) continue;
716
+ const runtimeInstanceIds: string[] = [];
717
+ let valid = true;
718
+ for (const effect of [firstEffect, secondEffect]) {
719
+ const dispatchRows = statement(db, "SELECT payload_json FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_started' AND occurred_at >= ? ORDER BY event_id", (s) => s.all(effect.effectId, input.notBefore)) as Array<{ payload_json?: string }>;
720
+ if (dispatchRows.length !== 1) { valid = false; break; }
721
+ const evidence = parsedObject(dispatchRows[0]?.payload_json);
722
+ if (boundedText(evidence.active_release_id) !== activeReleaseId) { valid = false; break; }
723
+ const runtimeInstanceId = boundedText(evidence.runtime_instance_id);
724
+ if (!runtimeInstanceId) { valid = false; break; }
725
+ runtimeInstanceIds.push(runtimeInstanceId);
726
+ const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' AND occurred_at >= ? LIMIT 1", (s) => s.get(effect.effectId, input.notBefore));
727
+ if (!applied) { valid = false; break; }
728
+ }
729
+ const minimumRuntimeInstances = task.continuationPolicy.kind === 'standalone_supervisor' ? 1 : 2;
730
+ if (!valid || new Set(runtimeInstanceIds).size < minimumRuntimeInstances) continue;
731
+ return {
732
+ taskId: task.taskId, conversationId: task.conversationId, activeReleaseId,
733
+ actions: ['CONTINUE', 'CONTINUE'],
734
+ completionFingerprints: [first.completionFingerprint, second.completionFingerprint],
735
+ sourceEffectIds: [first.sourceEffectId, second.sourceEffectId],
736
+ runtimeInstanceIds: [...new Set(runtimeInstanceIds)],
737
+ firstCommittedAt: first.committedAt, lastCommittedAt: second.committedAt,
738
+ };
739
+ }
740
+ }
741
+ return undefined;
742
+ });
743
+ }
744
+ latestEffectDispatch(effectId: string): { eventId: number; generation: number; evidence: Record<string, unknown> } | undefined {
745
+ return this.read((db) => {
746
+ const row = statement(db, "SELECT event_id,payload_json FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_started' ORDER BY event_id DESC LIMIT 1", (s) => s.get(effectId)) as { event_id?: number; payload_json?: string } | undefined;
747
+ if (!row?.event_id) return undefined;
748
+ return { eventId: Number(row.event_id), generation: storedGeneration(row.payload_json), evidence: parsedObject(row.payload_json) };
749
+ });
750
+ }
751
+ /**
752
+ * Select the one browser command for a task's oldest un-applied effect.
753
+ *
754
+ * A dispatch generation is only minted from the canonical negative proof that
755
+ * the previous send never reached the conversation, and only while the effect
756
+ * stays inside its mechanical retry budget and retry window. Mutation retries
757
+ * retain a hard ceiling. Outcome-unknown reconciliation is read-only and stays
758
+ * eligible forever at a capped cadence so late provider evidence can recover
759
+ * the same effect without user intervention or another send.
760
+ */
761
+ nextBrowserEffect(
762
+ taskId: string,
763
+ options: { nowMs?: number } = {},
764
+ ): { effect: WorkflowSupervisorEffect; mode: 'send' | 'reconcile'; generation: number } | undefined {
765
+ return this.read((db) => {
766
+ const effect = oldestUnappliedEffect(db, taskId);
767
+ if (!effect) return undefined;
768
+ const ledger = effectDispatchLedger(db, effect.effectId);
769
+ const nowMs = options.nowMs ?? this.clockMs();
770
+ if (ledger.generations === 0) {
771
+ // A fresh continuation is one further provider turn of the same task.
772
+ // Spacing is measured from the completion that authorized it, so a fast
773
+ // provider reply cannot turn the outer-turn chain into a request storm.
774
+ const source = effect.sourceCompletionFingerprint
775
+ ? statement(db, 'SELECT committed_at FROM completions WHERE completion_fingerprint = ?', (s) => s.get(effect.sourceCompletionFingerprint!)) as { committed_at?: string } | undefined
776
+ : undefined;
777
+ const committedAtMs = Date.parse(String(source?.committed_at ?? ''));
778
+ if (Number.isFinite(committedAtMs) && nowMs - committedAtMs < WORKFLOW_SUPERVISOR_MIN_TURN_INTERVAL_MS) return undefined;
779
+ if (this.preflightBlocked(db, effect.effectId)) return undefined;
780
+ return { effect, mode: 'send', generation: 1 };
781
+ }
782
+ const retryAuthorized = latestNotAppliedProofEventId(db, effect.effectId) > ledger.lastEventId;
783
+ if (!retryAuthorized) {
784
+ const unknownLedger = effectUnknownObservationLedger(db, effect.effectId, ledger.lastEventId);
785
+ if (unknownLedger.sameFingerprintCount > 0
786
+ && nowMs - unknownLedger.lastOccurredAtMs < unknownObservationDelayMs(unknownLedger.sameFingerprintCount)) {
787
+ return undefined;
788
+ }
789
+ return { effect, mode: 'reconcile', generation: ledger.lastGeneration };
790
+ }
791
+ if (ledger.generations >= WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + ledger.budgetRefunds) return undefined;
792
+ if (this.preflightBlocked(db, effect.effectId)) return undefined;
793
+ if (nowMs - ledger.lastOccurredAtMs < workflowSupervisorDispatchRetryDelayMs(ledger.lastGeneration)) {
794
+ return undefined;
795
+ }
796
+ return { effect, mode: 'send', generation: ledger.lastGeneration + 1 };
797
+ });
798
+ }
799
+ effectDispatchBudget(effectId: string): WorkflowSupervisorEffectDispatchBudget {
800
+ return this.read((db) => {
801
+ const ledger = effectDispatchLedger(db, effectId);
802
+ const exhausted = ledger.generations >= WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + ledger.budgetRefunds
803
+ && !statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId));
804
+ return {
805
+ effectId,
806
+ generations: ledger.generations,
807
+ maxGenerations: WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS,
808
+ ...(ledger.lastOccurredAtMs ? { lastDispatchedAtMs: ledger.lastOccurredAtMs } : {}),
809
+ ...(ledger.generations ? { retryDelayMs: workflowSupervisorDispatchRetryDelayMs(ledger.lastGeneration) } : {}),
810
+ exhausted,
811
+ };
812
+ });
813
+ }
814
+ /**
815
+ * Read-only classification of why a non-terminal task is or is not advancing.
816
+ * It derives only from the existing effect/observation ledger, so it adds no
817
+ * second durable state and no new lifecycle authority.
818
+ */
819
+ taskStall(taskId: string): WorkflowSupervisorTaskStall {
820
+ if (this.terminalAction(taskId)) return { state: 'terminal' };
821
+ const next = this.nextBrowserEffect(taskId);
822
+ if (next) {
823
+ if (next.mode === 'send') return { state: 'deliverable', effectId: next.effect.effectId, mode: 'send', generation: next.generation };
824
+ return { state: 'spaced', effectId: next.effect.effectId, reason: 'unknown_observation_spacing' };
825
+ }
826
+ const pending = this.currentUnappliedEffect(taskId);
827
+ if (pending) {
828
+ const ledger = this.read((db) => ({ ...effectDispatchLedger(db, pending.effectId), proof: latestNotAppliedProofEventId(db, pending.effectId), preflightBlocked: this.preflightBlocked(db, pending.effectId) }));
829
+ if (ledger.preflightBlocked) return { state: 'spaced', effectId: pending.effectId, reason: 'pre_send_infrastructure_blocked' };
830
+ const ceiling = WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + ledger.budgetRefunds;
831
+ const retryAuthorized = ledger.generations === 0 || ledger.proof > ledger.lastEventId;
832
+ // An exhausted retry ceiling is the operator-facing state: no amount of
833
+ // waiting can mint another generation. Everything else is ordinary
834
+ // mechanical spacing and needs no operator action.
835
+ if (retryAuthorized && ledger.generations >= ceiling) {
836
+ return { state: 'retryable', effectId: pending.effectId, generations: ledger.generations, maxGenerations: ceiling };
837
+ }
838
+ return { state: 'spaced', effectId: pending.effectId, reason: retryAuthorized ? 'retry_spacing' : 'unknown_observation_spacing' };
839
+ }
840
+ const leaf = this.latestAppliedLeafEffectWithoutCompletion(taskId);
841
+ if (leaf && this.providerResumeExhausted(leaf.effectId)) return { state: 'provider_resume_exhausted', effectId: leaf.effectId };
842
+ // An applied effect awaiting its receipt is a live obligation even when its
843
+ // bounded resume is exhausted and its recovery child was retired, so it is
844
+ // never reported as inert.
845
+ if (this.hasAppliedEffectAwaitingCompletion(taskId)) {
846
+ return { state: 'spaced', ...(leaf ? { effectId: leaf.effectId } : {}), reason: 'awaiting_completion_receipt' };
847
+ }
848
+ return { state: 'inert' };
849
+ }
850
+ /**
851
+ * Explicit operator authority to continue a *mechanically proven* un-applied
852
+ * effect after its retry ceiling. The proof is the dispatch owner's persisted
853
+ * pre-send negative result, never page absence, so this can never replay a
854
+ * submitted mutation. Idempotent per request id and bounded per effect.
855
+ */
856
+ authorizeOperatorRetry(input: { effectId: string; requestId: string; reason: string; authorizedBy: string }): { refunds: number; generations: number; deduplicated: boolean } {
857
+ return this.transaction((db) => {
858
+ const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(input.effectId)) as { task_id?: string } | undefined;
859
+ if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
860
+ if (statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(input.effectId))) {
861
+ throw new Error('WORKFLOW_SUPERVISOR_EFFECT_ALREADY_APPLIED');
862
+ }
863
+ if (statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_outcome_unknown_superseded' LIMIT 1", (s) => s.get(input.effectId))) {
864
+ throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_EFFECT_SUPERSEDED');
865
+ }
866
+ const ledger = effectDispatchLedger(db, input.effectId);
867
+ if (ledger.generations === 0 || latestNotAppliedProofEventId(db, input.effectId) <= ledger.lastEventId) {
868
+ throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_OUTCOME_UNKNOWN');
869
+ }
870
+ const key = `effect-dispatch-operator-refund:${input.effectId}:${input.requestId}`;
871
+ const existing = statement(db, 'SELECT 1 AS ok FROM events WHERE event_key = ?', (s) => s.get(key));
872
+ if (existing) return { refunds: operatorAuthorizedRefundCount(db, input.effectId), generations: ledger.generations, deduplicated: true };
873
+ if (operatorAuthorizedRefundCount(db, input.effectId) >= WORKFLOW_SUPERVISOR_MAX_OPERATOR_REFUNDS) {
874
+ throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_REFUND_BUDGET_EXHAUSTED');
875
+ }
876
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
877
+ effect.task_id!, key, 'effect_dispatch_budget_refunded', input.effectId,
878
+ json({
879
+ reason: 'operator_authorized_retry', request_id: input.requestId, authorized_by: input.authorizedBy,
880
+ reason_text: input.reason, prior_generations: ledger.generations, attestation: 'mechanical_pre_send_negative_proof',
881
+ }), now()));
882
+ return { refunds: operatorAuthorizedRefundCount(db, input.effectId), generations: ledger.generations, deduplicated: false };
883
+ });
884
+ }
885
+ /**
886
+ * Explicit operator authority to retire an effect whose submission outcome is
887
+ * genuinely unknown. It is never resent; the durable fact records who decided
888
+ * to stop waiting, and the replacement turn is reserved in the same
889
+ * transaction so the task can never sit in a half-superseded state. Requires
890
+ * the caller to have stated the intent.
891
+ */
892
+ supersedeUnknownEffect(input: {
893
+ effectId: string;
894
+ requestId: string;
895
+ reason: string;
896
+ authorizedBy: string;
897
+ replacement: { effectId: string; prompt: string };
898
+ }): { superseded: boolean; recoveryEffect: WorkflowSupervisorEffect } {
899
+ const originKey = `operator-recovery:${input.effectId}:${input.requestId}`;
900
+ return this.transaction((db) => {
901
+ const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(input.effectId)) as { task_id?: string } | undefined;
902
+ if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
903
+ const taskId = effect.task_id;
904
+ const reserve = (): WorkflowSupervisorEffect => this.reserveEffectWithin(db, {
905
+ taskId, effectId: input.replacement.effectId, kind: 'recovery', originKey, prompt: input.replacement.prompt,
906
+ });
907
+ const existingReplacement = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(originKey)) as Record<string, unknown> | undefined;
908
+ if (existingReplacement) return { superseded: true, recoveryEffect: effectFromRow(existingReplacement) };
909
+ if (statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(input.effectId))) {
910
+ throw new Error('WORKFLOW_SUPERVISOR_EFFECT_ALREADY_APPLIED');
911
+ }
912
+ const ledger = effectDispatchLedger(db, input.effectId);
913
+ if (ledger.generations === 0) throw new Error('WORKFLOW_SUPERVISOR_SUPERSEDE_NOT_DISPATCHED');
914
+ if (latestNotAppliedProofEventId(db, input.effectId) > ledger.lastEventId) {
915
+ throw new Error('WORKFLOW_SUPERVISOR_SUPERSEDE_NOT_APPLIED_PROVEN');
916
+ }
917
+ const key = `effect-outcome-unknown-superseded:${input.effectId}:${input.requestId}`;
918
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
919
+ taskId, key, 'effect_outcome_unknown_superseded', input.effectId,
920
+ json({
921
+ request_id: input.requestId, authorized_by: input.authorizedBy, reason: input.reason,
922
+ generation: ledger.lastGeneration, replacement_effect_id: input.replacement.effectId,
923
+ attestation: 'explicit_operator_authorization',
924
+ }), now()));
925
+ return { superseded: true, recoveryEffect: reserve() };
926
+ });
927
+ }
928
+ terminalAction(taskId: string): WorkflowSupervisorTerminalState | undefined {
929
+ return this.read((db) => {
930
+ const row = statement(db, "SELECT kind FROM events WHERE task_id = ? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') ORDER BY event_id DESC LIMIT 1", (s) => s.get(taskId)) as { kind?: string } | undefined;
931
+ return row?.kind === 'terminal_done' ? 'DONE' : row?.kind === 'terminal_needs_user' ? 'NEEDS_USER' : row?.kind === 'terminal_stopped' ? 'STOPPED' : undefined;
932
+ });
933
+ }
934
+ stopTask(taskId: string, reason: string): { taskId: string; terminal: 'STOPPED'; deduplicated: boolean } {
935
+ return this.transaction((db) => {
936
+ const task = statement(db, 'SELECT task_id FROM tasks WHERE task_id = ?', (s) => s.get(taskId)) as { task_id?: string } | undefined;
937
+ if (!task) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
938
+ const existing = statement(db, "SELECT kind FROM events WHERE task_id = ? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') ORDER BY event_id DESC LIMIT 1", (s) => s.get(taskId)) as { kind?: string } | undefined;
939
+ if (existing?.kind === 'terminal_stopped') return { taskId, terminal: 'STOPPED', deduplicated: true };
940
+ if (existing?.kind) {
941
+ const terminal = existing.kind === 'terminal_done' ? 'DONE' : 'NEEDS_USER';
942
+ throw new Error(`WORKFLOW_SUPERVISOR_TASK_TERMINAL:${terminal}`);
943
+ }
944
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(taskId, `task-stop:${taskId}`, 'terminal_stopped', json({ reason }), now()));
945
+ return { taskId, terminal: 'STOPPED', deduplicated: false };
946
+ });
947
+ }
948
+ effectApplied(effectId: string): boolean { return this.read((db) => Boolean(statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId)))); }
949
+ providerResumeExhausted(effectId: string): boolean { return this.read((db) => Boolean(statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'assistant_recovery_exhausted' LIMIT 1", (s) => s.get(effectId)))); }
950
+ latestAppliedEffectWithoutCompletion(taskId: string): WorkflowSupervisorEffect | undefined {
951
+ return this.read((db) => {
952
+ const row = statement(db, `SELECT e.* FROM effects e
953
+ WHERE e.task_id = ?
954
+ AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied')
955
+ AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.task_id = e.task_id AND c.source_effect_id = e.effect_id)
956
+ AND NOT EXISTS (SELECT 1 FROM effects child WHERE child.origin_key = 'provider-recovery:' || e.effect_id)
957
+ AND NOT EXISTS (SELECT 1 FROM events exhausted WHERE exhausted.effect_id = e.effect_id AND exhausted.kind = 'assistant_recovery_exhausted')
958
+ ORDER BY (SELECT MAX(event_id) FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied') DESC
959
+ LIMIT 1`, (s) => s.get(taskId)) as Record<string, unknown> | undefined;
960
+ return row ? effectFromRow(row) : undefined;
961
+ });
962
+ }
963
+ latestAppliedLeafEffectWithoutCompletion(taskId: string): WorkflowSupervisorEffect | undefined {
964
+ return this.read((db) => {
965
+ const row = statement(db, `SELECT e.* FROM effects e
966
+ WHERE e.task_id = ?
967
+ AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied')
968
+ AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.task_id = e.task_id AND c.source_effect_id = e.effect_id)
969
+ AND NOT EXISTS (SELECT 1 FROM effects child WHERE child.origin_key = 'provider-recovery:' || e.effect_id)
970
+ ORDER BY (SELECT MAX(event_id) FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied') DESC
971
+ LIMIT 1`, (s) => s.get(taskId)) as Record<string, unknown> | undefined;
972
+ return row ? effectFromRow(row) : undefined;
973
+ });
974
+ }
975
+
976
+ /**
977
+ * An applied browser effect remains a Supervisor-owned external observation
978
+ * obligation even when its lower ControllerRound temporarily waits. This
979
+ * intentionally includes effects that already have a bounded provider
980
+ * recovery child; the native adapter must stay alive long enough to observe
981
+ * and dispatch that recovery instead of abandoning the browser task.
982
+ */
983
+ hasAppliedEffectAwaitingCompletion(taskId: string): boolean {
984
+ return this.read((db) => Boolean(statement(db, `SELECT 1 AS ok FROM effects e
985
+ WHERE e.task_id = ?
986
+ AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied')
987
+ AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.task_id = e.task_id AND c.source_effect_id = e.effect_id)
988
+ LIMIT 1`, (s) => s.get(taskId))));
989
+ }
990
+
991
+ observeProviderTurn(input: {
992
+ taskId: string;
993
+ effectId: string;
994
+ generating: boolean;
995
+ assistantDigest: string;
996
+ providerFailureCode?: string;
997
+ receiptMissing?: boolean;
998
+ observedAtMs: number;
999
+ graceMs: number;
1000
+ recovery: { effectId: string; prompt: string };
1001
+ }): { state: 'none' | 'generating' | 'idle_pending' | 'recovery_reserved' | 'exhausted'; recoveryEffect?: WorkflowSupervisorEffect } {
1002
+ if (!Number.isFinite(input.observedAtMs)) throw new Error('WORKFLOW_SUPERVISOR_PROVIDER_OBSERVED_AT_INVALID');
1003
+ const graceMs = Math.max(1_000, Math.min(10 * 60_000, Math.floor(input.graceMs)));
1004
+ const digest = input.assistantDigest.trim().slice(0, 128);
1005
+ const observedAt = new Date(input.observedAtMs).toISOString();
1006
+ return this.transaction((db) => {
1007
+ const row = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(input.effectId)) as Record<string, unknown> | undefined;
1008
+ if (!row) return { state: 'none' };
1009
+ const effect = effectFromRow(row);
1010
+ if (effect.taskId !== input.taskId) throw new Error('WORKFLOW_SUPERVISOR_PROVIDER_EFFECT_TASK_MISMATCH');
1011
+ const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effect.effectId));
1012
+ const completed = statement(db, 'SELECT 1 AS ok FROM completions WHERE task_id = ? AND source_effect_id = ? LIMIT 1', (s) => s.get(input.taskId, effect.effectId));
1013
+ if (!applied || completed) return { state: 'none' };
1014
+ const providerRecoveryDepth = providerRecoveryDepthWithin(db, effect.effectId);
1015
+ const recoveryOrigin = `provider-recovery:${effect.effectId}`;
1016
+ const providerFailureCode = input.providerFailureCode?.trim().slice(0, 128);
1017
+ if (providerFailureCode) {
1018
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
1019
+ input.taskId,
1020
+ `assistant-provider-failed:${effect.effectId}:${providerFailureCode}`,
1021
+ 'assistant_provider_failed',
1022
+ effect.effectId,
1023
+ json({ code: providerFailureCode, assistant_digest: digest }),
1024
+ observedAt,
1025
+ ));
1026
+ }
1027
+ const existingRecovery = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(recoveryOrigin)) as Record<string, unknown> | undefined;
1028
+ if (existingRecovery) return { state: 'recovery_reserved', recoveryEffect: effectFromRow(existingRecovery) };
1029
+ if (providerFailureCode) {
1030
+ if (providerRecoveryDepth >= WORKFLOW_SUPERVISOR_MAX_PROVIDER_RECOVERY_TURNS) {
1031
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, `assistant-recovery-exhausted:${effect.effectId}`, 'assistant_recovery_exhausted', effect.effectId, json({ assistant_digest: digest, provider_failure_code: providerFailureCode, exactly_once_resume: true, recovery_depth: providerRecoveryDepth, max_recovery_turns: WORKFLOW_SUPERVISOR_MAX_PROVIDER_RECOVERY_TURNS }), observedAt));
1032
+ return { state: 'exhausted' };
1033
+ }
1034
+ const recoveryEffect = this.reserveEffectWithin(db, { taskId: input.taskId, effectId: input.recovery.effectId, kind: 'recovery', originKey: recoveryOrigin, prompt: input.recovery.prompt });
1035
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, `assistant-recovery-reserved:${effect.effectId}`, 'assistant_recovery_reserved', effect.effectId, json({ recovery_effect_id: recoveryEffect.effectId, provider_failure_code: providerFailureCode, exactly_once_resume: true }), observedAt));
1036
+ return { state: 'recovery_reserved', recoveryEffect };
1037
+ }
1038
+
1039
+ const latest = statement(db, `SELECT event_id,kind,payload_json,occurred_at FROM events
1040
+ WHERE effect_id = ? AND kind IN ('assistant_provider_generating','assistant_provider_idle')
1041
+ ORDER BY event_id DESC LIMIT 1`, (s) => s.get(effect.effectId)) as { event_id?: number; kind?: string; payload_json?: string; occurred_at?: string } | undefined;
1042
+ const latestEvidence = parsedObject(latest?.payload_json);
1043
+ const desiredKind = input.generating ? 'assistant_provider_generating' : 'assistant_provider_idle';
1044
+ const sameState = latest?.kind === desiredKind && latestEvidence.assistant_digest === digest;
1045
+ if (!sameState) {
1046
+ const eventKey = `assistant-provider-state:${effect.effectId}:${desiredKind}:${Number(latest?.event_id ?? 0) + 1}:${digest || 'empty'}`;
1047
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, eventKey, desiredKind, effect.effectId, json({ assistant_digest: digest }), observedAt));
1048
+ return { state: input.generating ? 'generating' : 'idle_pending' };
1049
+ }
1050
+ const unchangedSinceMs = Date.parse(String(latest?.occurred_at ?? ''));
1051
+ if (!Number.isFinite(unchangedSinceMs) || input.observedAtMs - unchangedSinceMs < graceMs) {
1052
+ return { state: input.generating ? 'generating' : 'idle_pending' };
1053
+ }
1054
+
1055
+ // Ordinary idle text cannot authorize recovery. Only a stable, exact
1056
+ // assistant response proven to lack its receipt can reserve the existing
1057
+ // bounded, separately identified recovery; never replay the source effect.
1058
+ if (!input.generating && !input.receiptMissing) return { state: 'idle_pending' };
1059
+
1060
+ // Progress, not the provider's visible "generating" control, is the
1061
+ // liveness authority. A frozen page can leave that control present for
1062
+ // hours after tool/output progress stopped. Once the assistant/activity
1063
+ // digest is unchanged for the existing grace window, reserve the next
1064
+ // separately identified recovery turn without replaying the applied source.
1065
+ // The causal chain is bounded to two automatic recovery turns.
1066
+ if (providerRecoveryDepth >= WORKFLOW_SUPERVISOR_MAX_PROVIDER_RECOVERY_TURNS) {
1067
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, `assistant-recovery-exhausted:${effect.effectId}`, 'assistant_recovery_exhausted', effect.effectId, json({ assistant_digest: digest, exactly_once_resume: true, stale_generation: input.generating, recovery_depth: providerRecoveryDepth, max_recovery_turns: WORKFLOW_SUPERVISOR_MAX_PROVIDER_RECOVERY_TURNS }), observedAt));
1068
+ return { state: 'exhausted' };
1069
+ }
1070
+ const recoveryEffect = this.reserveEffectWithin(db, { taskId: input.taskId, effectId: input.recovery.effectId, kind: 'recovery', originKey: recoveryOrigin, prompt: input.recovery.prompt });
1071
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, `assistant-recovery-reserved:${effect.effectId}`, 'assistant_recovery_reserved', effect.effectId, json({ recovery_effect_id: recoveryEffect.effectId, exactly_once_resume: true, stale_generation: input.generating }), observedAt));
1072
+ return { state: 'recovery_reserved', recoveryEffect };
1073
+ });
1074
+ }
1075
+
1076
+ reserveEffect(input: { taskId: string; effectId: string; kind: WorkflowEffectKind; originKey: string; sourceCompletionFingerprint?: string; prompt: string }): WorkflowSupervisorEffect {
1077
+ return this.transaction((db) => this.reserveEffectWithin(db, input));
1078
+ }
1079
+ reserveOperatorRecovery(input: { taskId: string; sourceEffectId: string; requestId: string; reason: string; effectId: string; prompt: string }): WorkflowSupervisorEffect {
1080
+ return this.transaction((db) => {
1081
+ if (oldestUnappliedEffect(db, input.taskId)) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_OUTCOME_UNKNOWN');
1082
+ const source = statement(db, `SELECT e.* FROM effects e WHERE e.task_id = ?
1083
+ AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id=e.effect_id AND applied.kind='effect_applied')
1084
+ AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.source_effect_id=e.effect_id)
1085
+ AND NOT EXISTS (SELECT 1 FROM effects child WHERE child.origin_key='provider-recovery:' || e.effect_id)
1086
+ ORDER BY e.created_at DESC LIMIT 1`, (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
1087
+ if (source?.effect_id !== input.sourceEffectId) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
1088
+ if (statement(db, "SELECT 1 FROM events WHERE task_id=? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') LIMIT 1", (s) => s.get(input.taskId))) throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
1089
+ const effect = this.reserveEffectWithin(db, { taskId: input.taskId, effectId: input.effectId, kind: 'recovery', originKey: `provider-recovery:${input.sourceEffectId}`, prompt: input.prompt });
1090
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
1091
+ input.taskId, `assistant-recovery-reserved:${input.sourceEffectId}`, 'assistant_recovery_reserved', input.sourceEffectId,
1092
+ json({ recovery_effect_id: effect.effectId, requested_by: 'user', request_id: input.requestId, reason: input.reason, exactly_once_resume: true }), now()));
1093
+ return effect;
1094
+ });
1095
+ }
1096
+ /**
1097
+ * Explicit operator recovery for an applied effect whose one bounded provider
1098
+ * resume was already exhausted. The source effect stays applied and completed
1099
+ * history is untouched; this only reserves one new, separately identified
1100
+ * provider turn, so it can never replay the source mutation.
1101
+ */
1102
+ reserveOperatorProviderRecovery(input: { taskId: string; sourceEffectId: string; requestId: string; reason: string; authorizedBy: string; originKey: string; effectId: string; prompt: string }): WorkflowSupervisorEffect {
1103
+ return this.transaction((db) => {
1104
+ if (oldestUnappliedEffect(db, input.taskId)) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_OUTCOME_UNKNOWN');
1105
+ if (statement(db, "SELECT 1 FROM events WHERE task_id=? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') LIMIT 1", (s) => s.get(input.taskId))) {
1106
+ throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
1107
+ }
1108
+ const source = statement(db, `SELECT e.* FROM effects e WHERE e.task_id = ? AND e.effect_id = ?
1109
+ AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id=e.effect_id AND applied.kind='effect_applied')
1110
+ AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.source_effect_id=e.effect_id)
1111
+ LIMIT 1`, (s) => s.get(input.taskId, input.sourceEffectId)) as Record<string, unknown> | undefined;
1112
+ if (!source) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
1113
+ // This class exists precisely because the bounded automatic resume ran
1114
+ // out. Without that durable fact there is no authorization here.
1115
+ if (!providerResumeExhaustedWithin(db, input.sourceEffectId)) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_NOT_EXHAUSTED');
1116
+ const originKey = input.originKey;
1117
+ const effect = this.reserveEffectWithin(db, { taskId: input.taskId, effectId: input.effectId, kind: 'recovery', originKey, prompt: input.prompt });
1118
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
1119
+ input.taskId, `assistant-recovery-reserved:${originKey}`, 'assistant_recovery_reserved', input.sourceEffectId,
1120
+ json({
1121
+ recovery_effect_id: effect.effectId, requested_by: 'user', authorized_by: input.authorizedBy,
1122
+ request_id: input.requestId, reason: input.reason, exactly_once_resume: true, operator_authorized: true,
1123
+ }), now()));
1124
+ return effect;
1125
+ });
1126
+ }
1127
+ freshConversationMigrationContext(taskId: string): { fromConversationId: string; requestId: string } | undefined {
1128
+ return this.read((db) => {
1129
+ const row = statement(db, "SELECT payload_json FROM events WHERE task_id = ? AND kind = 'conversation_migrated' ORDER BY event_id DESC LIMIT 1", (s) => s.get(taskId)) as { payload_json?: string } | undefined;
1130
+ const payload = parsedObject(row?.payload_json);
1131
+ if (payload.fresh_conversation !== true || payload.to_conversation_id !== `bootstrap:${taskId}`) return undefined;
1132
+ const fromConversationId = typeof payload.from_conversation_id === 'string' ? payload.from_conversation_id.trim() : '';
1133
+ const requestId = typeof payload.request_id === 'string' ? payload.request_id.trim() : '';
1134
+ if (!fromConversationId || !requestId) return undefined;
1135
+ return { fromConversationId, requestId };
1136
+ });
1137
+ }
1138
+
1139
+ /**
1140
+ * Explicit operator switch of an exhausted task to a *fresh* conversation in
1141
+ * its configured Project. This is the one path that legitimately opens a new
1142
+ * chat: it exists for an exact conversation that is durably unusable, never as
1143
+ * a reaction to provider backpressure. The whole move and its replacement
1144
+ * enrollment turn commit in one transaction, so the task can never be left on
1145
+ * a fresh conversation holding a prompt that assumes prior context.
1146
+ */
1147
+ migrateToFreshConversation(input: {
1148
+ taskId: string;
1149
+ expectedConversationId: string;
1150
+ requestId: string;
1151
+ reason: string;
1152
+ authorizedBy: string;
1153
+ replacement: { effectId: string; prompt: string };
1154
+ }): { task: WorkflowSupervisorTask; migrated: boolean; retiredEffectId?: string } {
1155
+ return this.transaction((db) => {
1156
+ const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
1157
+ if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
1158
+ const task = taskFromRow(row);
1159
+ if (task.conversationId !== input.expectedConversationId) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_STALE');
1160
+ if (statement(db, "SELECT 1 FROM events WHERE task_id=? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') LIMIT 1", (s) => s.get(input.taskId))) {
1161
+ throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
1162
+ }
1163
+ const targetConversationId = `bootstrap:${input.taskId}`;
1164
+ if (task.conversationId === targetConversationId) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_ALREADY_PENDING');
1165
+ // Bound repeated unsuccessful moves, not the lifetime of a healthy task.
1166
+ // A genuine completion reopens the budget for its next 15-turn rotation.
1167
+ const prior = statement(db, `SELECT COUNT(*) AS total FROM events
1168
+ WHERE task_id = ? AND kind = 'conversation_migrated'
1169
+ AND event_id > COALESCE(
1170
+ (SELECT MAX(event_id) FROM events WHERE task_id = ? AND kind = 'assistant_completion'), 0
1171
+ )`, (s) => s.get(input.taskId, input.taskId)) as { total?: number } | undefined;
1172
+ if (Number(prior?.total ?? 0) >= WORKFLOW_SUPERVISOR_MAX_CONVERSATION_MIGRATIONS) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_BUDGET_EXHAUSTED');
1173
+ // A submitted mutation whose outcome is unknown must be reconciled, never
1174
+ // abandoned by moving the task elsewhere.
1175
+ const pending = oldestUnappliedEffect(db, input.taskId);
1176
+ if (pending) {
1177
+ const ledger = effectDispatchLedger(db, pending.effectId);
1178
+ const negativeProof = latestNotAppliedProofEventId(db, pending.effectId) > ledger.lastEventId;
1179
+ if (ledger.generations > 0 && !negativeProof) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_OUTCOME_UNKNOWN');
1180
+ }
1181
+ const originKey = `fresh-conversation:${input.taskId}:${input.requestId}`;
1182
+ const replacement = this.reserveEffectWithin(db, {
1183
+ taskId: input.taskId, effectId: input.replacement.effectId, kind: 'enrollment', originKey, prompt: input.replacement.prompt,
1184
+ });
1185
+ if (pending && pending.effectId !== replacement.effectId) {
1186
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
1187
+ input.taskId, `effect-retired-fresh-conversation:${pending.effectId}:${input.requestId}`, 'effect_outcome_unknown_superseded',
1188
+ pending.effectId,
1189
+ json({
1190
+ request_id: input.requestId, authorized_by: input.authorizedBy, reason: input.reason,
1191
+ generation: 0, replacement_effect_id: replacement.effectId, attestation: 'never_dispatched_fresh_conversation',
1192
+ }), now()));
1193
+ }
1194
+ statement(db, 'UPDATE tasks SET conversation_id = ?, conversation_url = ? WHERE task_id = ?', (s) => s.run(targetConversationId, 'https://chatgpt.com/', input.taskId));
1195
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
1196
+ input.taskId, `conversation-migrated:${input.taskId}:${input.requestId}`, 'conversation_migrated',
1197
+ json({
1198
+ from_conversation_id: task.conversationId, to_conversation_id: targetConversationId, to_conversation_url: 'https://chatgpt.com/',
1199
+ fresh_conversation: true, replacement_effect_id: replacement.effectId,
1200
+ request_id: input.requestId, authorized_by: input.authorizedBy, reason: input.reason,
1201
+ ...(pending ? { retired_effect_id: pending.effectId } : {}),
1202
+ }), now()));
1203
+ const updated = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
1204
+ if (!updated) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
1205
+ return {
1206
+ task: taskFromRow(updated), migrated: true,
1207
+ ...(pending && pending.effectId !== replacement.effectId ? { retiredEffectId: pending.effectId } : {}),
1208
+ };
1209
+ });
1210
+ }
1211
+
1212
+ migrateConversation(input: { taskId: string; expectedConversationId: string; conversationId: string; conversationUrl: string; requestId: string; reason: string; authorizedBy: string }): { task: WorkflowSupervisorTask; migrated: boolean } {
1213
+ return this.transaction((db) => {
1214
+ const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
1215
+ if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
1216
+ const task = taskFromRow(row);
1217
+ if (task.conversationId !== input.expectedConversationId) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_STALE');
1218
+ if (statement(db, "SELECT 1 FROM events WHERE task_id=? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') LIMIT 1", (s) => s.get(input.taskId))) {
1219
+ throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
1220
+ }
1221
+ if (task.conversationId === input.conversationId) {
1222
+ if (task.conversationUrl !== input.conversationUrl) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_CONVERSATION_CONFLICT');
1223
+ return { task, migrated: false };
1224
+ }
1225
+ const conflict = statement(db, 'SELECT task_id FROM tasks WHERE conversation_id = ? AND task_id <> ?', (s) => s.get(input.conversationId, input.taskId)) as { task_id?: string } | undefined;
1226
+ if (conflict) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_CONVERSATION_CONFLICT');
1227
+ // Bound repeated unsuccessful moves, not the lifetime of a healthy task.
1228
+ // A genuine completion reopens the budget for its next 15-turn rotation.
1229
+ const prior = statement(db, `SELECT COUNT(*) AS total FROM events
1230
+ WHERE task_id = ? AND kind = 'conversation_migrated'
1231
+ AND event_id > COALESCE(
1232
+ (SELECT MAX(event_id) FROM events WHERE task_id = ? AND kind = 'assistant_completion'), 0
1233
+ )`, (s) => s.get(input.taskId, input.taskId)) as { total?: number } | undefined;
1234
+ if (Number(prior?.total ?? 0) >= WORKFLOW_SUPERVISOR_MAX_CONVERSATION_MIGRATIONS) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_BUDGET_EXHAUSTED');
1235
+ const pending = oldestUnappliedEffect(db, input.taskId);
1236
+ if (pending) {
1237
+ const ledger = effectDispatchLedger(db, pending.effectId);
1238
+ const negativeProof = latestNotAppliedProofEventId(db, pending.effectId) > ledger.lastEventId;
1239
+ if (ledger.generations > 0 && !negativeProof) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_OUTCOME_UNKNOWN');
1240
+ }
1241
+ statement(db, 'UPDATE tasks SET conversation_id = ?, conversation_url = ? WHERE task_id = ?', (s) => s.run(input.conversationId, input.conversationUrl, input.taskId));
1242
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
1243
+ input.taskId, `conversation-migrated:${input.taskId}:${input.requestId}`, 'conversation_migrated',
1244
+ json({
1245
+ from_conversation_id: task.conversationId, to_conversation_id: input.conversationId, to_conversation_url: input.conversationUrl,
1246
+ request_id: input.requestId, authorized_by: input.authorizedBy, reason: input.reason,
1247
+ ...(pending ? { pending_effect_id: pending.effectId } : {}),
1248
+ }), now()));
1249
+ const updated = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
1250
+ if (!updated) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
1251
+ return { task: taskFromRow(updated), migrated: true };
1252
+ });
1253
+ }
1254
+ reconcileNativePreSendFailure(effectId: string, requestId: string): boolean {
1255
+ // Bounded compatibility repair for the pre-20261002 native dispatcher.
1256
+ // These exact returned reasons occur before Send is clicked. Other unknown
1257
+ // observations, including transport exceptions, are never negative proof.
1258
+ const row = this.read((db) => {
1259
+ const dispatch = effectDispatchLedger(db, effectId);
1260
+ const owner = statement(db, "SELECT payload_json FROM events WHERE event_id=? AND kind='effect_dispatch_started'", (s) => s.get(dispatch.lastEventId)) as { payload_json?: string } | undefined;
1261
+ if (parsedObject(owner?.payload_json).surface !== 'macos-native') return undefined;
1262
+ // Other observers can report a transport error while the native sender
1263
+ // is still preparing. Only that generation's dispatch owner can prove
1264
+ // it returned before clicking Send.
1265
+ return statement(db, "SELECT payload_json FROM events WHERE effect_id=? AND kind='effect_unknown' AND event_id>? AND json_extract(payload_json,'$.surface')='macos-native' ORDER BY event_id LIMIT 1", (s) => s.get(effectId, dispatch.lastEventId)) as { payload_json?: string } | undefined;
1266
+ });
1267
+ const evidence = parsedObject(row?.payload_json);
1268
+ const beforeSend = new Set(['composer_missing', 'composer_not_empty', 'composer_selection_unavailable', 'composer_text_insertion_rejected', 'composer_text_unconfirmed', 'composer_submit_mismatch', 'send_button_missing']);
1269
+ if (evidence.surface !== 'macos-native' || evidence.reconciliation === true || !beforeSend.has(String(evidence.reason))) return false;
1270
+ this.recordEffectNotAppliedProof(effectId, `operator-pre-send:${requestId}`, { ...evidence, send_clicked: false, requested_by: 'user', request_id: requestId, attestation: 'persisted_native_pre_send_return' });
1271
+ return true;
1272
+ }
1273
+ private reserveEffectWithin(db: Database, input: { taskId: string; effectId: string; kind: WorkflowEffectKind; originKey: string; sourceCompletionFingerprint?: string; prompt: string }): WorkflowSupervisorEffect {
1274
+ statement(db, 'INSERT OR IGNORE INTO effects(effect_id,task_id,kind,origin_key,source_completion_fingerprint,prompt_text,created_at) VALUES (?,?,?,?,?,?,?)', (s) => s.run(input.effectId, input.taskId, input.kind, input.originKey, input.sourceCompletionFingerprint ?? null, input.prompt, now()));
1275
+ const row = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(input.originKey)) as Record<string, unknown> | undefined;
1276
+ if (!row) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_RESERVE_FAILED');
1277
+ const effect = effectFromRow(row);
1278
+ if (effect.effectId !== input.effectId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_ID_CONFLICT:${input.originKey}:${effect.effectId}:${input.effectId}`);
1279
+ if (effect.taskId !== input.taskId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT:${input.originKey}`);
1280
+ if (effect.kind !== input.kind) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_KIND_CONFLICT:${input.originKey}`);
1281
+ if ((effect.sourceCompletionFingerprint ?? '') !== (input.sourceCompletionFingerprint ?? '')) {
1282
+ throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_SOURCE_CONFLICT:${input.originKey}`);
1283
+ }
1284
+ if (effect.prompt !== input.prompt) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_PROMPT_CONFLICT:${input.originKey}`);
1285
+ return effect;
1286
+ }
1287
+
1288
+ /** Attest that no Send intent was reserved and the provider UI could not prepare. */
1289
+ recordEffectPreparationFailure(effectId: string, reasonCode: string): void {
1290
+ this.transaction((db) => {
1291
+ const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
1292
+ if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
1293
+ const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId));
1294
+ const dispatch = effectDispatchLedger(db, effectId);
1295
+ if (applied || (dispatch.generations > 0 && latestNotAppliedProofEventId(db, effectId) <= dispatch.lastEventId)) {
1296
+ throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_FAILURE_OUTCOME_NOT_PROVEN');
1297
+ }
1298
+ const activeReleaseId = this.activeReleaseId()?.trim() || 'unidentified-runtime';
1299
+ const key = `effect-preflight-rejected:${effectId}:${activeReleaseId}`;
1300
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
1301
+ effect.task_id!, key, 'effect_preflight_rejected', effectId,
1302
+ json({ active_release_id: activeReleaseId, reason: (boundedText(reasonCode) ?? 'COMPUTER_PREPARE_REJECTED').slice(0, 256), send_clicked: false, attestation: 'computer_prepare_returned_before_supervisor_send_intent' }), now(),
1303
+ ));
1304
+ });
1305
+ }
1306
+
1307
+ recordEffectDispatchStarted(effectId: string, generation: number, dispatchId: string, evidence: Record<string, unknown> = {}): boolean {
1308
+ if (!Number.isInteger(generation) || generation < 1 || generation > 1_000_000) throw new Error('WORKFLOW_SUPERVISOR_DISPATCH_GENERATION_INVALID');
1309
+ return this.transaction((db) => {
1310
+ const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
1311
+ if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
1312
+ const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId));
1313
+ if (applied) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_ALREADY_APPLIED');
1314
+ const refunds = statement(db, "SELECT event_id FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_budget_refunded'", (s) => s.all(effectId)) as Array<{ event_id?: number }>;
1315
+ if (generation > WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + refunds.length || this.preflightBlocked(db, effectId)) return false;
1316
+ const prior = statement(db, "SELECT event_id,payload_json FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_started' ORDER BY event_id DESC LIMIT 1", (s) => s.get(effectId)) as { event_id?: number; payload_json?: string } | undefined;
1317
+ const currentGeneration = prior?.event_id ? storedGeneration(prior.payload_json) : 0;
1318
+ const retryAuthorized = !prior?.event_id || latestNotAppliedProofEventId(db, effectId) > Number(prior.event_id);
1319
+ if (!retryAuthorized || generation !== currentGeneration + 1) return false;
1320
+ // `dispatched_at_ms` is the mechanical retry-spacing fact. It is recorded
1321
+ // inside the evidence payload so retry spacing stays deterministic under
1322
+ // an injected clock without changing durable event timestamps.
1323
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(effect.task_id, `effect-dispatch:${effectId}:${generation}`, 'effect_dispatch_started', effectId, json({ dispatchId, generation, dispatched_at_ms: this.clockMs(), ...evidence }), now()));
1324
+ return true;
1325
+ });
1326
+ }
1327
+
1328
+ recordEffectObservation(effectId: string, observationId: string, outcome: WorkflowEffectOutcome, evidence: Record<string, unknown> = {}): void {
1329
+ if (outcome === 'not_applied') throw new Error('WORKFLOW_SUPERVISOR_NOT_APPLIED_PROOF_REQUIRED');
1330
+ this.transaction((db) => {
1331
+ const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
1332
+ if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
1333
+ const key = `effect-observation:${effectId}:${observationId}`;
1334
+ const kind = `effect_${outcome}`;
1335
+ const existing = statement(db, 'SELECT kind,payload_json FROM events WHERE event_key = ?', (s) => s.get(key)) as { kind?: string; payload_json?: string } | undefined;
1336
+ const payload = json(evidence);
1337
+ if (existing && (existing.kind !== kind || existing.payload_json !== payload)) throw new Error('WORKFLOW_SUPERVISOR_OBSERVATION_ID_CONFLICT');
1338
+ // Identical unknown observations remain append-only evidence at the capped
1339
+ // reconciliation cadence. Permanently suppressing them freezes the last
1340
+ // observation timestamp and makes a transient outcome-unknown state unable
1341
+ // to discover late provider evidence after Runtime/browser recovery.
1342
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(effect.task_id, key, kind, effectId, payload, now()));
1343
+ });
1344
+ }
1345
+
1346
+ recordEffectNotAppliedProof(effectId: string, observationId: string, proof: Record<string, unknown>): void {
1347
+ this.transaction((db) => {
1348
+ const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
1349
+ if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
1350
+ const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId));
1351
+ if (applied) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_ALREADY_APPLIED');
1352
+ const key = `effect-observation:${effectId}:${observationId}`;
1353
+ const payload = json(proof);
1354
+ const existing = statement(db, 'SELECT kind,payload_json FROM events WHERE event_key = ?', (s) => s.get(key)) as { kind?: string; payload_json?: string } | undefined;
1355
+ if (existing && (existing.kind !== 'effect_not_applied' || existing.payload_json !== payload)) throw new Error('WORKFLOW_SUPERVISOR_OBSERVATION_ID_CONFLICT');
1356
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(effect.task_id, key, 'effect_not_applied', effectId, payload, now()));
1357
+ const ledger = effectDispatchLedger(db, effectId);
1358
+ if (proof.pre_send_rejection === true
1359
+ && ledger.generations >= WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS
1360
+ && ledger.budgetRefunds === 0) {
1361
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(effect.task_id, `effect-dispatch-budget-refund:${effectId}`, 'effect_dispatch_budget_refunded', effectId, json({ reason: 'pre_send_rejection_did_not_reach_provider' }), now()));
1362
+ }
1363
+ });
1364
+ }
1365
+
1366
+ commitCompletion(input: WorkflowSupervisorCompletion, successor?: { effectId: string; kind: WorkflowEffectKind; prompt: string; freshEnrollmentPrompt?: string }): { completion: WorkflowSupervisorCompletion; successorEffect?: WorkflowSupervisorEffect; deduplicated: boolean } {
1367
+ return this.transaction((db) => {
1368
+ const task = statement(db, 'SELECT 1 AS ok FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId));
1369
+ if (!task) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
1370
+ const sourceEffect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(input.sourceEffectId)) as { task_id?: string } | undefined;
1371
+ if (sourceEffect?.task_id !== input.taskId) throw new Error('WORKFLOW_SUPERVISOR_SOURCE_EFFECT_MISMATCH');
1372
+ const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(input.sourceEffectId));
1373
+ if (!applied) throw new Error('WORKFLOW_SUPERVISOR_SOURCE_EFFECT_NOT_APPLIED');
1374
+ const priorForEffect = statement(db, 'SELECT completion_fingerprint FROM completions WHERE task_id = ? AND source_effect_id = ?', (s) => s.get(input.taskId, input.sourceEffectId)) as { completion_fingerprint?: string } | undefined;
1375
+ if (priorForEffect?.completion_fingerprint && priorForEffect.completion_fingerprint !== input.completionFingerprint) {
1376
+ throw new Error('WORKFLOW_SUPERVISOR_SOURCE_EFFECT_COMPLETION_CONFLICT');
1377
+ }
1378
+ const existing = statement(db, 'SELECT completion_fingerprint FROM completions WHERE completion_fingerprint = ?', (s) => s.get(input.completionFingerprint));
1379
+ if (!existing) statement(db, 'INSERT INTO completions VALUES (?,?,?,?,?,?,?,?)', (s) => s.run(input.completionFingerprint, input.taskId, input.sourceEffectId, input.action, input.responseSha256, input.controlBlockSha256, json(input.proposal), input.committedAt));
1380
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,completion_fingerprint,payload_json,occurred_at) VALUES (?,?,?,?,?,?,?)', (s) => s.run(input.taskId, `completion:${input.completionFingerprint}`, 'assistant_completion', input.sourceEffectId, input.completionFingerprint, json(input.proposal), input.committedAt));
1381
+ let successorEffect: WorkflowSupervisorEffect | undefined;
1382
+ if (successor) {
1383
+ const originKey = `completion:${input.completionFingerprint}`;
1384
+ const priorSuccessorRow = existing
1385
+ ? statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(originKey)) as Record<string, unknown> | undefined
1386
+ : undefined;
1387
+ if (priorSuccessorRow) {
1388
+ const priorSuccessor = effectFromRow(priorSuccessorRow);
1389
+ if (priorSuccessor.taskId !== input.taskId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT:${originKey}`);
1390
+ if (priorSuccessor.kind !== successor.kind && !(successor.freshEnrollmentPrompt && priorSuccessor.kind === 'enrollment')) {
1391
+ throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_KIND_CONFLICT:${originKey}`);
1392
+ }
1393
+ if ((priorSuccessor.sourceCompletionFingerprint ?? '') !== input.completionFingerprint) {
1394
+ throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_SOURCE_CONFLICT:${originKey}`);
1395
+ }
1396
+ successorEffect = priorSuccessor;
1397
+ } else {
1398
+ // User-authorized periodic rotation is a mechanical cadence over
1399
+ // completed CONTINUE receipts, never sends, elapsed time or Work state.
1400
+ // The completion, fresh enrollment, binding change and migration
1401
+ // evidence commit atomically in the existing Supervisor journal.
1402
+ const current = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown>;
1403
+ const currentTask = taskFromRow(current);
1404
+ const lastMigration = statement(db, "SELECT MAX(event_id) AS id FROM events WHERE task_id=? AND kind='conversation_migrated'", (s) => s.get(input.taskId)) as { id?: number };
1405
+ const completedSinceMigration = statement(db, `SELECT COUNT(*) AS total FROM events e
1406
+ JOIN completions c ON c.completion_fingerprint=e.completion_fingerprint
1407
+ WHERE e.task_id=? AND e.kind='assistant_completion' AND e.event_id>? AND c.action='CONTINUE'`,
1408
+ (s) => s.get(input.taskId, lastMigration?.id ?? 0)) as { total?: number };
1409
+ const rotate = successor.freshEnrollmentPrompt !== undefined
1410
+ && currentTask.continuationPolicy.kind === 'standalone_supervisor'
1411
+ && !currentTask.conversationId.startsWith('bootstrap:')
1412
+ && (completedSinceMigration.total ?? 0) >= 15;
1413
+ successorEffect = this.reserveEffectWithin(db, {
1414
+ taskId: input.taskId, effectId: successor.effectId,
1415
+ kind: rotate ? 'enrollment' : successor.kind, originKey,
1416
+ sourceCompletionFingerprint: input.completionFingerprint,
1417
+ prompt: rotate ? successor.freshEnrollmentPrompt! : successor.prompt,
1418
+ });
1419
+ if (rotate) {
1420
+ const sourceConversationId = currentTask.conversationId;
1421
+ const bootstrapConversationId = `bootstrap:${input.taskId}`;
1422
+ statement(db, 'UPDATE tasks SET conversation_id=?, conversation_url=? WHERE task_id=?',
1423
+ (s) => s.run(bootstrapConversationId, 'https://chatgpt.com/', input.taskId));
1424
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
1425
+ input.taskId, `conversation-migrated:auto:${input.completionFingerprint}`, 'conversation_migrated',
1426
+ json({ from_conversation_id: sourceConversationId, to_conversation_id: bootstrapConversationId,
1427
+ to_conversation_url: 'https://chatgpt.com/', fresh_conversation: true,
1428
+ replacement_effect_id: successorEffect!.effectId, request_id: `automatic-receipt:${input.completionFingerprint}`,
1429
+ authorized_by: 'user-configured-standalone-rotation', reason: '15_confirmed_continue_receipts' }), now()));
1430
+ }
1431
+ }
1432
+ }
1433
+ return { completion: input, ...(successorEffect ? { successorEffect } : {}), deduplicated: Boolean(existing) };
1434
+ });
1435
+ }
1436
+
1437
+ resolveTerminal(input: { completionFingerprint: string; taskId: string; action: 'DONE' | 'NEEDS_USER'; accepted: boolean; reason: string; correction?: { effectId: string; prompt: string } }): { successorEffect?: WorkflowSupervisorEffect; deduplicated: boolean } {
1438
+ return this.transaction((db) => {
1439
+ const key = `terminal-resolution:${input.completionFingerprint}`;
1440
+ const existing = statement(db, 'SELECT kind,payload_json FROM events WHERE event_key = ?', (s) => s.get(key)) as { kind?: string; payload_json?: string } | undefined;
1441
+ if (existing) {
1442
+ const effect = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(`completion:${input.completionFingerprint}`));
1443
+ return { ...(effect ? { successorEffect: effectFromRow(effect as Record<string, unknown>) } : {}), deduplicated: true };
1444
+ }
1445
+ const kind = input.accepted ? (input.action === 'DONE' ? 'terminal_done' : 'terminal_needs_user') : 'terminal_rejected';
1446
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,completion_fingerprint,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, key, kind, input.completionFingerprint, json({ reason: input.reason }), now()));
1447
+ const successorEffect = !input.accepted && input.correction ? this.reserveEffectWithin(db, { taskId: input.taskId, effectId: input.correction.effectId, kind: 'correction', originKey: `completion:${input.completionFingerprint}`, sourceCompletionFingerprint: input.completionFingerprint, prompt: input.correction.prompt }) : undefined;
1448
+ return { ...(successorEffect ? { successorEffect } : {}), deduplicated: false };
1449
+ });
1450
+ }
1451
+ }