@moretea-labs/forge 1.8.1 → 1.8.2

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 (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -1,8 +1,9 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { createRequire } from 'node:module';
3
3
  import { mkdirSync } from 'node:fs';
4
4
  import { resolveWorkflowSupervisorForgeHome, workflowSupervisorDatabasePathValue, workflowSupervisorRootPath } from './paths';
5
- import type { WorkflowEffectKind, WorkflowEffectOutcome, WorkflowSupervisorCompletion, WorkflowSupervisorContinuationProof, WorkflowSupervisorDiscoverySnapshot, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTaskStall, WorkflowSupervisorTerminalState } from './types';
5
+ import { validateSupervisorObjective } from './protocol';
6
+ import type { WorkflowEffectKind, WorkflowEffectOutcome, WorkflowSupervisorCompletion, WorkflowSupervisorContinuationProof, WorkflowSupervisorDiscoverySnapshot, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTaskStall, WorkflowSupervisorTerminalState, WorkflowSupervisorPreflightComposerObservation } from './types';
6
7
  export type { WorkflowSupervisorTaskStall } from './types';
7
8
 
8
9
  interface Statement { get(...params: unknown[]): unknown; all(...params: unknown[]): unknown[]; run(...params: unknown[]): unknown; finalize?(): void }
@@ -53,6 +54,10 @@ function json(value: unknown): string {
53
54
  return JSON.stringify(value);
54
55
  }
55
56
 
57
+ export function workflowSupervisorCompletionContractSha256(contract: Record<string, unknown>): string {
58
+ return createHash('sha256').update(json(contract)).digest('hex');
59
+ }
60
+
56
61
  export { resolveWorkflowSupervisorForgeHome } from './paths';
57
62
  export function workflowSupervisorRoot(forgeHome?: string): string {
58
63
  const root = workflowSupervisorRootPath(forgeHome);
@@ -91,6 +96,10 @@ function openDatabase(forgeHome?: string): Database {
91
96
  kind TEXT NOT NULL, effect_id TEXT, completion_fingerprint TEXT, payload_json TEXT NOT NULL, occurred_at TEXT NOT NULL
92
97
  );
93
98
  CREATE INDEX IF NOT EXISTS events_task_order ON events(task_id, event_id);
99
+ -- Terminal probes run for every task on the polling/reconciliation paths.
100
+ -- Index only terminal facts so an open task does not rescan its whole log.
101
+ CREATE INDEX IF NOT EXISTS events_terminal_task_order ON events(task_id, event_id)
102
+ WHERE kind IN ('terminal_done','terminal_needs_user','terminal_stopped');
94
103
  CREATE INDEX IF NOT EXISTS events_effect_kind_order ON events(effect_id, kind, event_id);
95
104
  CREATE TABLE IF NOT EXISTS discovered_conversations (
96
105
  source TEXT NOT NULL, conversation_id TEXT NOT NULL, canonical_url TEXT NOT NULL, title TEXT,
@@ -143,6 +152,7 @@ function completionFromRow(row: Record<string, unknown>): WorkflowSupervisorComp
143
152
  };
144
153
  }
145
154
  function parsedObject(value: unknown): Record<string, unknown> {
155
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>;
146
156
  try { const parsed = JSON.parse(String(value)); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {}; }
147
157
  catch { return {}; }
148
158
  }
@@ -151,7 +161,12 @@ function storedGeneration(value: unknown): number {
151
161
  return Number.isInteger(generation) && generation > 0 ? generation : 1;
152
162
  }
153
163
  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;
164
+ // Delayed negative evidence from an older generation must not authorize
165
+ // another Send while the latest generation is outcome-unknown. Apply this
166
+ // read-side fence to journals written by earlier releases as well.
167
+ const dispatch = 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;
168
+ if (!dispatch?.event_id) return 0;
169
+ const notApplied = statement(db, "SELECT event_id FROM events WHERE effect_id = ? AND kind = 'effect_not_applied' AND event_id > ? AND json_extract(payload_json,'$.dispatch_generation') = ? ORDER BY event_id DESC LIMIT 1", (s) => s.get(effectId, dispatch.event_id, storedGeneration(dispatch.payload_json))) as { event_id?: number } | undefined;
155
170
  return Number(notApplied?.event_id ?? 0);
156
171
  }
157
172
 
@@ -192,6 +207,15 @@ function oldestUnappliedEffect(db: Database, taskId: string): WorkflowSupervisor
192
207
  SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied'
193
208
  ) AND NOT EXISTS (
194
209
  SELECT 1 FROM events superseded WHERE superseded.effect_id = e.effect_id AND superseded.kind = 'effect_outcome_unknown_superseded'
210
+ ) AND NOT (
211
+ e.kind = 'recovery' AND NOT EXISTS (
212
+ SELECT 1 FROM events started
213
+ WHERE started.effect_id = e.effect_id AND started.kind = 'effect_dispatch_started'
214
+ ) AND EXISTS (
215
+ SELECT 1 FROM completions completed
216
+ WHERE completed.task_id = e.task_id
217
+ AND e.origin_key = 'provider-recovery:' || completed.source_effect_id
218
+ )
195
219
  ) ORDER BY e.created_at, e.effect_id LIMIT 1`, (s) => s.get(taskId)) as Record<string, unknown> | undefined;
196
220
  return row ? effectFromRow(row) : undefined;
197
221
  }
@@ -312,6 +336,12 @@ function unknownObservationDelayMs(count: number): number {
312
336
  );
313
337
  }
314
338
 
339
+ function unknownObservationDue(db: Database, effectId: string, afterEventId: number, nowMs: number): boolean {
340
+ const observation = effectUnknownObservationLedger(db, effectId, afterEventId);
341
+ return observation.sameFingerprintCount === 0
342
+ || nowMs - observation.lastOccurredAtMs >= unknownObservationDelayMs(observation.sameFingerprintCount);
343
+ }
344
+
315
345
  function effectDispatchLedger(db: Database, effectId: string): EffectDispatchLedger {
316
346
  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
347
  const last = rows[rows.length - 1];
@@ -330,9 +360,9 @@ function effectDispatchLedger(db: Database, effectId: string): EffectDispatchLed
330
360
  };
331
361
  }
332
362
  /** 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'));
363
+ function effectPreflightFailureReleaseIds(db: Database, effectId: string): Array<{ eventId: number; releaseId: string }> {
364
+ const rows = statement(db, "SELECT event_id,payload_json FROM events WHERE effect_id = ? AND kind = 'effect_preflight_rejected' ORDER BY event_id", (s) => s.all(effectId)) as Array<{ event_id?: number; payload_json?: string }>;
365
+ return rows.map((row) => ({ eventId: Number(row.event_id ?? 0), releaseId: String(parsedObject(row.payload_json).active_release_id ?? 'unidentified-runtime') }));
336
366
  }
337
367
 
338
368
  export class WorkflowSupervisorStore {
@@ -350,8 +380,18 @@ export class WorkflowSupervisorStore {
350
380
 
351
381
  private preflightBlocked(db: Database, effectId: string): boolean {
352
382
  const tried = effectPreflightFailureReleaseIds(db, effectId);
353
- return tried.length >= WORKFLOW_SUPERVISOR_MAX_PREFLIGHT_RELEASE_ATTEMPTS
354
- || tried.includes(this.activeReleaseId()?.trim() || 'unidentified-runtime');
383
+ const releaseId = this.activeReleaseId()?.trim() || 'unidentified-runtime';
384
+ const lastFailure = tried.filter((entry) => entry.releaseId === releaseId).at(-1);
385
+ // Three failed releases exhaust automatic re-preparation, not an
386
+ // operator's single explicit post-repair proof. The same durable event
387
+ // ledger still enforces one rearm at most and re-blocks after its failure.
388
+ const rearm = statement(db, "SELECT event_id FROM events WHERE effect_id = ? AND kind = 'effect_preflight_operator_rearmed' ORDER BY event_id DESC LIMIT 1", (s) => s.get(effectId)) as { event_id?: number } | undefined;
389
+ const rearmedEventId = Number(rearm?.event_id ?? 0);
390
+ if (tried.length >= WORKFLOW_SUPERVISOR_MAX_PREFLIGHT_RELEASE_ATTEMPTS) {
391
+ return rearmedEventId <= (tried.at(-1)?.eventId ?? 0);
392
+ }
393
+ if (!lastFailure) return false;
394
+ return rearmedEventId <= lastFailure.eventId;
355
395
  }
356
396
 
357
397
  close(): void {
@@ -423,6 +463,7 @@ export class WorkflowSupervisorStore {
423
463
  }
424
464
 
425
465
  registerTask(input: WorkflowSupervisorTaskInput): WorkflowSupervisorTask {
466
+ validateSupervisorObjective(input.objective);
426
467
  return this.transaction((db) => {
427
468
  const createdAt = now();
428
469
  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));
@@ -468,6 +509,14 @@ export class WorkflowSupervisorStore {
468
509
  && task.conversationId === input.conversationId
469
510
  && task.conversationUrl === input.conversationUrl;
470
511
  if (bootstrapUpgrade) {
512
+ const existingWorkId = boundedText(task.completionContract.work_id);
513
+ const existingRequirementId = boundedText(task.completionContract.requirement_id);
514
+ const incomingWorkId = boundedText(input.completionContract.work_id);
515
+ const incomingRequirementId = boundedText(input.completionContract.requirement_id);
516
+ if ((existingWorkId && incomingWorkId !== existingWorkId)
517
+ || (existingRequirementId && incomingRequirementId !== existingRequirementId)) {
518
+ throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_CONFLICT');
519
+ }
471
520
  statement(db, `UPDATE tasks
472
521
  SET objective = ?, completion_contract_json = ?, continuation_policy_json = ?, user_blocker_policy_json = ?
473
522
  WHERE task_id = ?`, (s) => s.run(
@@ -525,32 +574,62 @@ export class WorkflowSupervisorStore {
525
574
  requestId: string;
526
575
  authorizedBy: string;
527
576
  refreshEffect?: { effectId: string; expectedPrompt: string; prompt: string };
528
- }): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; refreshedEffect?: WorkflowSupervisorEffect } {
577
+ semanticBinding?: { workId?: string; requirementId?: string; expectedCompletionContractSha256: string };
578
+ }): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; completionContractSha256: string; semanticBindingChanged: boolean; refreshedEffect?: WorkflowSupervisorEffect } {
529
579
  return this.transaction((db) => {
530
- const objective = input.objective.trim();
531
- if (!objective || objective.length > 8_000) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_INVALID');
580
+ const objective = validateSupervisorObjective(input.objective);
532
581
  if (!/^[0-9a-f]{64}$/i.test(input.expectedObjectiveSha256)) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_EXPECTED_SHA_INVALID');
533
582
  if (!input.requestId.trim()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_REQUIRED');
534
583
  const targetSha = objectiveSha256(objective);
584
+ const semanticBinding = input.semanticBinding;
585
+ const workId = boundedText(semanticBinding?.workId);
586
+ const requirementId = boundedText(semanticBinding?.requirementId);
587
+ if (semanticBinding) {
588
+ if (Boolean(workId) === Boolean(requirementId)) throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_TARGET_INVALID');
589
+ if (!/^[0-9a-f]{64}$/i.test(semanticBinding.expectedCompletionContractSha256)) {
590
+ throw new Error('WORKFLOW_SUPERVISOR_COMPLETION_CONTRACT_REVISION_EXPECTED_SHA_INVALID');
591
+ }
592
+ }
593
+ const semanticBindingKind = workId ? 'work' : requirementId ? 'requirement' : undefined;
594
+ const semanticBindingId = workId ?? requirementId;
535
595
  const eventKey = `task-objective-revise:${input.taskId}:${input.requestId}`;
596
+ const bindingEventKey = `task-semantic-binding-revise:${input.taskId}:${input.requestId}`;
536
597
  const existingEvent = statement(db, 'SELECT payload_json FROM events WHERE event_key = ?', (s) => s.get(eventKey)) as { payload_json?: string } | undefined;
598
+ const existingBindingEvent = statement(db, 'SELECT payload_json FROM events WHERE event_key = ?', (s) => s.get(bindingEventKey)) as { payload_json?: string } | undefined;
537
599
  if (existingEvent) {
538
600
  const payload = parsedObject(existingEvent.payload_json);
539
- if (boundedText(payload.objective_sha256) !== targetSha) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_CONFLICT');
601
+ if (boundedText(payload.objective_sha256) !== targetSha
602
+ || boundedText(payload.semantic_binding_id) !== semanticBindingId
603
+ || boundedText(payload.semantic_binding_kind) !== semanticBindingKind
604
+ || Boolean(existingBindingEvent) !== Boolean(semanticBinding)) {
605
+ throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_CONFLICT');
606
+ }
607
+ if (existingBindingEvent) {
608
+ const bindingPayload = parsedObject(existingBindingEvent.payload_json);
609
+ if (boundedText(bindingPayload.completion_contract_sha256) !== boundedText(payload.completion_contract_sha256)
610
+ || boundedText(bindingPayload.semantic_binding_id) !== semanticBindingId
611
+ || boundedText(bindingPayload.semantic_binding_kind) !== semanticBindingKind) {
612
+ throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_REVISION_REQUEST_CONFLICT');
613
+ }
614
+ }
540
615
  const taskRow = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
541
616
  if (!taskRow) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
617
+ const deduplicatedTask = taskFromRow(taskRow);
542
618
  const refreshedEffectId = boundedText(payload.refreshed_effect_id);
543
619
  const refreshedRow = refreshedEffectId
544
620
  ? statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(refreshedEffectId)) as Record<string, unknown> | undefined
545
621
  : undefined;
546
622
  return {
547
- task: taskFromRow(taskRow),
623
+ task: deduplicatedTask,
548
624
  changed: payload.changed === true,
549
625
  deduplicated: true,
550
626
  objectiveSha256: targetSha,
627
+ completionContractSha256: workflowSupervisorCompletionContractSha256(deduplicatedTask.completionContract),
628
+ semanticBindingChanged: payload.semantic_binding_changed === true,
551
629
  ...(refreshedRow ? { refreshedEffect: effectFromRow(refreshedRow) } : {}),
552
630
  };
553
631
  }
632
+ if (existingBindingEvent) throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_REVISION_REQUEST_CONFLICT');
554
633
  const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
555
634
  if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
556
635
  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;
@@ -559,6 +638,31 @@ export class WorkflowSupervisorStore {
559
638
  const currentSha = objectiveSha256(task.objective);
560
639
  if (currentSha !== input.expectedObjectiveSha256.toLowerCase()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_CONFLICT');
561
640
 
641
+ const currentContractSha = workflowSupervisorCompletionContractSha256(task.completionContract);
642
+ let nextCompletionContract = task.completionContract;
643
+ let completionContractSha = currentContractSha;
644
+ let semanticBindingChanged = false;
645
+ if (semanticBinding) {
646
+ if (task.completionContract.kind !== 'model_semantic_completion') {
647
+ throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_REVISION_KIND_UNSUPPORTED');
648
+ }
649
+ const currentWorkId = boundedText(task.completionContract.work_id);
650
+ const currentRequirementId = boundedText(task.completionContract.requirement_id);
651
+ if ((currentWorkId || currentRequirementId)
652
+ && (currentWorkId !== workId || currentRequirementId !== requirementId)) {
653
+ throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_CONFLICT');
654
+ }
655
+ if (currentContractSha !== semanticBinding.expectedCompletionContractSha256.toLowerCase()) {
656
+ throw new Error('WORKFLOW_SUPERVISOR_COMPLETION_CONTRACT_REVISION_CONFLICT');
657
+ }
658
+ nextCompletionContract = {
659
+ ...task.completionContract,
660
+ ...(workId ? { work_id: workId } : { requirement_id: requirementId }),
661
+ };
662
+ completionContractSha = workflowSupervisorCompletionContractSha256(nextCompletionContract);
663
+ semanticBindingChanged = completionContractSha !== currentContractSha;
664
+ }
665
+
562
666
  let refreshedEffect: WorkflowSupervisorEffect | undefined;
563
667
  if (input.refreshEffect) {
564
668
  const effectRow = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(input.refreshEffect!.effectId)) as Record<string, unknown> | undefined;
@@ -579,6 +683,26 @@ export class WorkflowSupervisorStore {
579
683
 
580
684
  const changed = task.objective !== objective;
581
685
  if (changed) statement(db, 'UPDATE tasks SET objective = ? WHERE task_id = ?', (s) => s.run(objective, input.taskId));
686
+ if (semanticBindingChanged) {
687
+ statement(db, 'UPDATE tasks SET completion_contract_json = ? WHERE task_id = ?', (s) => s.run(json(nextCompletionContract), input.taskId));
688
+ }
689
+ if (semanticBinding) {
690
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
691
+ input.taskId,
692
+ bindingEventKey,
693
+ 'task_semantic_binding_revised',
694
+ json({
695
+ request_id: input.requestId,
696
+ authorized_by: input.authorizedBy,
697
+ semantic_binding_kind: semanticBindingKind,
698
+ semantic_binding_id: semanticBindingId,
699
+ previous_completion_contract_sha256: currentContractSha,
700
+ completion_contract_sha256: completionContractSha,
701
+ changed: semanticBindingChanged,
702
+ }),
703
+ now(),
704
+ ));
705
+ }
582
706
  statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
583
707
  input.taskId,
584
708
  eventKey,
@@ -589,16 +713,120 @@ export class WorkflowSupervisorStore {
589
713
  previous_objective_sha256: currentSha,
590
714
  objective_sha256: targetSha,
591
715
  changed,
716
+ completion_contract_sha256: completionContractSha,
717
+ ...(semanticBinding ? {
718
+ semantic_binding_kind: semanticBindingKind,
719
+ semantic_binding_id: semanticBindingId,
720
+ semantic_binding_changed: semanticBindingChanged,
721
+ } : {}),
592
722
  ...(refreshedEffect ? { refreshed_effect_id: refreshedEffect.effectId } : {}),
593
723
  }),
594
724
  now(),
595
725
  ));
596
726
  const updatedTask = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
597
727
  if (!updatedTask) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
598
- return { task: taskFromRow(updatedTask), changed, deduplicated: false, objectiveSha256: targetSha, ...(refreshedEffect ? { refreshedEffect } : {}) };
728
+ return {
729
+ task: taskFromRow(updatedTask), changed, deduplicated: false, objectiveSha256: targetSha,
730
+ completionContractSha256: completionContractSha, semanticBindingChanged,
731
+ ...(refreshedEffect ? { refreshedEffect } : {}),
732
+ };
599
733
  });
600
734
  }
601
735
  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; }); }
736
+ bindScheduledWake(input: { taskId: string; repoId: string; workId: string; scheduleId: string }): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean } {
737
+ const scheduleId = boundedText(input.scheduleId);
738
+ if (!scheduleId || scheduleId.length > 512) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_ID_INVALID');
739
+ return this.transaction((db) => {
740
+ const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
741
+ if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
742
+ const task = taskFromRow(row);
743
+ 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))) {
744
+ throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
745
+ }
746
+ if (task.completionContract.repo_id !== input.repoId || task.completionContract.work_id !== input.workId) {
747
+ throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_WORK_IDENTITY_MISMATCH');
748
+ }
749
+ if (task.continuationPolicy.kind !== 'forge_goal_outer_turn') throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_OUTER_TURN_REQUIRED');
750
+ const existingScheduleId = boundedText(task.continuationPolicy.scheduled_wake_schedule_id);
751
+ if (existingScheduleId) {
752
+ if (existingScheduleId !== scheduleId) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_BINDING_CONFLICT');
753
+ return { task, changed: false, deduplicated: true };
754
+ }
755
+ const continuationPolicy = { ...task.continuationPolicy, scheduled_wake_schedule_id: scheduleId };
756
+ statement(db, 'UPDATE tasks SET continuation_policy_json = ? WHERE task_id = ?', (s) => s.run(json(continuationPolicy), input.taskId));
757
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
758
+ input.taskId, `scheduled-wake-bound:${input.taskId}:${scheduleId}`, 'scheduled_wake_bound',
759
+ json({ repo_id: input.repoId, work_id: input.workId, schedule_id: scheduleId, attestation: 'exact_work_schedule_binding' }), now()));
760
+ const updated = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
761
+ if (!updated) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
762
+ return { task: taskFromRow(updated), changed: true, deduplicated: false };
763
+ });
764
+ }
765
+ refreshUndispatchedEffectPrompt(input: { taskId: string; effectId: string; expectedPrompt: string; prompt: string }): boolean {
766
+ if (!input.prompt.trim()) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_PROMPT_REQUIRED');
767
+ return this.transaction((db) => {
768
+ const row = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(input.effectId)) as Record<string, unknown> | undefined;
769
+ if (!row || String(row.task_id) !== input.taskId) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_TASK_MISMATCH');
770
+ if (String(row.prompt_text) !== input.expectedPrompt) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_PROMPT_CHANGED');
771
+ const dispatched = statement(db, "SELECT 1 FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_started' LIMIT 1", (s) => s.get(input.effectId));
772
+ const applied = statement(db, "SELECT 1 FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(input.effectId));
773
+ if (dispatched || applied) return false;
774
+ statement(db, 'UPDATE effects SET prompt_text = ? WHERE effect_id = ? AND prompt_text = ?', (s) => s.run(input.prompt, input.effectId, input.expectedPrompt));
775
+ return true;
776
+ });
777
+ }
778
+ reserveScheduledWake(input: { taskId: string; scheduleId: string; occurrenceId: string; effectId: string; prompt: string }): { effect: WorkflowSupervisorEffect; deduplicated: boolean } {
779
+ const scheduleId = boundedText(input.scheduleId);
780
+ const occurrenceId = boundedText(input.occurrenceId);
781
+ if (!scheduleId || scheduleId.length > 512) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_ID_INVALID');
782
+ if (!occurrenceId || occurrenceId.length > 512) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_OCCURRENCE_ID_INVALID');
783
+ return this.transaction((db) => {
784
+ const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
785
+ if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
786
+ const task = taskFromRow(row);
787
+ if (task.continuationPolicy.kind !== 'forge_goal_outer_turn'
788
+ || task.continuationPolicy.scheduled_wake_schedule_id !== scheduleId) {
789
+ throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_BINDING_MISMATCH');
790
+ }
791
+ const originKey = `scheduled-wake:${scheduleId}:${occurrenceId}`;
792
+ const priorRow = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(originKey)) as Record<string, unknown> | undefined;
793
+ if (priorRow) {
794
+ const prior = effectFromRow(priorRow);
795
+ if (prior.taskId !== input.taskId || prior.effectId !== input.effectId || prior.kind !== 'continuation' || !prior.sourceCompletionFingerprint) {
796
+ throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_IDENTITY_CONFLICT');
797
+ }
798
+ return { effect: prior, deduplicated: true };
799
+ }
800
+ 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))) {
801
+ throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
802
+ }
803
+ if (oldestUnappliedEffect(db, input.taskId)) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_CAUSAL_OBLIGATION_PENDING');
804
+ const awaitingReceipt = statement(db, `SELECT 1 AS ok FROM effects e
805
+ WHERE e.task_id = ?
806
+ AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied')
807
+ AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.task_id = e.task_id AND c.source_effect_id = e.effect_id)
808
+ LIMIT 1`, (s) => s.get(input.taskId));
809
+ if (awaitingReceipt) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_CAUSAL_OBLIGATION_PENDING');
810
+ const latestRow = statement(db, "SELECT c.* FROM completions c WHERE c.task_id = ? ORDER BY COALESCE((SELECT MAX(completion_event.event_id) FROM events completion_event WHERE completion_event.task_id = c.task_id AND completion_event.completion_fingerprint = c.completion_fingerprint AND completion_event.kind = 'assistant_completion'), c.rowid) DESC LIMIT 1", (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
811
+ if (!latestRow) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_NOT_WAITING');
812
+ const completion = completionFromRow(latestRow);
813
+ if (completion.action !== 'WAIT') throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_NOT_WAITING');
814
+ const consumed = statement(db, 'SELECT * FROM effects WHERE task_id = ? AND source_completion_fingerprint = ?', (s) => s.get(input.taskId, completion.completionFingerprint)) as Record<string, unknown> | undefined;
815
+ if (consumed) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_ALREADY_CONSUMED');
816
+ const effect = this.reserveEffectWithin(db, {
817
+ taskId: input.taskId,
818
+ effectId: input.effectId,
819
+ kind: 'continuation',
820
+ originKey,
821
+ sourceCompletionFingerprint: completion.completionFingerprint,
822
+ prompt: input.prompt,
823
+ });
824
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,completion_fingerprint,payload_json,occurred_at) VALUES (?,?,?,?,?,?,?)', (s) => s.run(
825
+ input.taskId, `scheduled-wake-committed:${scheduleId}:${occurrenceId}`, 'scheduled_wake_reserved', effect.effectId,
826
+ completion.completionFingerprint, json({ schedule_id: scheduleId, occurrence_id: occurrenceId, source_completion_fingerprint: completion.completionFingerprint }), now()));
827
+ return { effect, deduplicated: false };
828
+ });
829
+ }
602
830
  bindBootstrapConversation(taskId: string, conversationId: string, conversationUrl: string): WorkflowSupervisorTask {
603
831
  return this.transaction((db) => {
604
832
  const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(taskId)) as Record<string, unknown> | undefined;
@@ -674,12 +902,12 @@ export class WorkflowSupervisorStore {
674
902
  WHERE c.action = 'CONTINUE'
675
903
  AND NOT EXISTS (SELECT 1 FROM effects successor WHERE successor.origin_key = 'completion:' || c.completion_fingerprint)
676
904
  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))
905
+ ORDER BY COALESCE((SELECT MAX(completion_event.event_id) FROM events completion_event WHERE completion_event.task_id = c.task_id AND completion_event.completion_fingerprint = c.completion_fingerprint AND completion_event.kind = 'assistant_completion'), c.rowid) DESC LIMIT ?`, (s) => s.all(boundedLimit))
678
906
  .map((row) => completionFromRow(row as Record<string, unknown>)));
679
907
  }
680
908
  getCompletionByResponseSha256(taskId: string, responseSha256: string): WorkflowSupervisorCompletion | undefined {
681
909
  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>[];
910
+ const rows = statement(db, 'SELECT * FROM completions WHERE task_id = ? AND response_sha256 = ? ORDER BY committed_at, rowid LIMIT 2', (s) => s.all(taskId, responseSha256)) as Record<string, unknown>[];
683
911
  if (rows.length > 1) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_COMPLETION_AMBIGUOUS');
684
912
  return rows[0] ? completionFromRow(rows[0]) : undefined;
685
913
  });
@@ -687,12 +915,13 @@ export class WorkflowSupervisorStore {
687
915
  /** The only replay candidate when no applied effect is awaiting a receipt. */
688
916
  getLatestCompletion(taskId: string): WorkflowSupervisorCompletion | undefined {
689
917
  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;
918
+ const row = statement(db, "SELECT c.* FROM completions c WHERE c.task_id = ? ORDER BY COALESCE((SELECT MAX(completion_event.event_id) FROM events completion_event WHERE completion_event.task_id = c.task_id AND completion_event.completion_fingerprint = c.completion_fingerprint AND completion_event.kind = 'assistant_completion'), c.rowid) DESC LIMIT 1", (s) => s.get(taskId)) as Record<string, unknown> | undefined;
691
919
  return row ? completionFromRow(row) : undefined;
692
920
  });
693
921
  }
694
922
  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');
923
+ const notBeforeMs = Date.parse(input.notBefore);
924
+ if (!Number.isFinite(notBeforeMs)) throw new Error('WORKFLOW_SUPERVISOR_PROOF_BOUNDARY_INVALID');
696
925
  const activeReleaseId = boundedText(input.activeReleaseId);
697
926
  if (!activeReleaseId) throw new Error('WORKFLOW_SUPERVISOR_PROOF_RELEASE_REQUIRED');
698
927
  return this.read((db) => {
@@ -701,46 +930,108 @@ export class WorkflowSupervisorStore {
701
930
  const task = taskFromRow(taskRow);
702
931
  const taskRepoId = boundedText(task.completionContract.repo_id) ?? boundedText(task.continuationPolicy.repo_id);
703
932
  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>[];
933
+ const completionRows = statement(db, "SELECT c.* FROM completions c WHERE c.task_id = ? AND c.committed_at >= ? ORDER BY COALESCE((SELECT MAX(completion_event.event_id) FROM events completion_event WHERE completion_event.task_id = c.task_id AND completion_event.completion_fingerprint = c.completion_fingerprint AND completion_event.kind = 'assistant_completion'), c.rowid) ASC", (s) => s.all(task.taskId, input.notBefore)) as Record<string, unknown>[];
705
934
  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) {
935
+ // The bounded release canary task must prove C -> C -> accepted D.
936
+ // Long-running production tasks may remain open; any disposable task can supply this proof.
937
+ for (let index = 0; index + 2 < completions.length; index += 1) {
938
+ if (index + 3 !== completions.length) continue;
709
939
  const first = completions[index]!;
710
940
  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);
941
+ const third = completions[index + 2]!;
942
+ if (first.action !== 'CONTINUE' || second.action !== 'CONTINUE' || third.action !== 'DONE') continue;
943
+ const rows = [first, second, third].map((completion) => statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(completion.sourceEffectId)) as Record<string, unknown> | undefined);
713
944
  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;
945
+ const [firstEffect, secondEffect, thirdEffect] = rows.map((row) => effectFromRow(row!)) as [WorkflowSupervisorEffect, WorkflowSupervisorEffect, WorkflowSupervisorEffect];
946
+ if (
947
+ [firstEffect, secondEffect, thirdEffect].some((effect) => effect.taskId !== task.taskId)
948
+ || secondEffect.sourceCompletionFingerprint !== first.completionFingerprint
949
+ || thirdEffect.sourceCompletionFingerprint !== second.completionFingerprint
950
+ ) continue;
716
951
  const runtimeInstanceIds: string[] = [];
717
952
  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);
953
+ let thirdCompletionEventId = 0;
954
+ for (const [completionIndex, completion, effect] of [
955
+ [0, first, firstEffect],
956
+ [1, second, secondEffect],
957
+ [2, third, thirdEffect],
958
+ ] as const) {
959
+ const dispatchRows = 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(effect.effectId)) as Array<{ event_id?: number; payload_json?: string; occurred_at?: string }>;
960
+ if (
961
+ dispatchRows.length !== 1
962
+ || !Number.isFinite(Date.parse(String(dispatchRows[0]?.occurred_at ?? '')))
963
+ || Date.parse(String(dispatchRows[0]?.occurred_at ?? '')) < notBeforeMs
964
+ ) { valid = false; break; }
965
+ const dispatch = dispatchRows[0]!;
966
+ const evidence = parsedObject(dispatch.payload_json);
722
967
  if (boundedText(evidence.active_release_id) !== activeReleaseId) { valid = false; break; }
723
968
  const runtimeInstanceId = boundedText(evidence.runtime_instance_id);
724
969
  if (!runtimeInstanceId) { valid = false; break; }
725
970
  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));
971
+ const appliedRows = statement(db, "SELECT event_id,occurred_at FROM events WHERE effect_id = ? AND kind = 'effect_applied' ORDER BY event_id", (s) => s.all(effect.effectId)) as Array<{ event_id?: number; occurred_at?: string }>;
972
+ const applied = appliedRows.find((row) => {
973
+ const occurredAtMs = Date.parse(String(row.occurred_at ?? ''));
974
+ return Number(row.event_id ?? 0) > Number(dispatch.event_id ?? 0)
975
+ && Number.isFinite(occurredAtMs)
976
+ && occurredAtMs >= notBeforeMs;
977
+ });
727
978
  if (!applied) { valid = false; break; }
979
+ const completionEvents = statement(db, "SELECT event_id,occurred_at FROM events WHERE task_id = ? AND completion_fingerprint = ? AND kind = 'assistant_completion' ORDER BY event_id", (s) => s.all(task.taskId, completion.completionFingerprint)) as Array<{ event_id?: number; occurred_at?: string }>;
980
+ if (
981
+ completionEvents.length !== 1
982
+ || !Number.isFinite(Date.parse(String(completionEvents[0]?.occurred_at ?? '')))
983
+ || Date.parse(String(completionEvents[0]?.occurred_at ?? '')) < notBeforeMs
984
+ || Number(completionEvents[0]?.event_id ?? 0) <= Number(applied.event_id ?? 0)
985
+ ) { valid = false; break; }
986
+ if (completionIndex === 2) thirdCompletionEventId = Number(completionEvents[0]?.event_id ?? 0);
728
987
  }
729
- const minimumRuntimeInstances = task.continuationPolicy.kind === 'standalone_supervisor' ? 1 : 2;
988
+ const minimumRuntimeInstances = 2;
730
989
  if (!valid || new Set(runtimeInstanceIds).size < minimumRuntimeInstances) continue;
990
+ const terminalRows = statement(db, "SELECT event_id,occurred_at FROM events WHERE task_id = ? AND completion_fingerprint = ? AND kind = 'terminal_done' ORDER BY event_id", (s) => s.all(task.taskId, third.completionFingerprint)) as Array<{ event_id?: number; occurred_at?: string }>;
991
+ const latestTerminal = statement(db, "SELECT kind,completion_fingerprint 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(task.taskId)) as { kind?: string; completion_fingerprint?: string } | undefined;
992
+ if (
993
+ terminalRows.length !== 1
994
+ || !Number.isFinite(Date.parse(String(terminalRows[0]?.occurred_at ?? '')))
995
+ || Date.parse(String(terminalRows[0]?.occurred_at ?? '')) < notBeforeMs
996
+ || Number(terminalRows[0]?.event_id ?? 0) <= thirdCompletionEventId
997
+ || latestTerminal?.kind !== 'terminal_done'
998
+ || latestTerminal.completion_fingerprint !== third.completionFingerprint
999
+ ) continue;
731
1000
  return {
732
1001
  taskId: task.taskId, conversationId: task.conversationId, activeReleaseId,
733
- actions: ['CONTINUE', 'CONTINUE'],
734
- completionFingerprints: [first.completionFingerprint, second.completionFingerprint],
735
- sourceEffectIds: [first.sourceEffectId, second.sourceEffectId],
1002
+ actions: ['CONTINUE', 'CONTINUE', 'DONE'],
1003
+ completionFingerprints: [first.completionFingerprint, second.completionFingerprint, third.completionFingerprint],
1004
+ sourceEffectIds: [first.sourceEffectId, second.sourceEffectId, third.sourceEffectId],
736
1005
  runtimeInstanceIds: [...new Set(runtimeInstanceIds)],
737
- firstCommittedAt: first.committedAt, lastCommittedAt: second.committedAt,
1006
+ firstCommittedAt: first.committedAt, lastCommittedAt: third.committedAt,
738
1007
  };
739
1008
  }
740
1009
  }
741
1010
  return undefined;
742
1011
  });
743
1012
  }
1013
+ /**
1014
+ * Exact persisted pre-Send editor evidence. ProseMirror may introduce a
1015
+ * mid-body line break; only the observed prior exact digest grants recovery.
1016
+ */
1017
+ preSendComposerObservation(effectId: string): WorkflowSupervisorPreflightComposerObservation | undefined {
1018
+ return this.read((db) => {
1019
+ const sql = "SELECT e.payload_json FROM events e JOIN effects fx ON fx.effect_id = e.effect_id "
1020
+ + "WHERE e.effect_id = ? AND e.kind = 'effect_preflight_rejected' "
1021
+ + "AND e.event_id < (SELECT MIN(t.event_id) FROM events t WHERE t.task_id = fx.task_id AND t.kind = 'terminal_stopped') "
1022
+ + "ORDER BY e.event_id DESC LIMIT 1";
1023
+ const row = statement(db, sql, (query) => query.get(effectId)) as { payload_json?: string } | undefined;
1024
+ const data = parsedObject(row?.payload_json);
1025
+ const editor = parsedObject(data.composer_observation);
1026
+ if (data.send_clicked !== false || data.attestation !== 'computer_prepare_returned_before_supervisor_send_intent'
1027
+ || editor.classification !== 'current_effect' || editor.selectorVersion !== 'visible-single-v2'
1028
+ || !Number.isInteger(editor.chars) || Number(editor.chars) < 1
1029
+ || typeof editor.fingerprint !== 'string' || !/^[a-f0-9]{24}$/.test(editor.fingerprint)) return undefined;
1030
+ return { chars: Number(editor.chars), fingerprint: editor.fingerprint,
1031
+ classification: 'current_effect', selectorVersion: 'visible-single-v2' };
1032
+ });
1033
+ }
1034
+
744
1035
  latestEffectDispatch(effectId: string): { eventId: number; generation: number; evidence: Record<string, unknown> } | undefined {
745
1036
  return this.read((db) => {
746
1037
  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;
@@ -781,11 +1072,7 @@ export class WorkflowSupervisorStore {
781
1072
  }
782
1073
  const retryAuthorized = latestNotAppliedProofEventId(db, effect.effectId) > ledger.lastEventId;
783
1074
  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
- }
1075
+ if (!unknownObservationDue(db, effect.effectId, ledger.lastEventId, nowMs)) return undefined;
789
1076
  return { effect, mode: 'reconcile', generation: ledger.lastGeneration };
790
1077
  }
791
1078
  if (ledger.generations >= WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + ledger.budgetRefunds) return undefined;
@@ -826,7 +1113,19 @@ export class WorkflowSupervisorStore {
826
1113
  const pending = this.currentUnappliedEffect(taskId);
827
1114
  if (pending) {
828
1115
  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' };
1116
+ if (ledger.preflightBlocked) {
1117
+ const failure = this.read((db) => statement(db,
1118
+ "SELECT payload_json FROM events WHERE effect_id = ? AND kind = 'effect_preflight_rejected' ORDER BY event_id DESC LIMIT 1",
1119
+ (s) => s.get(pending.effectId)) as { payload_json?: string } | undefined);
1120
+ const record = parsedObject(failure?.payload_json);
1121
+ const reasonCode = boundedText(record.reason)?.slice(0, 256);
1122
+ const diagnostic = record.composer_observation;
1123
+ const preflightComposerObservation = diagnostic && typeof diagnostic === 'object' && !Array.isArray(diagnostic)
1124
+ ? diagnostic as WorkflowSupervisorPreflightComposerObservation : undefined;
1125
+ return { state: 'spaced', effectId: pending.effectId, reason: 'pre_send_infrastructure_blocked',
1126
+ ...(reasonCode ? { preflightReasonCode: reasonCode } : {}),
1127
+ ...(preflightComposerObservation ? { preflightComposerObservation } : {}) };
1128
+ }
830
1129
  const ceiling = WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + ledger.budgetRefunds;
831
1130
  const retryAuthorized = ledger.generations === 0 || ledger.proof > ledger.lastEventId;
832
1131
  // An exhausted retry ceiling is the operator-facing state: no amount of
@@ -845,13 +1144,92 @@ export class WorkflowSupervisorStore {
845
1144
  if (this.hasAppliedEffectAwaitingCompletion(taskId)) {
846
1145
  return { state: 'spaced', ...(leaf ? { effectId: leaf.effectId } : {}), reason: 'awaiting_completion_receipt' };
847
1146
  }
1147
+ const waiting = this.read((db) => {
1148
+ const taskRow = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(taskId)) as Record<string, unknown> | undefined;
1149
+ if (!taskRow) return undefined;
1150
+ const task = taskFromRow(taskRow);
1151
+ const scheduleId = boundedText(task.continuationPolicy.scheduled_wake_schedule_id);
1152
+ if (!scheduleId) return undefined;
1153
+ const latest = statement(db, "SELECT c.completion_fingerprint, c.action FROM completions c WHERE c.task_id = ? ORDER BY COALESCE((SELECT MAX(completion_event.event_id) FROM events completion_event WHERE completion_event.task_id = c.task_id AND completion_event.completion_fingerprint = c.completion_fingerprint AND completion_event.kind = 'assistant_completion'), c.rowid) DESC LIMIT 1", (s) => s.get(taskId)) as { completion_fingerprint?: string; action?: string } | undefined;
1154
+ return latest?.action === 'WAIT' && latest.completion_fingerprint
1155
+ ? { state: 'waiting' as const, scheduleId, completionFingerprint: latest.completion_fingerprint }
1156
+ : undefined;
1157
+ });
1158
+ if (waiting) return waiting;
848
1159
  return { state: 'inert' };
849
1160
  }
850
1161
  /**
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.
1162
+ * Consecutive genuinely failed task-local attempts since the last durable
1163
+ * provider progress or conversation move. Counts across failure codes and
1164
+ * Runtime restarts; an unknown submission observation is NOT a failed send.
1165
+ * This is a projection of the existing Supervisor journal, not another
1166
+ * mutable retry or delivery authority.
1167
+ */
1168
+ automaticConversationFailureEvidence(taskId: string): { count: number; latestEventId: number; lastFailureAtMs: number } {
1169
+ return this.read((db) => {
1170
+ const row = statement(db, `SELECT COUNT(*) AS total, COALESCE(MAX(event_id),0) AS latest_id, MAX(occurred_at) AS last_at
1171
+ FROM events WHERE task_id = ? AND kind IN (
1172
+ 'effect_preflight_rejected', 'assistant_provider_failed',
1173
+ 'assistant_recovery_exhausted', 'browser_delivery_attempt_failed'
1174
+ ) AND event_id > COALESCE((
1175
+ SELECT MAX(event_id) FROM events WHERE task_id = ?
1176
+ AND kind IN ('effect_applied','assistant_completion','conversation_migrated')
1177
+ ),0)`, (s) => s.get(taskId, taskId)) as { total?: number; latest_id?: number; last_at?: string };
1178
+ return { count: Number(row?.total ?? 0), latestEventId: Number(row?.latest_id ?? 0),
1179
+ lastFailureAtMs: Date.parse(row?.last_at ?? '') || 0 };
1180
+ });
1181
+ }
1182
+
1183
+ /** Journal one attempted task-local Computer operation, never a guessed Send. */
1184
+ recordBrowserDeliveryAttemptFailure(input: { taskId: string; conversationId: string; code: string; effectId?: string }): void {
1185
+ this.transaction((db) => {
1186
+ const task = statement(db, 'SELECT conversation_id FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as { conversation_id?: string } | undefined;
1187
+ // An old tab or pre-migration provider result cannot charge the new chat.
1188
+ if (!task || task.conversation_id !== input.conversationId) return;
1189
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
1190
+ input.taskId, `browser-delivery-failed:${input.taskId}:${randomUUID()}`,
1191
+ 'browser_delivery_attempt_failed', input.effectId ?? null,
1192
+ json({ conversation_id: input.conversationId, code: input.code.slice(0, 128), phase: 'task_local_pre_mutation' }), now(),
1193
+ ));
1194
+ });
1195
+ }
1196
+
1197
+ /** One operator-approved repair of a proven pre-send failure, never a provider retry refund. */
1198
+ authorizeOperatorPreflightRetry(input: { effectId: string; requestId: string; reason: string; authorizedBy: string }): { deduplicated: boolean } {
1199
+ return this.transaction((db) => {
1200
+ const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(input.effectId)) as { task_id?: string } | undefined;
1201
+ if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
1202
+ if (statement(db, "SELECT 1 FROM events WHERE effect_id = ? AND kind IN ('effect_applied','effect_outcome_unknown_superseded') LIMIT 1", (s) => s.get(input.effectId))) {
1203
+ throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_EFFECT_TERMINAL');
1204
+ }
1205
+ if (effectDispatchLedger(db, input.effectId).generations !== 0) throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_RECOVERY_SEND_ALREADY_RECORDED');
1206
+ const key = `effect-preflight-operator-rearmed:${input.effectId}:${input.requestId}`;
1207
+ if (statement(db, 'SELECT 1 FROM events WHERE event_key = ?', (s) => s.get(key))) return { deduplicated: true };
1208
+ const failed = effectPreflightFailureReleaseIds(db, input.effectId);
1209
+ const releaseId = this.activeReleaseId()?.trim() || 'unidentified-runtime';
1210
+ // An operator may authorize ONE bounded no-Send re-observation after a
1211
+ // verified repair, including on a successor Runtime after the automatic
1212
+ // three-release cap. Never erase the prior failures or refund Send budget.
1213
+ if (statement(db, "SELECT 1 FROM events WHERE effect_id = ? AND kind = 'effect_preflight_operator_rearmed' LIMIT 1", (s) => s.get(input.effectId))) {
1214
+ throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_OPERATOR_REARM_EXHAUSTED');
1215
+ }
1216
+ if (!this.preflightBlocked(db, input.effectId)
1217
+ || (failed.length < WORKFLOW_SUPERVISOR_MAX_PREFLIGHT_RELEASE_ATTEMPTS
1218
+ && !failed.some((entry) => entry.releaseId === releaseId))) {
1219
+ throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_RECOVERY_NOT_BLOCKED');
1220
+ }
1221
+ statement(db, 'INSERT INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
1222
+ effect.task_id!, key, 'effect_preflight_operator_rearmed', input.effectId,
1223
+ json({ request_id: input.requestId, authorized_by: input.authorizedBy, reason: boundedText(input.reason)?.slice(0, 512), active_release_id: releaseId, attestation: 'operator_authorized_after_proven_no_send' }), now(),
1224
+ ));
1225
+ return { deduplicated: false };
1226
+ });
1227
+ }
1228
+
1229
+ /**
1230
+ * Explicit operator authority to continue a mechanically proven un-applied
1231
+ * send after its retry ceiling, without replaying an outcome-unknown mutation.
1232
+ * This refund path is distinct from zero-generation UI preparation repair.
855
1233
  */
856
1234
  authorizeOperatorRetry(input: { effectId: string; requestId: string; reason: string; authorizedBy: string }): { refunds: number; generations: number; deduplicated: boolean } {
857
1235
  return this.transaction((db) => {
@@ -946,6 +1324,12 @@ export class WorkflowSupervisorStore {
946
1324
  });
947
1325
  }
948
1326
  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)))); }
1327
+ bootstrapAppliedReconcileDue(effectId: string): boolean {
1328
+ return this.read((db) => {
1329
+ const dispatch = effectDispatchLedger(db, effectId);
1330
+ return dispatch.generations > 0 && unknownObservationDue(db, effectId, dispatch.lastEventId, this.clockMs());
1331
+ });
1332
+ }
949
1333
  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
1334
  latestAppliedEffectWithoutCompletion(taskId: string): WorkflowSupervisorEffect | undefined {
951
1335
  return this.read((db) => {
@@ -966,7 +1350,10 @@ export class WorkflowSupervisorStore {
966
1350
  WHERE e.task_id = ?
967
1351
  AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied')
968
1352
  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)
1353
+ AND NOT EXISTS (SELECT 1 FROM effects child
1354
+ WHERE child.origin_key = 'provider-recovery:' || e.effect_id
1355
+ AND EXISTS (SELECT 1 FROM events started
1356
+ WHERE started.effect_id = child.effect_id AND started.kind = 'effect_dispatch_started'))
970
1357
  ORDER BY (SELECT MAX(event_id) FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied') DESC
971
1358
  LIMIT 1`, (s) => s.get(taskId)) as Record<string, unknown> | undefined;
972
1359
  return row ? effectFromRow(row) : undefined;
@@ -1262,12 +1649,16 @@ export class WorkflowSupervisorStore {
1262
1649
  // Other observers can report a transport error while the native sender
1263
1650
  // is still preparing. Only that generation's dispatch owner can prove
1264
1651
  // 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;
1652
+ const unknown = 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;
1653
+ return unknown ? { ...unknown, dispatch_generation: dispatch.lastGeneration } : undefined;
1266
1654
  });
1267
1655
  const evidence = parsedObject(row?.payload_json);
1268
1656
  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
1657
  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' });
1658
+ this.recordEffectNotAppliedProof(effectId, `operator-pre-send:${requestId}`, {
1659
+ ...evidence, send_clicked: false, dispatch_generation: row?.dispatch_generation, pre_send_rejection: true,
1660
+ requested_by: 'user', request_id: requestId, attestation: 'persisted_native_pre_send_return',
1661
+ });
1271
1662
  return true;
1272
1663
  }
1273
1664
  private reserveEffectWithin(db: Database, input: { taskId: string; effectId: string; kind: WorkflowEffectKind; originKey: string; sourceCompletionFingerprint?: string; prompt: string }): WorkflowSupervisorEffect {
@@ -1285,8 +1676,8 @@ export class WorkflowSupervisorStore {
1285
1676
  return effect;
1286
1677
  }
1287
1678
 
1288
- /** Attest that no Send intent was reserved and the provider UI could not prepare. */
1289
- recordEffectPreparationFailure(effectId: string, reasonCode: string): void {
1679
+ /** Persist a bounded provider pre-send rejection; a started generation requires a durable exact no-click proof first. */
1680
+ recordEffectPreparationFailure(effectId: string, reasonCode: string, composerObservation?: WorkflowSupervisorPreflightComposerObservation): void {
1290
1681
  this.transaction((db) => {
1291
1682
  const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
1292
1683
  if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
@@ -1296,10 +1687,13 @@ export class WorkflowSupervisorStore {
1296
1687
  throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_FAILURE_OUTCOME_NOT_PROVEN');
1297
1688
  }
1298
1689
  const activeReleaseId = this.activeReleaseId()?.trim() || 'unidentified-runtime';
1299
- const key = `effect-preflight-rejected:${effectId}:${activeReleaseId}`;
1690
+ // After one explicit repair the next real preparation failure MUST be
1691
+ // recorded again; otherwise INSERT OR IGNORE could permit infinite retries.
1692
+ const rearm = statement(db, "SELECT COUNT(*) AS count FROM events WHERE effect_id = ? AND kind = 'effect_preflight_operator_rearmed'", (s) => s.get(effectId)) as { count?: number } | undefined;
1693
+ const key = `effect-preflight-rejected:${effectId}:${activeReleaseId}${Number(rearm?.count ?? 0) > 0 ? ':operator-rearmed' : ''}`;
1300
1694
  statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
1301
1695
  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(),
1696
+ 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', ...(composerObservation ? { composer_observation: composerObservation } : {}) }), now(),
1303
1697
  ));
1304
1698
  });
1305
1699
  }
@@ -1309,6 +1703,11 @@ export class WorkflowSupervisorStore {
1309
1703
  return this.transaction((db) => {
1310
1704
  const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
1311
1705
  if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
1706
+ // Completion and recovery Send compete in this same Store transaction.
1707
+ // A command obtained before a late completion cannot revive its recovery.
1708
+ if (statement(db, `SELECT 1 FROM effects e JOIN completions c
1709
+ ON c.task_id = e.task_id AND e.origin_key = 'provider-recovery:' || c.source_effect_id
1710
+ WHERE e.effect_id = ? AND e.kind = 'recovery' LIMIT 1`, (s) => s.get(effectId))) return false;
1312
1711
  const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId));
1313
1712
  if (applied) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_ALREADY_APPLIED');
1314
1713
  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 }>;
@@ -1353,12 +1752,23 @@ export class WorkflowSupervisorStore {
1353
1752
  const payload = json(proof);
1354
1753
  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
1754
  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()));
1755
+ if (existing) return; // An identical old receipt is idempotent, not fresh retry authority.
1357
1756
  const ledger = effectDispatchLedger(db, effectId);
1757
+ const proofGeneration = Number(proof.dispatch_generation);
1758
+ if (!Number.isInteger(proofGeneration) || proofGeneration < 1 || proofGeneration !== ledger.lastGeneration) {
1759
+ throw new Error('WORKFLOW_SUPERVISOR_NOT_APPLIED_PROOF_GENERATION_STALE');
1760
+ }
1761
+ 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()));
1762
+ const legacyRefund = statement(db, 'SELECT event_id FROM events WHERE event_key = ?', (s) => s.get(`effect-dispatch-budget-refund:${effectId}`)) as { event_id?: number } | undefined;
1763
+ const legacyRefundCoversGeneration = proofGeneration === WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS && Boolean(legacyRefund);
1358
1764
  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()));
1765
+ && Number.isInteger(proofGeneration) && proofGeneration > 0
1766
+ && proofGeneration === ledger.lastGeneration
1767
+ && !legacyRefundCoversGeneration) {
1768
+ statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
1769
+ effect.task_id, `effect-dispatch-budget-refund:${effectId}:${proofGeneration}`, 'effect_dispatch_budget_refunded', effectId,
1770
+ json({ reason: 'pre_send_rejection_did_not_reach_provider', dispatch_generation: proofGeneration }), now(),
1771
+ ));
1362
1772
  }
1363
1773
  });
1364
1774
  }
@@ -1376,6 +1786,15 @@ export class WorkflowSupervisorStore {
1376
1786
  throw new Error('WORKFLOW_SUPERVISOR_SOURCE_EFFECT_COMPLETION_CONFLICT');
1377
1787
  }
1378
1788
  const existing = statement(db, 'SELECT completion_fingerprint FROM completions WHERE completion_fingerprint = ?', (s) => s.get(input.completionFingerprint));
1789
+ // Once the recovery has a Send intent, its outcome may be unknown. A
1790
+ // late source reply must not create a parallel successor or rotate away
1791
+ // from that obligation. Exact historical completion replay stays inert.
1792
+ if (!existing && statement(db, `SELECT 1 FROM effects child JOIN events started
1793
+ ON started.effect_id = child.effect_id AND started.kind = 'effect_dispatch_started'
1794
+ WHERE child.task_id = ? AND child.origin_key = ? LIMIT 1`,
1795
+ (s) => s.get(input.taskId, 'provider-recovery:' + input.sourceEffectId))) {
1796
+ throw new Error('WORKFLOW_SUPERVISOR_SOURCE_EFFECT_RECOVERY_DISPATCHED');
1797
+ }
1379
1798
  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
1799
  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
1800
  let successorEffect: WorkflowSupervisorEffect | undefined;