@moretea-labs/forge 1.8.0 → 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 (695) hide show
  1. package/AGENTS.md +73 -7
  2. package/CHANGELOG.md +34 -0
  3. package/CLAUDE.md +17 -2
  4. package/CONTRIBUTING.md +3 -2
  5. package/README.en.md +24 -3
  6. package/README.md +10 -6
  7. package/README.zh-CN.md +5 -3
  8. package/THIRD_PARTY_NOTICES.md +1 -2
  9. package/adapters/chatgpt/browser-delivery-host.ts +7 -1
  10. package/adapters/chatgpt/browser-delivery-runtime.ts +531 -56
  11. package/adapters/chatgpt/controller-binding-store.ts +101 -22
  12. package/adapters/chatgpt/controller-round-host.ts +15 -25
  13. package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
  14. package/adapters/chatgpt/provider-delivery.ts +229 -2
  15. package/adapters/chatgpt/work-conversation-binding-store.ts +99 -54
  16. package/adapters/computer/chatgpt-extension-target.ts +708 -0
  17. package/adapters/computer/chrome-extension/background.js +541 -0
  18. package/adapters/computer/chrome-extension/content.js +402 -0
  19. package/adapters/computer/chrome-extension/core.js +24 -0
  20. package/adapters/computer/chrome-extension/manifest.json +32 -0
  21. package/adapters/computer/desktop-operator-discovery.ts +11 -31
  22. package/adapters/computer/desktop-operator-provider.ts +18 -6
  23. package/adapters/computer/interaction-target-authority.ts +178 -24
  24. package/adapters/mcp/access-mode.ts +18 -39
  25. package/adapters/mcp/auth.ts +2 -7
  26. package/adapters/mcp/instructions.ts +6 -5
  27. package/adapters/mcp/multi-repository.ts +10 -47
  28. package/adapters/mcp/oauth.ts +73 -0
  29. package/adapters/mcp/paths.ts +4 -1
  30. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  31. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  32. package/adapters/mcp/runtime-gateway/context-adapter.ts +490 -113
  33. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +83 -0
  34. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
  35. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  36. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
  37. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  38. package/adapters/mcp/runtime-gateway/execution-tools.ts +14 -54
  39. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
  40. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +132 -47
  41. package/adapters/mcp/runtime-gateway/process-tools.ts +173 -23
  42. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
  43. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
  44. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
  45. package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
  46. package/adapters/mcp/runtime-gateway/router.ts +19 -507
  47. package/adapters/mcp/runtime-gateway/routing-policy.ts +506 -0
  48. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +15 -66
  49. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  50. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +123 -178
  51. package/adapters/mcp/runtime-gateway/runtime-tools.ts +11 -5
  52. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +37 -1
  53. package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
  54. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +416 -270
  55. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +140 -0
  56. package/adapters/mcp/runtime-gateway/work-adapter.ts +262 -3123
  57. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  58. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  59. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +229 -0
  60. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  61. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  62. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  63. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  64. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  65. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +320 -0
  66. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  67. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +180 -0
  68. package/adapters/mcp/server.ts +91 -26
  69. package/adapters/mcp/setup.ts +5 -13
  70. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  71. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +703 -3092
  72. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +5 -10
  73. package/adapters/mcp/tool-mapping/repository-tools.ts +170 -213
  74. package/adapters/mcp/toolset-names.ts +11 -37
  75. package/adapters/mcp/toolset.ts +19 -92
  76. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  77. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  78. package/adapters/mcp/transports/http-observation.ts +377 -0
  79. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  80. package/adapters/mcp/transports/http.ts +200 -683
  81. package/adapters/mcp/transports/oauth-http.ts +322 -0
  82. package/adapters/mcp/transports/session-registry.ts +6 -0
  83. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
  84. package/adapters/mcp/types.ts +0 -6
  85. package/assets/hooks/lib/session-state.sh +0 -38
  86. package/assets/hooks/post-bash.sh +1 -1
  87. package/assets/hooks/post-tool-observer.sh +0 -5
  88. package/assets/hooks/prompt-guard.sh +0 -55
  89. package/assets/partials/04-project-structure.partial.md +1 -1
  90. package/assets/partials/07-footer.partial.md +1 -1
  91. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  92. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  93. package/assets/plugin-registry.v1.json +6 -4
  94. package/assets/reference-configs/agentic-development-flow.md +26 -1
  95. package/assets/reference-configs/external-tooling.md +0 -76
  96. package/assets/reference-configs/global-working-rules.md +19 -0
  97. package/assets/reference-configs/release-deploy.md +1 -1
  98. package/assets/templates/helpers/architecture-queue.sh +1 -1
  99. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  100. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  101. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  102. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  103. package/assets/workflow-contract.v1.json +0 -2
  104. package/docs/README.md +5 -2
  105. package/docs/architecture/CURRENT.md +205 -51
  106. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  107. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  108. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  109. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
  110. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
  111. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  112. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  113. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +234 -0
  114. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  115. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +74 -0
  116. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +342 -0
  117. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  118. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  119. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  120. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  121. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  122. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  123. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  124. package/docs/architecture/domains/public-surface.md +3 -5
  125. package/docs/architecture/domains/verification.md +6 -5
  126. package/docs/architecture/domains/workflow-engine.md +6 -0
  127. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +3 -3
  128. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  129. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  130. package/docs/forge-plugin-management.md +16 -4
  131. package/docs/operations/controller-browser-plugin.md +23 -0
  132. package/docs/operations/controller-performance-and-502.md +17 -0
  133. package/docs/operations/controller-reliability-runbook.md +2 -2
  134. package/docs/operations/features.md +6 -7
  135. package/docs/operations/features.zh-CN.md +1 -1
  136. package/docs/operations/github-repository.md +4 -4
  137. package/docs/operations/github-semantic-release-projection.md +20 -0
  138. package/docs/operations/homebrew.md +2 -2
  139. package/docs/operations/mcp-tool-exposure.md +13 -52
  140. package/docs/operations/releasing.md +2 -0
  141. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  142. package/docs/operations/standalone-disaster-recovery.md +15 -7
  143. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  144. package/docs/tutorials/03-first-repository-task.md +2 -0
  145. package/docs/wiki/Computer-and-Plugins.md +78 -0
  146. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  147. package/docs/wiki/Home.md +16 -2
  148. package/docs/wiki/Implementation.md +25 -13
  149. package/docs/wiki/Installation.md +28 -32
  150. package/docs/wiki/Integrations.md +4 -0
  151. package/docs/wiki/Quick-Start.md +35 -22
  152. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  153. package/docs/wiki/_Sidebar.md +5 -1
  154. package/package.json +25 -25
  155. package/packages/kernel/cognition/api/index.ts +6 -0
  156. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  157. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  158. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  159. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  160. package/packages/kernel/cognition/domain/learning.ts +106 -0
  161. package/packages/kernel/cognition/domain/memory.ts +254 -0
  162. package/packages/kernel/controller/api/index.ts +1 -0
  163. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  164. package/packages/kernel/controller/application/controller-round-service.ts +12 -0
  165. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
  166. package/packages/kernel/controller/domain/controller-round.ts +4 -1
  167. package/packages/kernel/controller/domain/execution-quality.ts +3 -1
  168. package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
  169. package/packages/kernel/controller/infrastructure/controller-session-store.ts +348 -97
  170. package/packages/kernel/controller/ports/controller-host.ts +2 -0
  171. package/packages/kernel/identity/api/index.ts +10 -0
  172. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  173. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  174. package/packages/kernel/identity/domain/grant.ts +72 -0
  175. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  176. package/packages/kernel/identity/domain/user-request.ts +93 -0
  177. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +87 -0
  178. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  179. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  180. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  181. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  182. package/packages/kernel/scheduler/api/index.ts +0 -2
  183. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  184. package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
  185. package/packages/kernel/scheduler/application/settlement.ts +0 -15
  186. package/packages/kernel/scheduler/domain/schedule.ts +87 -0
  187. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +166 -143
  188. package/packages/kernel/work/api/index.ts +3 -2
  189. package/packages/kernel/work/application/work-service.ts +18 -42
  190. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  191. package/packages/kernel/work/domain/engineering-design.ts +10 -5
  192. package/packages/kernel/work/domain/engineering-profile.ts +8 -12
  193. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  194. package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
  195. package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
  196. package/packages/kernel/work/domain/implementation-review.ts +14 -331
  197. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  198. package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
  199. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  200. package/packages/kernel/work/domain/state-machine.ts +34 -65
  201. package/packages/kernel/work/domain/types.ts +57 -51
  202. package/packages/kernel/work/infrastructure/work-contract-store.ts +847 -729
  203. package/packages/kernel/work/ports/work-contract-store.ts +5 -2
  204. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  205. package/packages/plugin-runtime/browser/session-authority.ts +2 -1
  206. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  207. package/packages/plugin-runtime/computer/conversation-target.ts +153 -0
  208. package/packages/plugin-runtime/computer/index.ts +2 -0
  209. package/packages/plugin-runtime/computer/target-authority.ts +26 -1
  210. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
  211. package/packages/protocols/computer/contract.ts +27 -1
  212. package/scripts/activate-source-baseline.ts +7 -9
  213. package/scripts/assemble-template.ts +11 -72
  214. package/scripts/canary-controller-semantic-wait.ts +12 -24
  215. package/scripts/check-mcp-compatibility.ts +94 -201
  216. package/scripts/check-package-identity.mjs +0 -5
  217. package/scripts/check-runtime-architecture.mjs +921 -239
  218. package/scripts/forge-local-recovery-helper.mjs +93 -15
  219. package/scripts/init-project.sh +0 -46
  220. package/scripts/install-ios-hid-toolchain.sh +60 -0
  221. package/scripts/install-standalone-recovery.ts +1 -1
  222. package/scripts/lib/project-init-lib.sh +6 -3
  223. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  224. package/scripts/open-source-audit-allowlist.txt +1 -3
  225. package/scripts/run-bounded-command.mjs +5 -0
  226. package/scripts/run-governed-gate.ts +2 -1
  227. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  228. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  229. package/scripts/smoke-ios-physical-device.ts +38 -10
  230. package/scripts/smoke-mcp-http-runtime.ts +12 -8
  231. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  232. package/scripts/smoke-runtime-recovery.ts +12 -43
  233. package/scripts/smoke-schedule-engine.ts +23 -15
  234. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  235. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  236. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +95 -0
  237. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  238. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  239. package/scripts/smoke-workflow-supervisor.ts +103 -0
  240. package/scripts/sync-generated-authority.mjs +0 -1
  241. package/scripts/verify-forge-runtime.sh +2 -1
  242. package/src/cli/agent-jobs/executor-health.ts +12 -35
  243. package/src/cli/agent-jobs/integration.ts +20 -51
  244. package/src/cli/agent-jobs/job-manager.ts +7 -3
  245. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  246. package/src/cli/commands/brain-assistant.ts +27 -13
  247. package/src/cli/commands/brain.ts +13 -15
  248. package/src/cli/commands/chatgpt.ts +1 -163
  249. package/src/cli/commands/doctor.ts +1 -143
  250. package/src/cli/commands/global-runtime.ts +3 -196
  251. package/src/cli/commands/init-hook.ts +6 -23
  252. package/src/cli/commands/init.ts +3 -252
  253. package/src/cli/commands/mcp.ts +0 -5
  254. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  255. package/src/cli/commands/recovery.ts +137 -135
  256. package/src/cli/commands/repository.ts +17 -1
  257. package/src/cli/commands/runtime.ts +3 -3
  258. package/src/cli/commands/setup-execution.ts +41 -0
  259. package/src/cli/commands/setup-profile.ts +8 -0
  260. package/src/cli/controller/check-runner.ts +13 -2
  261. package/src/cli/controller/completion-backlog.ts +3 -3
  262. package/src/cli/controller/completion-orchestrator.ts +5 -44
  263. package/src/cli/controller/completion-target.ts +3 -72
  264. package/src/cli/controller/composite-operations.ts +1 -1
  265. package/src/cli/controller/context/instruction-context.ts +1 -1
  266. package/src/cli/controller/context/known-paths.ts +2 -2
  267. package/src/cli/controller/context/query-planning.ts +11 -4
  268. package/src/cli/controller/context/rule-resolution.ts +2 -2
  269. package/src/cli/controller/context/source-materializer.ts +83 -102
  270. package/src/cli/controller/context/structural-provider.ts +84 -0
  271. package/src/cli/controller/context/structural.ts +6 -6
  272. package/src/cli/controller/context/types.ts +8 -9
  273. package/src/cli/controller/context-pack.ts +107 -84
  274. package/src/cli/controller/execution-completion.ts +4 -7
  275. package/src/cli/controller/execution-policy.ts +36 -153
  276. package/src/cli/controller/governance.ts +2 -3
  277. package/src/cli/controller/issue-store.ts +4 -99
  278. package/src/cli/controller/launch-agents.ts +59 -18
  279. package/src/cli/controller/operational-plan.ts +25 -52
  280. package/src/cli/controller/runtime-config.ts +5 -1
  281. package/src/cli/controller/stuck-state-migration.ts +2 -3
  282. package/src/cli/controller/systemd-user.ts +42 -10
  283. package/src/cli/editing/edit-session.ts +18 -12
  284. package/src/cli/github/github.ts +4 -4
  285. package/src/cli/github/semantic-projection.ts +174 -0
  286. package/src/cli/hook/prompt-intents.ts +0 -9
  287. package/src/cli/index.ts +1 -43
  288. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  289. package/src/cli/local-bridge/console-automations.ts +19 -14
  290. package/src/cli/local-bridge/console-view-models.ts +4 -13
  291. package/src/cli/local-bridge/facade-api.ts +64 -193
  292. package/src/cli/local-bridge/server.ts +24 -147
  293. package/src/cli/local-bridge/types.ts +0 -1
  294. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  295. package/src/cli/repositories/command-classifier.ts +0 -9
  296. package/src/cli/repositories/command-executor.ts +29 -23
  297. package/src/cli/repositories/command-scope.ts +37 -18
  298. package/src/cli/repositories/controller-home.ts +48 -0
  299. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  300. package/src/cli/repositories/git-command-executor.ts +2 -2
  301. package/src/cli/repositories/locks.ts +20 -0
  302. package/src/cli/repositories/registry.ts +79 -12
  303. package/src/cli/repositories/safe-patch.ts +2 -12
  304. package/src/cli/repositories/structured-git.ts +101 -5
  305. package/src/cli/repository/inspector.ts +185 -343
  306. package/src/cli/repository/session-cache.ts +92 -6
  307. package/src/effects/process-runner.ts +17 -1
  308. package/src/runtime/context/assistant-context.ts +20 -7
  309. package/src/runtime/context/assistant-work-context.ts +89 -27
  310. package/src/runtime/context/automatic-learning.ts +565 -0
  311. package/src/runtime/context/cognitive-attention.ts +173 -0
  312. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  313. package/src/runtime/context/context-closure.ts +46 -9
  314. package/src/runtime/context/context-pack-process.ts +103 -0
  315. package/src/runtime/context/context-plane.ts +1 -9
  316. package/src/runtime/context/direct-controller-learning.ts +298 -0
  317. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  318. package/src/runtime/context/semantic-navigation-contract.ts +16 -0
  319. package/src/runtime/context/semantic-navigation.ts +7 -35
  320. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  321. package/src/runtime/context/typescript-navigation.ts +89 -3
  322. package/src/runtime/control-plane/cleanup-artifact-retention.ts +215 -12
  323. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
  324. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
  325. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
  326. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  327. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  328. package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
  329. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  330. package/src/runtime/control-plane/execution/validation.ts +6 -8
  331. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
  332. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  333. package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
  334. package/src/runtime/control-plane/execution/work-delivery-service.ts +616 -0
  335. package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
  336. package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
  337. package/src/runtime/control-plane/execution/work-handle-authority.ts +197 -110
  338. package/src/runtime/control-plane/execution/work-handle-store.ts +72 -44
  339. package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
  340. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
  341. package/src/runtime/control-plane/execution/work-operation-service.ts +60 -34
  342. package/src/runtime/control-plane/execution/work-preparation-service.ts +231 -267
  343. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  344. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  345. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  346. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +201 -199
  347. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +33 -85
  348. package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
  349. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +6 -2
  350. package/src/runtime/control-plane/facade/capability-registry.ts +364 -37
  351. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  352. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  353. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  354. package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
  355. package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
  356. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
  357. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
  358. package/src/runtime/control-plane/facade/index.ts +0 -1
  359. package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
  360. package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
  361. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  362. package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
  363. package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
  364. package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
  365. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
  366. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  367. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
  368. package/src/runtime/control-plane/facade/types.ts +49 -108
  369. package/src/runtime/control-plane/facade/work-continuation.ts +9 -9
  370. package/src/runtime/control-plane/generated-cache-retention.ts +206 -59
  371. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  372. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
  373. package/src/runtime/control-plane/global-scheduler/maintenance.ts +256 -117
  374. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -12
  375. package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
  376. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  377. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  378. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  379. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  380. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
  381. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +29 -300
  382. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  383. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
  384. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
  385. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  386. package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
  387. package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
  388. package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
  389. package/src/runtime/control-plane/release-retention.ts +135 -47
  390. package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
  391. package/src/runtime/control-plane/runtime-cleanup.ts +295 -107
  392. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  393. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
  394. package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
  395. package/src/runtime/diagnostics/incident-repair.ts +313 -146
  396. package/src/runtime/diagnostics/mcp-timing.ts +114 -0
  397. package/src/runtime/evidence/artifact-store.ts +158 -40
  398. package/src/runtime/evidence/evidence-store.ts +112 -8
  399. package/src/runtime/evidence/output-handle-index.ts +225 -0
  400. package/src/runtime/evidence/result-store.ts +158 -44
  401. package/src/runtime/evidence/sensitive-output.ts +22 -2
  402. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  403. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  404. package/src/runtime/execution/jobs/store.ts +3 -4
  405. package/src/runtime/execution/managed-workspace.ts +57 -0
  406. package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
  407. package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
  408. package/src/runtime/execution/process-runtime/check-result.ts +105 -1
  409. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
  410. package/src/runtime/execution/process-runtime/command-facade.ts +82 -30
  411. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  412. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  413. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  414. package/src/runtime/execution/process-runtime/index.ts +3 -0
  415. package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
  416. package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
  417. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  418. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  419. package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
  420. package/src/runtime/execution/process-runtime/runtime.ts +55 -18
  421. package/src/runtime/execution/process-runtime/store.ts +30 -2
  422. package/src/runtime/execution/process-runtime/types.ts +6 -1
  423. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  424. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  425. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  426. package/src/runtime/execution/workers/executor.ts +3 -8
  427. package/src/runtime/maintenance/cleanup.ts +41 -5
  428. package/src/runtime/platform/service-inventory.ts +130 -0
  429. package/src/runtime/platform/service-manager.ts +8 -0
  430. package/src/runtime/plugins/action-application.ts +97 -29
  431. package/src/runtime/plugins/app-store-connect-adapter.ts +290 -18
  432. package/src/runtime/plugins/browser-adapter.ts +1124 -333
  433. package/src/runtime/plugins/browser-automation-service.ts +23 -3
  434. package/src/runtime/plugins/browser-macos-bridge.ts +431 -137
  435. package/src/runtime/plugins/browser-manifest-surface.ts +47 -12
  436. package/src/runtime/plugins/browser-native-session-service.ts +63 -13
  437. package/src/runtime/plugins/browser-registration.ts +18 -0
  438. package/src/runtime/plugins/browser-runtime.ts +40 -36
  439. package/src/runtime/plugins/browser-session-authority.ts +2 -2
  440. package/src/runtime/plugins/browser-session-store.ts +46 -76
  441. package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
  442. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +228 -0
  443. package/src/runtime/plugins/computer-registration.ts +298 -20
  444. package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
  445. package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
  446. package/src/runtime/plugins/external-adapter.ts +42 -6
  447. package/src/runtime/plugins/external-registration.ts +21 -1
  448. package/src/runtime/plugins/github-adapter.ts +30 -3
  449. package/src/runtime/plugins/gmail-adapter.ts +189 -3
  450. package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
  451. package/src/runtime/plugins/google-shared.ts +24 -12
  452. package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
  453. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  454. package/src/runtime/plugins/ios-adapter.ts +46 -41
  455. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  456. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  457. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  458. package/src/runtime/plugins/lightweight-action.ts +208 -2
  459. package/src/runtime/plugins/local-system-adapter.ts +12 -100
  460. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  461. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  462. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  463. package/src/runtime/plugins/store.ts +682 -435
  464. package/src/runtime/plugins/types.ts +24 -0
  465. package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
  466. package/src/runtime/projections/git-status-sampler.ts +2 -19
  467. package/src/runtime/projections/invalidation.ts +7 -3
  468. package/src/runtime/projections/materialized-view.ts +80 -36
  469. package/src/runtime/recovery/actions.ts +4 -4
  470. package/src/runtime/recovery/capability-action-application.ts +46 -45
  471. package/src/runtime/recovery/lifecycle-inventory.ts +30 -40
  472. package/src/runtime/recovery/maintenance-executor.ts +374 -123
  473. package/src/runtime/recovery/probe.ts +2 -2
  474. package/src/runtime/recovery/store.ts +29 -11
  475. package/src/runtime/release/release-coordinator.ts +312 -0
  476. package/src/runtime/release/release-session.ts +716 -0
  477. package/src/runtime/release/release-state-migration.ts +25 -0
  478. package/src/runtime/resources/leases/store.ts +56 -25
  479. package/src/runtime/root/computer-composition.ts +191 -6
  480. package/src/runtime/root/controller-progression-composition.ts +254 -0
  481. package/src/runtime/root/controller-round-composition.ts +20 -1
  482. package/src/runtime/root/deployment-topology.ts +106 -0
  483. package/src/runtime/root/entry.ts +4 -3
  484. package/src/runtime/root/known-good-recovery.ts +184 -0
  485. package/src/runtime/root/local-bridge.ts +0 -1
  486. package/src/runtime/root/mcp-transport.ts +111 -18
  487. package/src/runtime/root/ownership.ts +261 -9
  488. package/src/runtime/root/package-connector-service.ts +227 -29
  489. package/src/runtime/root/package-runtime-release.ts +117 -30
  490. package/src/runtime/root/package-runtime-service.ts +24 -24
  491. package/src/runtime/root/release-execution-canary.ts +57 -12
  492. package/src/runtime/root/release-loader.ts +90 -8
  493. package/src/runtime/root/release-manifest.ts +80 -40
  494. package/src/runtime/root/release-materialize.ts +372 -106
  495. package/src/runtime/root/release-store.ts +395 -36
  496. package/src/runtime/root/runtime-lane.ts +191 -0
  497. package/src/runtime/root/runtime.ts +160 -29
  498. package/src/runtime/root/scheduled-controller-composition.ts +29 -3
  499. package/src/runtime/root/service-runner.ts +2 -2
  500. package/src/runtime/root/service.ts +126 -44
  501. package/src/runtime/root/status.ts +48 -0
  502. package/src/runtime/root/types.ts +32 -9
  503. package/src/runtime/root/workflow-supervisor-composition.ts +681 -0
  504. package/src/runtime/root/workflow-supervisor-runtime.ts +564 -0
  505. package/src/runtime/root/write-fence.ts +50 -2
  506. package/src/runtime/safe-tooling/google-credential-store.ts +67 -22
  507. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  508. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  509. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  510. package/src/runtime/safe-tooling/workspace-auth.ts +5 -2
  511. package/src/runtime/shared/atomic-text-file.ts +21 -0
  512. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  513. package/src/runtime/shared/json-files.ts +8 -3
  514. package/src/runtime/shared/process-environment.ts +20 -9
  515. package/src/runtime/shared/response-budget.ts +1 -3
  516. package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
  517. package/src/runtime/standalone-recovery/core.ts +3792 -417
  518. package/src/runtime/standalone-recovery/entry.ts +507 -45
  519. package/src/runtime/standalone-recovery/installer.ts +262 -148
  520. package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
  521. package/src/runtime/standalone-recovery/operation-lock.ts +12 -2
  522. package/src/runtime/standalone-recovery/performance.ts +54 -16
  523. package/src/runtime/standalone-recovery/release.ts +53 -5
  524. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  525. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  526. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  527. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +11 -3
  528. package/src/runtime/workflow/schedules/engine.ts +166 -178
  529. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  530. package/src/runtime/workflow/schedules/work-continuation.ts +95 -11
  531. package/src/runtime/workflows/runtime.ts +15 -19
  532. package/src/testing/test-governance.ts +5 -1
  533. package/supervisor/chatgpt-conversation.ts +23 -0
  534. package/supervisor/client.ts +218 -0
  535. package/supervisor/control-plane.ts +963 -0
  536. package/supervisor/entry.ts +15 -0
  537. package/supervisor/forge-validators.ts +147 -0
  538. package/supervisor/index.ts +5 -0
  539. package/supervisor/native-browser-adapter.ts +1151 -0
  540. package/supervisor/native-messaging/host.ts +58 -0
  541. package/supervisor/native-messaging/manifest.ts +26 -0
  542. package/supervisor/native-messaging/protocol.ts +27 -0
  543. package/supervisor/paths.ts +18 -0
  544. package/supervisor/protocol.ts +250 -0
  545. package/supervisor/server.ts +412 -0
  546. package/supervisor/store.ts +1870 -0
  547. package/supervisor/tsconfig.json +8 -0
  548. package/supervisor/types.ts +261 -0
  549. package/SKILL.md +0 -427
  550. package/adapters/chatgpt/controller-host.ts +0 -119
  551. package/adapters/mcp/controller-round-compatibility.ts +0 -184
  552. package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
  553. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
  554. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  555. package/adapters/mcp/toolset-selection.ts +0 -41
  556. package/agents/openai.yaml +0 -4
  557. package/assets/skill-hooks.json +0 -41
  558. package/assets/skill-version.json +0 -189
  559. package/assets/skills/claude-review/SKILL.md +0 -231
  560. package/assets/skills/codex-review/SKILL.md +0 -103
  561. package/assets/templates/helpers/check-skill-version.ts +0 -193
  562. package/packages/kernel/progression/api/index.ts +0 -2
  563. package/packages/kernel/progression/application/projector.ts +0 -211
  564. package/packages/kernel/progression/domain/types.ts +0 -116
  565. package/packages/kernel/progression/index.ts +0 -1
  566. package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
  567. package/packages/kernel/scheduler/domain/continuation.ts +0 -38
  568. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
  569. package/packages/kernel/work/domain/admission-policy.ts +0 -34
  570. package/scripts/benchmark-background-check-overlap.ts +0 -228
  571. package/scripts/benchmark-computer-hotpath.ts +0 -255
  572. package/scripts/benchmark-controller-hotpath.ts +0 -212
  573. package/scripts/benchmark-controller-round2.ts +0 -191
  574. package/scripts/benchmark-ios-agent-device.ts +0 -511
  575. package/scripts/benchmark-mcp-host.sh +0 -117
  576. package/scripts/benchmark-mcp-transport.ts +0 -169
  577. package/scripts/benchmark-operational-memory-activation.ts +0 -235
  578. package/scripts/benchmark-operational-shadow.ts +0 -28
  579. package/scripts/benchmark-quality-harness.ts +0 -146
  580. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  581. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  582. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  583. package/scripts/benchmark-thin-harness.ts +0 -251
  584. package/scripts/benchmark-typescript-navigation.ts +0 -186
  585. package/scripts/benchmark-work-efficiency.ts +0 -334
  586. package/scripts/build-controller-ui.ts +0 -28
  587. package/scripts/check-npm-release.sh +0 -32
  588. package/scripts/check-public-release-surface.sh +0 -19
  589. package/scripts/check-release-published.sh +0 -57
  590. package/scripts/check-release-readiness.sh +0 -37
  591. package/scripts/check-release-version.mjs +0 -74
  592. package/scripts/check-skill-version.ts +0 -5
  593. package/scripts/check-tarball-install-smoke.sh +0 -150
  594. package/scripts/ensure-codegraph.sh +0 -16
  595. package/scripts/package-source-archive.sh +0 -147
  596. package/scripts/public-release-files.txt +0 -39
  597. package/scripts/public-release.ts +0 -174
  598. package/scripts/publish-release-tarball.sh +0 -27
  599. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  600. package/scripts/run-skill-hook.ts +0 -244
  601. package/scripts/stage-runtime-release.ts +0 -44
  602. package/scripts/sync-codex-installed-copies.sh +0 -182
  603. package/src/cli/commands/tools.ts +0 -167
  604. package/src/cli/controller/progress.ts +0 -656
  605. package/src/cli/controller/work-mode.ts +0 -181
  606. package/src/cli/local-bridge/console-assets.ts +0 -17
  607. package/src/cli/local-bridge/dashboard.ts +0 -17
  608. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  609. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  610. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  611. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  612. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  613. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  614. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  615. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  616. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  617. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  618. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  619. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  620. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  621. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  622. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  623. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  624. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  625. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  626. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  627. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  628. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  629. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  630. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  631. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  632. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  633. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  634. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  635. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  636. package/src/cli/mcp/access-mode.ts +0 -2
  637. package/src/cli/mcp/access-tools.ts +0 -2
  638. package/src/cli/mcp/audit.ts +0 -2
  639. package/src/cli/mcp/auth.ts +0 -2
  640. package/src/cli/mcp/instructions.ts +0 -2
  641. package/src/cli/mcp/legacy-context.ts +0 -2
  642. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  643. package/src/cli/mcp/multi-repository.ts +0 -2
  644. package/src/cli/mcp/oauth.ts +0 -2
  645. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  646. package/src/cli/mcp/paths.ts +0 -2
  647. package/src/cli/mcp/policy.ts +0 -2
  648. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  649. package/src/cli/mcp/proxy-env.ts +0 -2
  650. package/src/cli/mcp/readiness-projection.ts +0 -2
  651. package/src/cli/mcp/redaction.ts +0 -2
  652. package/src/cli/mcp/repo.ts +0 -2
  653. package/src/cli/mcp/repository-tools.ts +0 -2
  654. package/src/cli/mcp/server.ts +0 -2
  655. package/src/cli/mcp/setup.ts +0 -2
  656. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  657. package/src/cli/mcp/tool-contract.ts +0 -2
  658. package/src/cli/mcp/tools.ts +0 -2
  659. package/src/cli/mcp/toolset-names.ts +0 -2
  660. package/src/cli/mcp/toolset-selection.ts +0 -2
  661. package/src/cli/mcp/toolset.ts +0 -2
  662. package/src/cli/mcp/transports/http.ts +0 -2
  663. package/src/cli/mcp/transports/session-registry.ts +0 -2
  664. package/src/cli/mcp/transports/stdio.ts +0 -2
  665. package/src/cli/mcp/types.ts +0 -2
  666. package/src/cli/mcp/umbrella-tools.ts +0 -2
  667. package/src/cli/tools/codegraph.ts +0 -695
  668. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  669. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  670. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  671. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  672. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  673. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
  674. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
  675. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
  676. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
  677. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  678. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
  679. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
  680. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
  681. package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
  682. package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
  683. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  684. package/src/runtime/control-plane/routing/index.ts +0 -1
  685. package/src/runtime/control-plane/routing/route-policy.ts +0 -401
  686. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
  687. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  688. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  689. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  690. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  691. package/src/runtime/gateway/mcp/router.ts +0 -2
  692. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  693. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  694. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  695. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
@@ -0,0 +1,1151 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import type {
5
+ ComputerChatgptConversationIdentity,
6
+ ComputerChatgptConversationObservation,
7
+ ComputerChatgptConversationTarget,
8
+ ComputerChatgptConversationTargetPort,
9
+ } from '../packages/plugin-runtime/computer';
10
+ import {
11
+ CHATGPT_AUTOMATION_RATE_LIMITED,
12
+ CHATGPT_AUTOMATION_CONVERSATION_LIMIT_REACHED,
13
+ chatgptProviderBackpressureRemainingMs,
14
+ chatgptProviderPageFailure,
15
+ noteChatgptProviderBackpressure,
16
+ withChatgptProviderDispatchLane,
17
+ } from '../adapters/chatgpt/provider-delivery';
18
+ import { parseChatgptConversationIdentity } from './chatgpt-conversation';
19
+ import { WorkflowSupervisorControlPlane } from './control-plane';
20
+ import { hasCommittedSupervisorEnvelope, renderEffectMarker, sha256 } from './protocol';
21
+ import type { WorkflowSupervisorEphemeralDiscovery } from './server';
22
+ import type {
23
+ WorkflowSupervisorBrowserCommand,
24
+ WorkflowSupervisorBrowserTask,
25
+ WorkflowSupervisorConsumerStatus,
26
+ WorkflowSupervisorPreflightComposerObservation,
27
+ } from './types';
28
+
29
+ const DEFAULT_INTERVAL_MS = 1_000;
30
+ const IDLE_INTERVAL_MS = 5_000;
31
+ const MAX_TRANSPORT_BACKOFF_MS = 60_000;
32
+ const MAX_TRANSPORT_BACKOFF_STEPS = 6;
33
+ const MAX_LOCAL_OBSERVATION_ATTEMPTS = 3;
34
+ const USER_VISIBLE_DEGRADED_AFTER_MS = 30_000;
35
+ /**
36
+ * A task-local target/conversation failure must space its *own* retries. The
37
+ * exact conversation is a provider resource: re-navigating or replacing its tab
38
+ * on every tick is what produced conversation-read 429s, and a 429 is answered
39
+ * by sending fewer requests, never by opening another conversation.
40
+ *
41
+ * Only failures about *this exact conversation or target* are spaced here.
42
+ * Provider-wide unavailability already backs off the whole consumer, so spacing
43
+ * it again would delay a legitimate resume without reducing any request volume.
44
+ * A new exact-conversation failure code must be added to this mechanical
45
+ * retry-spacing classification.
46
+ */
47
+ const TASK_TARGET_RETRY_BASE_MS = 30_000;
48
+ /** Slow read-only probe cadence for an applied turn whose bounded resume is exhausted. */
49
+ const AWAITING_RECEIPT_OBSERVATION_MS = 60_000;
50
+ // Give a fully idle conversation longer than the 60-second late-receipt probe
51
+ // cadence before releasing its disposable Chrome attachment. Under pressure
52
+ // only the eight most recently idle exact resources retain that grace.
53
+ const SUPERVISOR_IDLE_TAB_GRACE_MS = 5 * 60_000;
54
+ const SUPERVISOR_IDLE_TAB_RETAIN_LIMIT = 8;
55
+ const TASK_TARGET_RETRY_MAX_MS = 10 * 60_000;
56
+ // Preparation may inspect/change a local draft, but never sends to ChatGPT.
57
+ // Stop after bounded unsuccessful preflights; an unchanged failing provider
58
+ // must not become either an infinite UI loop or a spent send generation.
59
+ const SUPERVISOR_PRE_SEND_BLOCKED = 'WORKFLOW_SUPERVISOR_PRE_SEND_BLOCKED';
60
+
61
+ export const TASK_TARGET_SPACED_FAILURE_CODES: ReadonlySet<string> = new Set([
62
+ 'COMPUTER_CHATGPT_CONVERSATION_CONTENT_UNAVAILABLE',
63
+ // Desktop Operator has already bounded and reaped the Apple Events holder.
64
+ // Space this task's next native attempt instead of immediately recontending
65
+ // the one global browser lane on another Supervisor tick.
66
+ 'BROWSER_AUTOMATION_TIMEOUT',
67
+ 'BROWSER_AUTOMATION_SERIALIZATION_BUSY',
68
+ 'WORKFLOW_SUPERVISOR_EXACT_CONVERSATION_UNPROVEN',
69
+ 'COMPUTER_CHATGPT_EXACT_TARGET_UNPROVEN',
70
+ 'COMPUTER_CHATGPT_EXACT_TARGET_AMBIGUOUS',
71
+ 'COMPUTER_CHATGPT_EXTENSION_EXACT_TARGET_AMBIGUOUS',
72
+ 'COMPUTER_CHATGPT_RESTORED_TARGET_UNPROVEN',
73
+ 'COMPUTER_CHATGPT_TARGET_RESTORE_FAILED',
74
+ // An absent manually closed tab cannot be re-created by a read-only probe.
75
+ 'COMPUTER_CHATGPT_EXTENSION_EXACT_TARGET_ABSENT_READ_ONLY',
76
+ 'COMPUTER_CHATGPT_BOOTSTRAP_TARGET_UNAVAILABLE',
77
+ 'COMPUTER_CHATGPT_EXTENSION_TARGET_OPEN_OUTCOME_UNKNOWN',
78
+ 'COMPUTER_CHATGPT_EXTENSION_BOOTSTRAP_OPEN_OUTCOME_UNKNOWN',
79
+ SUPERVISOR_PRE_SEND_BLOCKED,
80
+ 'WORKFLOW_SUPERVISOR_BOOTSTRAP_EXISTING_CONVERSATION',
81
+ ]);
82
+ function taskTargetRetryDelayMs(streak: number): number {
83
+ const exponent = Math.max(0, Math.min(6, Math.trunc(streak) - 1));
84
+ return Math.min(TASK_TARGET_RETRY_MAX_MS, TASK_TARGET_RETRY_BASE_MS * 2 ** exponent);
85
+ }
86
+
87
+ type ObservedConversation = {
88
+ conversation_id: string;
89
+ canonical_url: string;
90
+ title?: string;
91
+ projectTitle?: string;
92
+ projectUrl?: string;
93
+ is_current?: boolean;
94
+ };
95
+
96
+ type TaskTransportFailure = {
97
+ code: string;
98
+ observedAtMs: number;
99
+ firstFailureAtMs: number;
100
+ effectId?: string;
101
+ projectionVisible: boolean;
102
+ /** Consecutive identical failures; drives this task's own retry spacing. */
103
+ streak: number;
104
+ };
105
+
106
+ export interface WorkflowSupervisorTransportProjection {
107
+ taskId?: string;
108
+ effectId?: string;
109
+ state: 'degraded' | 'recovered';
110
+ code?: string;
111
+ firstFailureAt?: string;
112
+ observedAt: string;
113
+ }
114
+
115
+ export interface WorkflowSupervisorStallProjection {
116
+ taskId: string;
117
+ effectId: string;
118
+ state: 'exhausted' | 'recovered';
119
+ stallKind: 'retryable' | 'provider_resume_exhausted';
120
+ generations?: number;
121
+ maxGenerations?: number;
122
+ observedAt: string;
123
+ }
124
+
125
+ export interface WorkflowSupervisorComputerBrowserDependencies {
126
+ targetPort: ComputerChatgptConversationTargetPort;
127
+ nowMs(): number;
128
+ providerIdleGraceMs: number;
129
+ providerScopeKey: string;
130
+ sleep(ms: number): Promise<void>;
131
+ setInterval(handler: () => void, ms: number): ReturnType<typeof setInterval>;
132
+ clearInterval(timer: ReturnType<typeof setInterval>): void;
133
+ onError(error: unknown): void;
134
+ reportTransportState?(projection: WorkflowSupervisorTransportProjection): void;
135
+ reportTaskStall?(projection: WorkflowSupervisorStallProjection): void;
136
+ requestHumanAction?(input: {
137
+ taskId: string;
138
+ effectId?: string;
139
+ action: 'login' | 'grant_permission';
140
+ code: string;
141
+ }): void;
142
+ }
143
+
144
+ export interface WorkflowSupervisorNativeBrowserHandle {
145
+ readonly adapter: WorkflowSupervisorNativeBrowserAdapter;
146
+ status(): WorkflowSupervisorConsumerStatus;
147
+ close(): Promise<void>;
148
+ }
149
+
150
+ function normalize(value: string): string { return value.replace(/\s+/g, ' ').trim(); }
151
+ function localObservationDelayMs(completedAttempts: number, baseMs: number, maxMs: number): number {
152
+ const exponent = Math.max(0, Math.min(8, Math.trunc(completedAttempts) - 1));
153
+ return Math.min(maxMs, baseMs * 2 ** exponent);
154
+ }
155
+ function consumerFailureCode(error: unknown, fallback: string): string {
156
+ const message = error instanceof Error ? error.message : String(error);
157
+ const candidate = message.split(':', 1)[0]?.trim() ?? '';
158
+ return /^[A-Z][A-Z0-9_]+$/.test(candidate) ? candidate : fallback;
159
+ }
160
+ function isoTime(value: number | undefined): string | undefined {
161
+ return value === undefined ? undefined : new Date(value).toISOString();
162
+ }
163
+ function targetMarkerPresent(text: string, effectId: string): boolean { return text.includes(renderEffectMarker(effectId)); }
164
+ function snapshotTargetMarkerPresent(snapshot: ComputerChatgptConversationObservation, effectId: string): boolean {
165
+ return targetMarkerPresent(snapshot.latestUserText, effectId)
166
+ || snapshot.userMessages?.some((message) => targetMarkerPresent(message, effectId)) === true;
167
+ }
168
+ function unknownObservationFingerprint(effectId: string, reason: string, snapshot: ComputerChatgptConversationObservation): string {
169
+ return sha256(JSON.stringify({
170
+ effectId,
171
+ reason,
172
+ url: snapshot.url,
173
+ latestUserText: normalize(snapshot.latestUserText),
174
+ latestAssistantResponse: normalize(snapshot.latestAssistantResponse),
175
+ composerText: snapshot.composerText === undefined ? null : normalize(snapshot.composerText),
176
+ latestTurnRole: snapshot.latestTurnRole ?? null,
177
+ isGenerating: snapshot.isGenerating,
178
+ }));
179
+ }
180
+ function preflightComposerObservation(snapshot: ComputerChatgptConversationObservation, effectId: string): WorkflowSupervisorPreflightComposerObservation {
181
+ const value = snapshot.composerText;
182
+ const selectorVersion = snapshot.composerSelectorVersion === 'visible-single-v2' ? snapshot.composerSelectorVersion : undefined;
183
+ if (typeof value !== 'string') return { chars: 0, fingerprint: '', classification: 'unavailable', ...(selectorVersion ? { selectorVersion } : {}) };
184
+ const marker = /^<<<FORGE_WORKFLOW_EFFECT_V1:((?:fx|crpe)_[a-zA-Z0-9_-]{8,120})>>>/.exec(value);
185
+ return {
186
+ chars: value.length,
187
+ fingerprint: sha256(`preflight-composer-v1:\0${value}`).slice(0, 24),
188
+ classification: !value.trim() ? 'empty' : marker ? (marker[1] === effectId ? 'current_effect' : 'other_effect') : 'unattributed',
189
+ ...(selectorVersion ? { selectorVersion } : {}),
190
+ };
191
+ }
192
+ /**
193
+ * Optional, single-effect operator backup: this records no draft by default.
194
+ * A 0600 on-device authorization marker must name the exact blocked effect,
195
+ * current task, project URL, and observed fingerprint before raw text is saved.
196
+ * The file remains local; no transcript, plugin receipt, or logs carry text.
197
+ * This never clears an editor or permits a Send.
198
+ */
199
+ export function backupAuthorizedBlockedComposer(
200
+ controllerHome: string,
201
+ taskId: string,
202
+ effectId: string,
203
+ projectUrl: string,
204
+ snapshot: ComputerChatgptConversationObservation,
205
+ ): string | undefined {
206
+ if (!/^supervisor:[a-z0-9]+$/.test(taskId) || !/^(?:fx|crpe)_[a-zA-Z0-9_-]{8,120}$/.test(effectId)) return;
207
+ if (snapshot.composerSelectorVersion !== 'visible-single-v2' || !snapshot.composerText?.trim()) return;
208
+ if (preflightComposerObservation(snapshot, effectId).classification !== 'unattributed') return;
209
+ let expectedUrl: URL;
210
+ let actualUrl: URL;
211
+ try { expectedUrl = new URL(projectUrl); actualUrl = new URL(snapshot.url); } catch { return; }
212
+ const projectRoute = (url: URL): string | undefined =>
213
+ /^\/g\/(g-p-[a-z0-9]+)(?:-[^/]+)?\/project\/?$/i.exec(url.pathname)?.[1]?.toLowerCase();
214
+ if (expectedUrl.origin !== 'https://chatgpt.com' || actualUrl.origin !== expectedUrl.origin
215
+ || !projectRoute(expectedUrl) || projectRoute(actualUrl) !== projectRoute(expectedUrl)) return;
216
+ const observation = preflightComposerObservation(snapshot, effectId);
217
+ const base = join(controllerHome, 'supervisor', 'draft-backups');
218
+ const authorization = join(base, `authorization-${effectId}.json`);
219
+ let marker: unknown;
220
+ try { marker = JSON.parse(readFileSync(authorization, 'utf8')); } catch { return; }
221
+ if (!marker || typeof marker !== 'object' || Array.isArray(marker)) return;
222
+ const expected = marker as Record<string, unknown>;
223
+ if (expected.schemaVersion !== 1 || expected.taskId !== taskId
224
+ || expected.effectId !== effectId || expected.projectUrl !== projectUrl
225
+ || expected.fingerprint !== observation.fingerprint || expected.consent !== 'user-authorized-exact-backup') return;
226
+ mkdirSync(base, { recursive: true, mode: 0o700 });
227
+ const path = join(base, `composer-${effectId}-${observation.fingerprint}.json`);
228
+ const payload = { schemaVersion: 1, taskId, effectId, projectUrl, observedUrl: snapshot.url,
229
+ selectorVersion: snapshot.composerSelectorVersion, fingerprint: observation.fingerprint,
230
+ composerText: snapshot.composerText };
231
+ try {
232
+ writeFileSync(path, JSON.stringify(payload) + '\n', { encoding: 'utf8', mode: 0o600, flag: 'wx' });
233
+ } catch (error) {
234
+ // A repeated preflight must not overwrite the original evidence.
235
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') return;
236
+ try {
237
+ const existing = JSON.parse(readFileSync(path, 'utf8')) as typeof payload;
238
+ if (existing.fingerprint !== payload.fingerprint || existing.composerText !== payload.composerText
239
+ || existing.taskId !== taskId || existing.observedUrl !== snapshot.url) return;
240
+ } catch { return; }
241
+ }
242
+ return path;
243
+ }
244
+
245
+ /** An explicit on-device authorization + verified backup permits exactly one
246
+ * existing Chrome-extension editor replacement at prepare time. The dispatch
247
+ * still uses the unmodified canonical effect prompt and sole provider writer.
248
+ */
249
+ export function authorizedDraftReplacementPrompt(
250
+ controllerHome: string, taskId: string, effectId: string, projectUrl: string,
251
+ snapshot: ComputerChatgptConversationObservation, prompt: string,
252
+ ): string | undefined {
253
+ // Disk projection does not attest that this exact Chrome tab loaded the latest script.
254
+ // Never spend a one-use authorization before a matching read-only contract receipt.
255
+ if (snapshot.composerReplacementContract !== 'exact-backed-up-project-draft-v1') return;
256
+ const backup = backupAuthorizedBlockedComposer(controllerHome, taskId, effectId, projectUrl, snapshot);
257
+ if (!backup || !prompt.startsWith(`<<<FORGE_WORKFLOW_EFFECT_V1:${effectId}>>>`)) return;
258
+ const observation = preflightComposerObservation(snapshot, effectId);
259
+ let marker: unknown;
260
+ try { marker = JSON.parse(readFileSync(join(controllerHome, 'supervisor', 'draft-backups', `authorization-${effectId}.json`), 'utf8')); }
261
+ catch { return; }
262
+ if (!marker || typeof marker !== 'object' || Array.isArray(marker)) return;
263
+ const auth = marker as Record<string, unknown>;
264
+ if (auth.taskId !== taskId || auth.effectId !== effectId || auth.projectUrl !== projectUrl
265
+ || auth.fingerprint !== observation.fingerprint || auth.consent !== 'user-authorized-exact-backup'
266
+ || typeof auth.nonce !== 'string' || !/^[a-f0-9]{32}$/.test(auth.nonce)) return;
267
+ // The authorization is a single-use physical mutation capability, not a
268
+ // reusable password. Atomic exclusive creation remains durable across
269
+ // Runtime/extension restarts and concurrent preflights. A failed prepare
270
+ // consumes this token; the operator must explicitly authorize another one.
271
+ const consumption = join(controllerHome, 'supervisor', 'draft-backups',
272
+ `consumed-${effectId}-${auth.nonce}.json`);
273
+ try {
274
+ writeFileSync(consumption, JSON.stringify({ schemaVersion: 1, taskId, effectId,
275
+ projectUrl, fingerprint: observation.fingerprint, nonce: auth.nonce }) + '\n',
276
+ { encoding: 'utf8', mode: 0o600, flag: 'wx' });
277
+ } catch { return; }
278
+ return `<<<FORGE_DRAFT_REPLACE_V1:${observation.fingerprint}:${auth.nonce}>>>\n${prompt}`;
279
+ }
280
+
281
+ /**
282
+ * An exact machine-owned draft can outlive its effect in a Project editor.
283
+ * Already-applied effects may be reflected by ChatGPT in other Project tabs;
284
+ * only the immutable full ledger prompt (allowing layout-only whitespace)
285
+ * qualifies. For never-sent STOPPED effects retain the original stricter
286
+ * preflight digest rule. Human, unknown-send and modified drafts never qualify.
287
+ */
288
+ export function retiredBootstrapDraftReplacementPrompt(
289
+ control: WorkflowSupervisorControlPlane,
290
+ task: WorkflowSupervisorBrowserTask,
291
+ snapshot: ComputerChatgptConversationObservation,
292
+ prompt: string,
293
+ ): string | undefined {
294
+ if (snapshot.composerSelectorVersion !== 'visible-single-v2' || !snapshot.composerText
295
+ || snapshot.isGenerating || !prompt.startsWith('<<<FORGE_WORKFLOW_EFFECT_V1:')) return;
296
+ const draft = snapshot.composerText.replace(/\r\n?/g, '\n');
297
+ const match = /^<<<FORGE_WORKFLOW_EFFECT_V1:((?:fx|crpe)_[a-zA-Z0-9_-]{8,120})>>>/.exec(draft);
298
+ if (!match) return;
299
+ const staleEffect = control.getEffect(match[1]!);
300
+ if (!staleEffect || staleEffect.taskId === task.taskId) return;
301
+ const alreadyApplied = control.store.effectApplied(staleEffect.effectId);
302
+ // A live effect can only be retired after durable proof that its own Send
303
+ // already applied. This never treats an unknown Send as a safe draft.
304
+ if (alreadyApplied) {
305
+ if (snapshot.composerReplacementContract !== 'exact-backed-up-project-draft-v1') return;
306
+ let expected: URL; let actual: URL;
307
+ try { expected = new URL(control.bootstrapProjectUrl(task.taskId)); actual = new URL(snapshot.url); }
308
+ catch { return; }
309
+ const projectRoute = (url: URL) => /^\/g\/(g-p-[a-z0-9]+)(?:-[^/]+)?\/project\/?$/i.exec(url.pathname)?.[1]?.toLowerCase();
310
+ if (expected.origin !== 'https://chatgpt.com' || actual.origin !== expected.origin
311
+ || !projectRoute(expected) || projectRoute(actual) !== projectRoute(expected)) return;
312
+ const canonical = staleEffect.prompt.replace(/\r\n?/g, '\n');
313
+ if (draft.replace(/\s+/g, ' ').trim() !== canonical.replace(/\s+/g, ' ').trim()) return;
314
+ } else {
315
+ if (control.effectDispatchBudget(staleEffect.effectId).generations !== 0) return;
316
+ const oldTask = control.getTask(staleEffect.taskId);
317
+ const retired = oldTask && control.getTaskByConversationId(oldTask.conversationId);
318
+ if (retired?.terminal !== 'STOPPED'
319
+ || control.bootstrapProjectUrl(staleEffect.taskId) !== control.bootstrapProjectUrl(task.taskId)) return;
320
+ }
321
+ const attested = control.preSendComposerObservation(staleEffect.effectId);
322
+ // Both proofs are exact: the immutable effect text, or the previously
323
+ // attested pre-Send editor bytes. The older extension could synthesize an
324
+ // extra newline inside a URL; the corrected extension reads the canonical
325
+ // text without it. Require the persisted no-click preflight in either case.
326
+ const fingerprint = sha256('preflight-composer-v1:\0' + draft).slice(0, 24);
327
+ const exactSource = draft === staleEffect.prompt.replace(/\r\n?/g, '\n');
328
+ const exactPreflight = attested?.fingerprint === fingerprint && attested.chars === draft.length;
329
+ if (!alreadyApplied && (!attested || (!exactSource && !exactPreflight))) return;
330
+ return '<<<FORGE_RETIRED_DRAFT_V1:' + staleEffect.effectId + ':' + fingerprint + '>>>\n' + prompt;
331
+ }
332
+
333
+ function exactConversation(snapshot: ComputerChatgptConversationObservation, task: WorkflowSupervisorBrowserTask): boolean {
334
+ try { return parseChatgptConversationIdentity(snapshot.url).conversationId === task.conversationId; }
335
+ catch { return false; }
336
+ }
337
+ function identityForTask(task: WorkflowSupervisorBrowserTask): ComputerChatgptConversationIdentity {
338
+ const parsed = parseChatgptConversationIdentity(task.conversationUrl);
339
+ if (parsed.conversationId !== task.conversationId) throw new Error('WORKFLOW_SUPERVISOR_CONVERSATION_IDENTITY_MISMATCH');
340
+ return { namespace: 'chatgpt.conversation', conversationId: parsed.conversationId, canonicalUrl: parsed.canonicalUrl };
341
+ }
342
+ function conversationContentAvailable(snapshot: ComputerChatgptConversationObservation): boolean {
343
+ // Navigation can commit the exact URL before ChatGPT loads the conversation.
344
+ // An empty/error shell is transport evidence, never evidence about a send or
345
+ // about whether the model stopped working.
346
+ return snapshot.composerText !== undefined || snapshot.isGenerating
347
+ || Boolean(snapshot.latestUserText.trim() || snapshot.latestAssistantResponse.trim() || snapshot.providerActivityText.trim());
348
+ }
349
+ function projectMetadataFromConversationUrl(value: string): { projectTitle?: string; projectUrl?: string } {
350
+ try {
351
+ const parsed = new URL(value);
352
+ const match = /^\/g\/(g-p-[a-z0-9]+)(?:-([^/]+))?\/c\/[a-z0-9-]+\/?$/i.exec(parsed.pathname);
353
+ if (!match?.[1]) return {};
354
+ const slug = match[2]?.trim();
355
+ return {
356
+ ...(slug ? { projectTitle: slug.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim() } : {}),
357
+ projectUrl: `https://chatgpt.com/g/${match[1]}/project`,
358
+ };
359
+ } catch { return {}; }
360
+ }
361
+
362
+ export class WorkflowSupervisorNativeBrowserAdapter {
363
+ private readonly observedAssistant = new Map<string, string>();
364
+ private readonly providerFailureSeen = new Map<string, string>();
365
+ /** A recovery sent while an error surface is still visible must not inherit that stale error as its own failure. */
366
+ private readonly providerFailureAwaitingClear = new Map<string, string>();
367
+ private readonly freshSendCheckedAt = new Map<string, number>();
368
+ private timer?: ReturnType<typeof setInterval>;
369
+ private inflight?: Promise<void>;
370
+ private closed = false;
371
+ private nextRunAtMs = 0;
372
+ private lastRunHadTasks = false;
373
+ private lastRunTransportUnavailable = false;
374
+ private transportFailureStreak = 0;
375
+ private servicingFreshSend = false;
376
+ private conversations: ObservedConversation[] = [];
377
+ private lastTickStartedAtMs?: number;
378
+ private lastTickCompletedAtMs?: number;
379
+ private lastCommandAttemptAtMs?: number;
380
+ private lastFailure?: { code: string; observedAtMs: number; taskId?: string; effectId?: string };
381
+ private firstTransportFailureAtMs?: number;
382
+ private transportProjectionVisible = false;
383
+ private readonly taskTransportFailures = new Map<string, TaskTransportFailure>();
384
+ /** Derived projection cache only; Supervisor store remains the stall authority. */
385
+ private readonly projectedTaskStalls = new Map<string, Omit<WorkflowSupervisorStallProjection, 'taskId' | 'state' | 'observedAt'>>();
386
+ /** Spacing for read-only observation of a turn whose bounded resume is exhausted. */
387
+ private readonly awaitingReceiptObservedAtMs = new Map<string, number>();
388
+ /** Rebuildable idle-grace projection; Supervisor effects remain the sole authority. */
389
+ private readonly idleResourceSinceMs = new Map<string, number>();
390
+
391
+ constructor(
392
+ private readonly control: WorkflowSupervisorControlPlane,
393
+ private readonly discovery: WorkflowSupervisorEphemeralDiscovery,
394
+ private readonly deps: WorkflowSupervisorComputerBrowserDependencies,
395
+ ) {}
396
+
397
+ private noteFailure(code: string, task?: WorkflowSupervisorBrowserTask, effectId?: string): void {
398
+ const nowMs = this.deps.nowMs();
399
+ this.lastFailure = { code, observedAtMs: nowMs, ...(task ? { taskId: task.taskId } : {}), ...(effectId ? { effectId } : {}) };
400
+ this.firstTransportFailureAtMs ??= nowMs;
401
+ if (!this.transportProjectionVisible && nowMs - this.firstTransportFailureAtMs >= USER_VISIBLE_DEGRADED_AFTER_MS) {
402
+ this.transportProjectionVisible = true;
403
+ this.deps.reportTransportState?.({
404
+ state: 'degraded', code,
405
+ ...(task ? { taskId: task.taskId } : {}), ...(effectId ? { effectId } : {}),
406
+ firstFailureAt: new Date(this.firstTransportFailureAtMs).toISOString(), observedAt: new Date(nowMs).toISOString(),
407
+ });
408
+ }
409
+ }
410
+
411
+ private clearTransportFailure(): void {
412
+ this.lastFailure = undefined;
413
+ if (this.firstTransportFailureAtMs !== undefined) {
414
+ const nowMs = this.deps.nowMs();
415
+ if (this.transportProjectionVisible) this.deps.reportTransportState?.({ state: 'recovered', observedAt: new Date(nowMs).toISOString() });
416
+ this.firstTransportFailureAtMs = undefined;
417
+ this.transportProjectionVisible = false;
418
+ }
419
+ }
420
+
421
+ /** Rebuildable de-duplication only; SQLite events and CAS own the decision. */
422
+ private readonly automaticMigrationAttempted = new Map<string, { key: string; atMs: number }>();
423
+ /** Read-only provider progress, not another durable effect/Send ledger. */
424
+ private readonly stalledProviderObservations = new Map<string, { fingerprint: string; changedAtMs: number; failedProbeAtMs: number }>();
425
+
426
+ /**
427
+ * Fail over a proven repeatedly unusable conversation, not a provider-wide
428
+ * outage or an outcome-unknown Send. The canonical migration transaction
429
+ * enforces the existing maximum of two unsuccessful conversation moves.
430
+ */
431
+ private maybeAutoMigrate(task: WorkflowSupervisorBrowserTask): boolean {
432
+ if (task.conversationId.startsWith('bootstrap:')
433
+ || chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs()) > 0) return false;
434
+ const evidence = this.control.store.automaticConversationFailureEvidence(task.taskId);
435
+ if (evidence.count < 3 || evidence.latestEventId <= 0) return false;
436
+ const key = `${task.taskId}:${task.conversationId}:${evidence.latestEventId}`;
437
+ const attempt = this.automaticMigrationAttempted.get(task.taskId);
438
+ if (attempt?.key === key && this.deps.nowMs() - attempt.atMs < TASK_TARGET_RETRY_MAX_MS) return false;
439
+ this.automaticMigrationAttempted.set(task.taskId, { key, atMs: this.deps.nowMs() });
440
+ try {
441
+ const migrated = this.control.migrateConversation({
442
+ taskId: task.taskId,
443
+ expectedConversationId: task.conversationId,
444
+ fresh: true,
445
+ requestId: `automatic-delivery-fuse:${task.conversationId}:${evidence.latestEventId}`,
446
+ reason: `The same Supervisor conversation failed ${evidence.count} task-local delivery/progress attempts without an applied effect or completion. The prior conversation is not reused; the exact effect journal and existing two-migration cap remain authoritative.`,
447
+ authorizedBy: 'supervisor-automatic-delivery-fuse',
448
+ });
449
+ if (migrated.migrated) {
450
+ this.clearTaskFailure(task.taskId);
451
+ this.awaitingReceiptObservedAtMs.delete(task.taskId);
452
+ }
453
+ return migrated.migrated;
454
+ } catch (error) {
455
+ // A pending unknown send, exhausted migration budget, or stale task is a
456
+ // true safety barrier: do not replay it or bypass the canonical store.
457
+ const code = consumerFailureCode(error, 'WORKFLOW_SUPERVISOR_AUTOMATIC_MIGRATION_DENIED');
458
+ this.noteFailure(code, task);
459
+ return false;
460
+ }
461
+ }
462
+
463
+ private noteTaskFailure(code: string, task: WorkflowSupervisorBrowserTask, effectId?: string, countTowardFuse = true): void {
464
+ const nowMs = this.deps.nowMs();
465
+ const previous = this.taskTransportFailures.get(task.taskId);
466
+ const failure: TaskTransportFailure = {
467
+ code,
468
+ observedAtMs: nowMs,
469
+ firstFailureAtMs: previous?.code === code ? previous.firstFailureAtMs : nowMs,
470
+ ...(effectId ? { effectId } : previous?.effectId ? { effectId: previous.effectId } : {}),
471
+ projectionVisible: previous?.projectionVisible ?? false,
472
+ // A materially different failure reason is new evidence and restarts the
473
+ // spacing; an unchanged one keeps growing it.
474
+ streak: previous?.code === code ? previous.streak + 1 : 1,
475
+ };
476
+ if (!failure.projectionVisible && nowMs - failure.firstFailureAtMs >= USER_VISIBLE_DEGRADED_AFTER_MS) {
477
+ failure.projectionVisible = true;
478
+ this.deps.reportTransportState?.({
479
+ state: 'degraded', code, taskId: task.taskId,
480
+ ...(failure.effectId ? { effectId: failure.effectId } : {}),
481
+ firstFailureAt: new Date(failure.firstFailureAtMs).toISOString(), observedAt: new Date(nowMs).toISOString(),
482
+ });
483
+ }
484
+ this.taskTransportFailures.set(task.taskId, failure);
485
+ // Account-wide cooldown is not evidence that this *conversation* failed.
486
+ if (!countTowardFuse || chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, nowMs) > 0) return;
487
+ // Preflight rejections are already one durable failed attempt in the
488
+ // Supervisor effect ledger; counting them again would double the fuse.
489
+ if (code !== SUPERVISOR_PRE_SEND_BLOCKED) {
490
+ this.control.store.recordBrowserDeliveryAttemptFailure({
491
+ taskId: task.taskId, conversationId: task.conversationId, code,
492
+ ...(effectId ? { effectId } : {}),
493
+ });
494
+ }
495
+ this.maybeAutoMigrate(task);
496
+ }
497
+
498
+ private clearTaskFailure(taskId: string, effectId?: string): void {
499
+ const failure = this.taskTransportFailures.get(taskId);
500
+ if (!failure) return;
501
+ this.taskTransportFailures.delete(taskId);
502
+ const recoveredEffectId = effectId ?? failure.effectId;
503
+ if (failure.projectionVisible) this.deps.reportTransportState?.({
504
+ state: 'recovered', taskId,
505
+ ...(recoveredEffectId ? { effectId: recoveredEffectId } : {}),
506
+ observedAt: new Date(this.deps.nowMs()).toISOString(),
507
+ });
508
+ }
509
+
510
+ private noteTargetUnavailable(task: WorkflowSupervisorBrowserTask, effectId: string | undefined, failure: { code: string; humanAction?: 'login' | 'grant_permission' }): void {
511
+ // A user login/grant problem cannot be fixed by a different conversation.
512
+ if (failure.humanAction) {
513
+ this.noteTaskFailure(failure.code, task, effectId, false);
514
+ this.deps.requestHumanAction?.({ taskId: task.taskId, ...(effectId ? { effectId } : {}), action: failure.humanAction, code: failure.code });
515
+ return;
516
+ }
517
+ // Task-local failures are spaced independently of unrelated conversations.
518
+ this.noteTaskFailure(failure.code, task, effectId);
519
+ }
520
+
521
+ private syncTaskStallProjections(): void {
522
+ const activeTaskIds = new Set<string>();
523
+ const observedAt = new Date(this.deps.nowMs()).toISOString();
524
+ for (const task of this.control.listTasks(true)) {
525
+ activeTaskIds.add(task.taskId);
526
+ let stall: ReturnType<WorkflowSupervisorControlPlane['taskStall']>;
527
+ try { stall = this.control.taskStall(task.taskId); }
528
+ catch (error) { this.deps.onError(error); continue; }
529
+ if (stall.state === 'retryable' || stall.state === 'provider_resume_exhausted') {
530
+ const next = {
531
+ effectId: stall.effectId,
532
+ stallKind: stall.state,
533
+ ...(stall.state === 'retryable' ? { generations: stall.generations, maxGenerations: stall.maxGenerations } : {}),
534
+ } as Omit<WorkflowSupervisorStallProjection, 'taskId' | 'state' | 'observedAt'>;
535
+ const prior = this.projectedTaskStalls.get(task.taskId);
536
+ const unchanged = prior?.effectId === next.effectId
537
+ && prior.stallKind === next.stallKind
538
+ && prior.generations === next.generations
539
+ && prior.maxGenerations === next.maxGenerations;
540
+ if (!unchanged) {
541
+ try { this.deps.reportTaskStall?.({ taskId: task.taskId, state: 'exhausted', observedAt, ...next }); }
542
+ catch (error) { this.deps.onError(error); }
543
+ this.projectedTaskStalls.set(task.taskId, next);
544
+ }
545
+ continue;
546
+ }
547
+ const prior = this.projectedTaskStalls.get(task.taskId);
548
+ if (prior) {
549
+ try { this.deps.reportTaskStall?.({ taskId: task.taskId, state: 'recovered', observedAt, ...prior }); }
550
+ catch (error) { this.deps.onError(error); }
551
+ this.projectedTaskStalls.delete(task.taskId);
552
+ }
553
+ }
554
+ for (const [taskId, prior] of this.projectedTaskStalls) {
555
+ if (activeTaskIds.has(taskId)) continue;
556
+ try { this.deps.reportTaskStall?.({ taskId, state: 'recovered', observedAt, ...prior }); }
557
+ catch (error) { this.deps.onError(error); }
558
+ this.projectedTaskStalls.delete(taskId);
559
+ }
560
+ this.pruneAutomaticRecoveryProjections(activeTaskIds);
561
+ }
562
+
563
+ private pruneAutomaticRecoveryProjections(activeTaskIds: ReadonlySet<string>): void {
564
+ for (const id of this.automaticMigrationAttempted.keys())
565
+ if (!activeTaskIds.has(id)) this.automaticMigrationAttempted.delete(id);
566
+ for (const id of this.stalledProviderObservations.keys())
567
+ if (!activeTaskIds.has(id)) this.stalledProviderObservations.delete(id);
568
+ }
569
+
570
+ private dueCommand(nowMs: number): WorkflowSupervisorConsumerStatus['dueCommand'] {
571
+ const candidates: NonNullable<WorkflowSupervisorConsumerStatus['dueCommand']>[] = [];
572
+ for (const task of this.control.browserTasks()) {
573
+ try {
574
+ const poll = task.conversationId.startsWith('bootstrap:') ? this.control.bootstrapPoll(task.taskId) : this.control.browserPoll({ conversationId: task.conversationId, conversationUrl: task.conversationUrl });
575
+ const command = poll.command;
576
+ if (!command) continue;
577
+ const effect = this.control.getEffect(command.effectId);
578
+ const createdAtMs = effect?.createdAt ? Date.parse(effect.createdAt) : Number.NaN;
579
+ candidates.push({ taskId: task.taskId, conversationId: task.conversationId, effectId: command.effectId, mode: command.mode, kind: command.kind,
580
+ ...(effect?.createdAt ? { createdAt: effect.createdAt } : {}), ...(Number.isFinite(createdAtMs) ? { ageMs: Math.max(0, nowMs - createdAtMs) } : {}) });
581
+ } catch { /* read projection only */ }
582
+ }
583
+ candidates.sort((left, right) => (right.ageMs ?? -1) - (left.ageMs ?? -1));
584
+ return candidates[0];
585
+ }
586
+
587
+ status(): WorkflowSupervisorConsumerStatus {
588
+ const nowMs = this.deps.nowMs();
589
+ const dueCommand = this.dueCommand(nowMs);
590
+ const dueTaskFailure = dueCommand ? this.taskTransportFailures.get(dueCommand.taskId) : undefined;
591
+ const projectedFailure = dueTaskFailure
592
+ ? { code: dueTaskFailure.code, observedAtMs: dueTaskFailure.observedAtMs, taskId: dueCommand!.taskId, ...(dueTaskFailure.effectId ? { effectId: dueTaskFailure.effectId } : {}) }
593
+ : this.lastFailure;
594
+ const stalled = Boolean(this.inflight && this.lastTickStartedAtMs !== undefined && nowMs - this.lastTickStartedAtMs > MAX_TRANSPORT_BACKOFF_MS + 10_000);
595
+ return {
596
+ enabled: true, running: Boolean(this.timer) && !this.closed, observedAt: new Date(nowMs).toISOString(),
597
+ ...(isoTime(this.lastTickStartedAtMs) ? { lastTickStartedAt: isoTime(this.lastTickStartedAtMs) } : {}),
598
+ ...(isoTime(this.lastTickCompletedAtMs) ? { lastTickCompletedAt: isoTime(this.lastTickCompletedAtMs) } : {}),
599
+ ...(isoTime(this.lastCommandAttemptAtMs) ? { lastCommandAttemptAt: isoTime(this.lastCommandAttemptAtMs) } : {}),
600
+ ...(this.nextRunAtMs > 0 ? { nextAttemptAt: new Date(this.nextRunAtMs).toISOString() } : {}),
601
+ transportFailureStreak: this.transportFailureStreak,
602
+ providerBackpressureMs: chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, nowMs), stalled,
603
+ ...(dueCommand ? { dueCommand } : {}),
604
+ ...(projectedFailure ? { lastFailure: { code: projectedFailure.code, observedAt: new Date(projectedFailure.observedAtMs).toISOString(),
605
+ ...(projectedFailure.taskId ? { taskId: projectedFailure.taskId } : {}), ...(projectedFailure.effectId ? { effectId: projectedFailure.effectId } : {}) } } : {}),
606
+ };
607
+ }
608
+
609
+ start(intervalMs = DEFAULT_INTERVAL_MS): void {
610
+ if (this.timer || this.closed) return;
611
+ const activeIntervalMs = Math.max(1, Math.trunc(intervalMs));
612
+ const idleIntervalMs = Math.max(activeIntervalMs, IDLE_INTERVAL_MS);
613
+ const tick = () => {
614
+ if (this.inflight || this.closed || this.deps.nowMs() < this.nextRunAtMs) return;
615
+ this.inflight = this.runOnce().catch((error) => { this.noteFailure(consumerFailureCode(error, 'WORKFLOW_SUPERVISOR_COMPUTER_TARGET_CONSUMER_FAILED')); this.deps.onError(error); }).finally(() => {
616
+ const baseIntervalMs = this.lastRunHadTasks ? activeIntervalMs : idleIntervalMs;
617
+ this.nextRunAtMs = this.deps.nowMs() + (this.lastRunTransportUnavailable
618
+ ? Math.min(baseIntervalMs * 2 ** Math.min(this.transportFailureStreak, MAX_TRANSPORT_BACKOFF_STEPS), MAX_TRANSPORT_BACKOFF_MS)
619
+ : baseIntervalMs);
620
+ this.inflight = undefined;
621
+ });
622
+ };
623
+ tick();
624
+ // This consumer is part of the persistent canonical Runtime, not optional
625
+ // background cleanup. Keep the timer referenced so provider continuation
626
+ // cannot silently stop while the Runtime and MCP listener remain healthy.
627
+ this.timer = this.deps.setInterval(tick, activeIntervalMs);
628
+ }
629
+
630
+ async close(): Promise<void> {
631
+ this.closed = true;
632
+ if (this.timer) this.deps.clearInterval(this.timer);
633
+ this.timer = undefined;
634
+ await this.inflight?.catch(() => undefined);
635
+ await this.deps.targetPort.close().catch(() => undefined);
636
+ this.observedAssistant.clear(); this.providerFailureSeen.clear(); this.providerFailureAwaitingClear.clear(); this.freshSendCheckedAt.clear(); this.taskTransportFailures.clear(); this.projectedTaskStalls.clear(); this.awaitingReceiptObservedAtMs.clear(); this.idleResourceSinceMs.clear(); this.automaticMigrationAttempted.clear(); this.stalledProviderObservations.clear();
637
+ }
638
+
639
+ async runOnce(): Promise<void> {
640
+ if (this.closed) return;
641
+ this.lastTickStartedAtMs = this.deps.nowMs();
642
+ // Preserve fresh-send fairness across ticks. serviceFreshSend prunes
643
+ // retired effect ids; resetting here repeatedly favors the first task.
644
+ this.lastRunTransportUnavailable = false;
645
+ // One Supervisor terminal-event authority. A Work closed while this
646
+ // consumer was offline cannot keep delivering a stale effect on restart.
647
+ this.control.reconcileTerminalWorkBoundTasks();
648
+ this.syncTaskStallProjections();
649
+ const previouslyObservedTasks = this.control.browserTasks();
650
+ // Read-only watchdog for a pre-send blocked task: after each bounded
651
+ // interval with no applied effect or completion, record a failed progress
652
+ // check in the same journal. This is not a Send/retry attempt.
653
+ for (const task of previouslyObservedTasks) {
654
+ const stall = this.control.taskStall(task.taskId);
655
+ if (stall.state === 'spaced' && stall.reason === 'pre_send_infrastructure_blocked') {
656
+ const evidence = this.control.store.automaticConversationFailureEvidence(task.taskId);
657
+ if (evidence.count > 0 && evidence.count < 3
658
+ && this.deps.nowMs() - evidence.lastFailureAtMs >= AWAITING_RECEIPT_OBSERVATION_MS
659
+ && chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs()) === 0) {
660
+ this.control.store.recordBrowserDeliveryAttemptFailure({ taskId: task.taskId,
661
+ conversationId: task.conversationId, effectId: stall.effectId, code: 'WORKFLOW_SUPERVISOR_NO_SEND_PROGRESS_WATCHDOG' });
662
+ }
663
+ this.maybeAutoMigrate(task);
664
+ }
665
+ }
666
+ // The watchdog may have changed the exact Task conversation identity.
667
+ // Never process a stale pre-migration browser task in this tick.
668
+ const tasks = this.control.browserTasks();
669
+ this.lastRunHadTasks = tasks.length > 0;
670
+ let inventory;
671
+ try { inventory = await this.deps.targetPort.inventory(); }
672
+ catch (error) {
673
+ this.lastRunTransportUnavailable = true;
674
+ this.noteFailure(consumerFailureCode(error, 'WORKFLOW_SUPERVISOR_COMPUTER_TARGET_INVENTORY_UNAVAILABLE'));
675
+ this.transportFailureStreak = Math.min(this.transportFailureStreak + 1, MAX_TRANSPORT_BACKOFF_STEPS);
676
+ this.lastTickCompletedAtMs = this.deps.nowMs();
677
+ throw error;
678
+ }
679
+ this.conversations = inventory.conversations.map((conversation) => ({ conversation_id: conversation.conversationId, canonical_url: conversation.canonicalUrl,
680
+ ...(conversation.title ? { title: conversation.title } : {}), ...(conversation.projectTitle ? { projectTitle: conversation.projectTitle } : {}),
681
+ ...(conversation.projectUrl ? { projectUrl: conversation.projectUrl } : {}), ...(conversation.isCurrent ? { is_current: true } : {}) }));
682
+ if (!inventory.complete && tasks.length > 0) {
683
+ // Partial cross-browser discovery is degraded evidence, but must not
684
+ // throttle an independently healthy provider's exact-target delivery.
685
+ this.lastRunTransportUnavailable = inventory.providerInventoryReady !== true;
686
+ this.noteFailure('WORKFLOW_SUPERVISOR_NATIVE_INVENTORY_INCOMPLETE');
687
+ } else if (inventory.complete) {
688
+ this.clearTransportFailure();
689
+ }
690
+ // Retire only disposable provider attachments. The durable Supervisor
691
+ // effect ledger (not textual DONE or a browser tab) proves protection: a
692
+ // queued/reconciling command and an applied turn awaiting its receipt both
693
+ // remain attached even during cooldown. A terminal task is absent here.
694
+ const taskResourceKeys = new Set<string>();
695
+ const protectedResourceKeys = new Set<string>();
696
+ for (const task of this.control.listTasks(true)) {
697
+ const key = task.conversationId.startsWith('bootstrap:')
698
+ ? `chatgpt.bootstrap:${task.taskId}`
699
+ : `chatgpt.conversation:${task.conversationId}`;
700
+ taskResourceKeys.add(key);
701
+ // Eligibility is time-dependent; an unresolved effect still owns its
702
+ // exact attachment during retry spacing or a blocked preparation.
703
+ if (this.control.store.currentUnappliedEffect(task.taskId)
704
+ || this.control.store.hasAppliedEffectAwaitingCompletion(task.taskId)) protectedResourceKeys.add(key);
705
+ }
706
+ for (const key of this.idleResourceSinceMs.keys()) {
707
+ if (!taskResourceKeys.has(key) || protectedResourceKeys.has(key)) this.idleResourceSinceMs.delete(key);
708
+ }
709
+ const now = this.deps.nowMs();
710
+ for (const key of taskResourceKeys) {
711
+ if (!protectedResourceKeys.has(key) && !this.idleResourceSinceMs.has(key)) this.idleResourceSinceMs.set(key, now);
712
+ }
713
+ // Enforce a bounded idle attachment budget. This does not cap truly active
714
+ // effects: they cannot be retired merely because tab pressure is high.
715
+ const graceKeys = [...this.idleResourceSinceMs.entries()]
716
+ .filter(([key, since]) => taskResourceKeys.has(key) && now - since < SUPERVISOR_IDLE_TAB_GRACE_MS)
717
+ .sort((left, right) => right[1] - left[1])
718
+ .slice(0, SUPERVISOR_IDLE_TAB_RETAIN_LIMIT)
719
+ .map(([key]) => key);
720
+ const activeResourceKeys = [...new Set([...protectedResourceKeys, ...graceKeys])];
721
+ await this.deps.targetPort.cleanup(activeResourceKeys).catch((error) => this.deps.onError(error));
722
+ for (const task of tasks) {
723
+ if (this.closed) break;
724
+ await this.serviceFreshSend();
725
+ try {
726
+ const poll = task.conversationId.startsWith('bootstrap:') ? this.control.bootstrapPoll(task.taskId) : this.control.browserPoll({ conversationId: task.conversationId, conversationUrl: task.conversationUrl });
727
+ if (poll.terminal || poll.command?.mode === 'send') continue;
728
+ await this.processTask(task);
729
+ } catch (error) {
730
+ this.noteTaskFailure(consumerFailureCode(error, 'WORKFLOW_SUPERVISOR_COMPUTER_TARGET_COMMAND_FAILED'), task);
731
+ this.deps.onError(error);
732
+ }
733
+ }
734
+ await this.serviceFreshSend();
735
+ this.discovery.update(this.conversations, 'computer-browser');
736
+ this.control.recordBrowserDiscovery('computer-browser', this.discovery.sourceConversations('computer-browser'));
737
+ this.transportFailureStreak = this.lastRunTransportUnavailable ? Math.min(this.transportFailureStreak + 1, MAX_TRANSPORT_BACKOFF_STEPS) : 0;
738
+ this.lastTickCompletedAtMs = this.deps.nowMs();
739
+ }
740
+
741
+ private async serviceFreshSend(excludeTaskId?: string): Promise<void> {
742
+ if (this.closed || this.servicingFreshSend) return;
743
+ const nowMs = this.deps.nowMs();
744
+ const eligible = this.control.browserTasks().flatMap((task) => {
745
+ if (task.taskId === excludeTaskId) return [];
746
+ try {
747
+ const poll = task.conversationId.startsWith('bootstrap:') ? this.control.bootstrapPoll(task.taskId) : this.control.browserPoll({ conversationId: task.conversationId, conversationUrl: task.conversationUrl });
748
+ if (poll.command?.mode !== 'send') return [];
749
+ return [{ task, effectId: poll.command.effectId, checkedAt: this.freshSendCheckedAt.get(poll.command.effectId) }];
750
+ } catch { return []; }
751
+ });
752
+ const pending = new Set(eligible.map(({ effectId }) => effectId));
753
+ for (const effectId of this.freshSendCheckedAt.keys()) if (!pending.has(effectId)) this.freshSendCheckedAt.delete(effectId);
754
+ eligible.sort((left, right) => (left.checkedAt ?? 0) - (right.checkedAt ?? 0));
755
+ const next = eligible.find(({ checkedAt }) => checkedAt === undefined || nowMs - checkedAt >= DEFAULT_INTERVAL_MS);
756
+ if (!next) return;
757
+ this.freshSendCheckedAt.set(next.effectId, nowMs); this.lastCommandAttemptAtMs = nowMs; this.servicingFreshSend = true;
758
+ try { await this.processTask(next.task); }
759
+ catch (error) { this.noteTaskFailure(consumerFailureCode(error, 'WORKFLOW_SUPERVISOR_COMPUTER_TARGET_COMMAND_FAILED'), next.task, next.effectId); this.deps.onError(error); }
760
+ finally { this.servicingFreshSend = false; }
761
+ }
762
+
763
+ private async processTask(task: WorkflowSupervisorBrowserTask): Promise<void> {
764
+ // Retry spacing is task-local and covers every exact-target/conversation
765
+ // failure, not just an unloaded page: the retained tab is re-observed at a
766
+ // growing interval instead of being closed, re-opened, and reloaded.
767
+ const transportFailure = this.taskTransportFailures.get(task.taskId);
768
+ // Derived from the durable effect log, not this adapter's process memory.
769
+ const stall = this.control.taskStall(task.taskId);
770
+ if (stall.state === 'spaced' && stall.reason === 'pre_send_infrastructure_blocked') {
771
+ // A blocked new recovery cannot authorize a Send, but it must not hide
772
+ // the completed final reply of its already-applied source effect.
773
+ // Probe read-only at the existing bounded receipt cadence.
774
+ if (!this.control.store.hasAppliedEffectAwaitingCompletion(task.taskId)) return;
775
+ const checkedAt = this.awaitingReceiptObservedAtMs.get(task.taskId) ?? 0;
776
+ if (this.deps.nowMs() - checkedAt < AWAITING_RECEIPT_OBSERVATION_MS) return;
777
+ this.awaitingReceiptObservedAtMs.set(task.taskId, this.deps.nowMs());
778
+ }
779
+ if (transportFailure
780
+ && TASK_TARGET_SPACED_FAILURE_CODES.has(transportFailure.code)
781
+ && this.deps.nowMs() - transportFailure.observedAtMs < taskTargetRetryDelayMs(transportFailure.streak)) return;
782
+ if (task.conversationId.startsWith('bootstrap:')) { await this.bootstrapTask(task); return; }
783
+ let poll = this.control.browserPoll({ conversationId: task.conversationId, conversationUrl: task.conversationUrl });
784
+ if (poll.terminal) return;
785
+ if (poll.command) this.lastCommandAttemptAtMs = this.deps.nowMs();
786
+ // A turn whose bounded resume is exhausted keeps one slow read-only probe so
787
+ // late evidence can still land, without polling an unchanged surface at the
788
+ // active tick rate and without minting any new provider turn.
789
+ if (!poll.command && this.control.taskStall(task.taskId).state === 'provider_resume_exhausted') {
790
+ const observedAt = this.awaitingReceiptObservedAtMs.get(task.taskId) ?? 0;
791
+ if (this.deps.nowMs() - observedAt < AWAITING_RECEIPT_OBSERVATION_MS) return;
792
+ this.awaitingReceiptObservedAtMs.set(task.taskId, this.deps.nowMs());
793
+ }
794
+ // Acquiring a conversation the inventory does not already expose is a
795
+ // provider navigation/history read, not local observation. The ChatGPT rate
796
+ // limit is account-wide, so while explicit backpressure is active the answer
797
+ // is strictly fewer requests: keep an already-retained surface observable and
798
+ // never open another one. Cooldown is transient, so this defers rather than
799
+ // strands the task.
800
+ if (chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs()) > 0
801
+ && !this.conversations.some((conversation) => conversation.conversation_id === task.conversationId)) return;
802
+ // Only a new, explicitly admitted send may open a missing tab. A late
803
+ // receipt probe or unknown-effect reconciliation uses existing attachments
804
+ // and never resurrects a manually closed conversation by observation.
805
+ const ensured = await this.deps.targetPort.ensureExact(identityForTask(task), {
806
+ createIfMissing: poll.command?.mode === 'send',
807
+ });
808
+ if (ensured.state !== 'ready') { this.noteTargetUnavailable(task, poll.command?.effectId, ensured.failure); return; }
809
+ const target = ensured.target;
810
+ const snapshot = ensured.observation ?? await target.observe({ includeUserHistory: false, includePageText: false });
811
+ if (!exactConversation(snapshot, task)) { this.noteTargetUnavailable(task, poll.command?.effectId, { code: 'WORKFLOW_SUPERVISOR_EXACT_CONVERSATION_UNPROVEN' }); return; }
812
+ if (!conversationContentAvailable(snapshot)) { this.noteTargetUnavailable(task, poll.command?.effectId, { code: 'COMPUTER_CHATGPT_CONVERSATION_CONTENT_UNAVAILABLE' }); return; }
813
+ // Do not clear a pre-send failure merely because a conversation loaded:
814
+ // only a successful provider preparation proves the obstacle is gone.
815
+ if (transportFailure?.code !== SUPERVISOR_PRE_SEND_BLOCKED) this.clearTaskFailure(task.taskId, poll.command?.effectId);
816
+ this.conversations.push({ conversation_id: task.conversationId, canonical_url: task.conversationUrl,
817
+ ...(snapshot.title.trim() ? { title: snapshot.title.trim().slice(0, 512) } : {}), ...projectMetadataFromConversationUrl(snapshot.url) });
818
+ const providerBusy = snapshot.isGenerating;
819
+ // Recovery-exhausted tasks can otherwise remain visibly stalled without a
820
+ // new exception. Three spaced, unchanged exact-page observations trip the
821
+ // same durable fuse; changing assistant activity resets this observation.
822
+ if (!poll.command && this.control.taskStall(task.taskId).state === 'provider_resume_exhausted') {
823
+ const fingerprint = sha256(JSON.stringify({ text: snapshot.latestAssistantResponse,
824
+ activity: snapshot.providerActivityText, failure: snapshot.providerFailureText, generating: providerBusy }));
825
+ const previous = this.stalledProviderObservations.get(task.taskId);
826
+ if (!previous || previous.fingerprint !== fingerprint) {
827
+ this.stalledProviderObservations.set(task.taskId, {
828
+ fingerprint, changedAtMs: this.deps.nowMs(), failedProbeAtMs: this.deps.nowMs(),
829
+ });
830
+ } else if (this.deps.nowMs() - previous.changedAtMs >= 2 * AWAITING_RECEIPT_OBSERVATION_MS
831
+ && this.deps.nowMs() - previous.failedProbeAtMs >= AWAITING_RECEIPT_OBSERVATION_MS
832
+ && chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs()) === 0) {
833
+ previous.failedProbeAtMs = this.deps.nowMs();
834
+ this.control.store.recordBrowserDeliveryAttemptFailure({ taskId: task.taskId,
835
+ conversationId: task.conversationId, code: 'WORKFLOW_SUPERVISOR_PROVIDER_NO_PROGRESS_WATCHDOG' });
836
+ if (this.maybeAutoMigrate(task)) return;
837
+ }
838
+ } else this.stalledProviderObservations.delete(task.taskId);
839
+ const latestRoleStillUser = snapshot.latestTurnRole === 'user';
840
+ // Capacity is only authoritative on dedicated live provider UI. A model
841
+ // can quote the same phrase in an ordinary assistant turn; historical
842
+ // transcript text must never trigger automatic conversation migration.
843
+ const liveFailure = chatgptProviderPageFailure(snapshot.providerFailureText);
844
+ const turnFailure = chatgptProviderPageFailure(snapshot.providerActivityText);
845
+ const observedProviderFailureCode = liveFailure
846
+ ?? (turnFailure === CHATGPT_AUTOMATION_CONVERSATION_LIMIT_REACHED ? undefined : turnFailure);
847
+ const providerFailureAwaitingClear = this.providerFailureAwaitingClear.get(task.conversationId);
848
+ if (!observedProviderFailureCode) this.providerFailureAwaitingClear.delete(task.conversationId);
849
+ const providerFailureCode = observedProviderFailureCode && observedProviderFailureCode !== providerFailureAwaitingClear
850
+ ? observedProviderFailureCode
851
+ : undefined;
852
+ if (observedProviderFailureCode === CHATGPT_AUTOMATION_CONVERSATION_LIMIT_REACHED) {
853
+ // This conversation cannot produce another meaningful turn. Unlike a
854
+ // provider 429, fresh migration is justified by an explicit terminal
855
+ // capacity message on the exact bound provider surface. The existing
856
+ // Supervisor CAS transaction preserves already-applied source effects.
857
+ this.control.migrateConversation({
858
+ taskId: task.taskId,
859
+ expectedConversationId: task.conversationId,
860
+ fresh: true,
861
+ requestId: `capacity:${task.conversationId}`,
862
+ reason: 'live_provider_conversation_capacity_exhausted',
863
+ authorizedBy: 'user-authorized-automatic-conversation-rotation',
864
+ });
865
+ return;
866
+ }
867
+ const priorProviderFailure = this.providerFailureSeen.get(task.conversationId);
868
+ if (!observedProviderFailureCode) this.providerFailureSeen.delete(task.conversationId);
869
+ else if (priorProviderFailure !== observedProviderFailureCode) { noteChatgptProviderBackpressure(this.deps.providerScopeKey, observedProviderFailureCode, this.deps.nowMs()); this.providerFailureSeen.set(task.conversationId, observedProviderFailureCode); }
870
+ let providerBackpressureMs = chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs());
871
+ if (poll.command?.mode !== 'reconcile' && providerFailureCode === CHATGPT_AUTOMATION_RATE_LIMITED && providerBackpressureMs > 0) return;
872
+ let missingReceiptForAppliedTurn = false;
873
+ // A queued, never-dispatched recovery still leaves its source reply eligible
874
+ // for completion. Consume that reply before preparing an unnecessary Send.
875
+ const unsentRecovery = poll.command?.mode === 'send'
876
+ && poll.command.kind === 'recovery' && poll.command.dispatchGeneration === 1;
877
+ if ((!poll.command || unsentRecovery) && !providerBusy && snapshot.latestTurnRole === 'assistant' && snapshot.latestAssistantResponse.trim()) {
878
+ const responseFingerprint = sha256(snapshot.latestAssistantResponse);
879
+ const pending = this.control.taskStall(task.taskId);
880
+ // Text-only replies are not receipts. Observe their stable idle digest,
881
+ // but avoid generating an identical parser error on every browser tick.
882
+ missingReceiptForAppliedTurn = !hasCommittedSupervisorEnvelope(snapshot.latestAssistantResponse)
883
+ && 'effectId' in pending && Boolean(pending.effectId && targetMarkerPresent(snapshot.latestUserText, pending.effectId));
884
+ if (!missingReceiptForAppliedTurn && this.observedAssistant.get(task.conversationId) !== responseFingerprint) {
885
+ try { await this.control.browserObserveAssistant({ conversationId: task.conversationId, conversationUrl: task.conversationUrl, responseText: snapshot.latestAssistantResponse }); this.observedAssistant.set(task.conversationId, responseFingerprint); }
886
+ catch (error) {
887
+ const message = error instanceof Error ? error.message : String(error);
888
+ if (message.includes('WORKFLOW_SUPERVISOR_') && !message.includes('WORKFLOW_SUPERVISOR_COMPACT_RECEIPT_CHALLENGE_MISMATCH')) this.observedAssistant.set(task.conversationId, responseFingerprint);
889
+ if (!message.includes('WORKFLOW_SUPERVISOR_TASK_TERMINAL')) this.deps.onError(error);
890
+ }
891
+ }
892
+ poll = this.control.browserPoll({ conversationId: task.conversationId, conversationUrl: task.conversationUrl });
893
+ if (poll.terminal) return;
894
+ }
895
+ // A queued continuation must not hide a visible failure from the preceding
896
+ // provider turn. Observe the error first so the existing bounded recovery
897
+ // chain owns it instead of repeatedly attempting the queued send.
898
+ if (!poll.command || providerFailureCode) {
899
+ this.control.browserObserveProviderTurn({ conversationId: task.conversationId, conversationUrl: task.conversationUrl,
900
+ generating: providerFailureCode ? false : providerBusy || latestRoleStillUser, latestAssistantResponse: snapshot.latestAssistantResponse,
901
+ providerActivityText: snapshot.providerActivityText, receiptMissing: missingReceiptForAppliedTurn, providerFailureCode: providerFailureCode === CHATGPT_AUTOMATION_RATE_LIMITED ? undefined : providerFailureCode,
902
+ observedAtMs: this.deps.nowMs(), graceMs: this.deps.providerIdleGraceMs });
903
+ poll = this.control.browserPoll({ conversationId: task.conversationId, conversationUrl: task.conversationUrl });
904
+ const staleTurnRecovery = poll.command?.kind === 'recovery';
905
+ if (providerBusy && !providerFailureCode && !staleTurnRecovery) return;
906
+ if (latestRoleStillUser && !providerFailureCode && !staleTurnRecovery) return;
907
+ }
908
+ providerBackpressureMs = chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs());
909
+ const completedSource = poll.command ? Boolean(this.control.getEffect(poll.command.effectId)?.sourceCompletionFingerprint) : false;
910
+ const staleTurnRecovery = poll.command?.kind === 'recovery';
911
+ const commandMutationBlocked = providerBackpressureMs > 0
912
+ || (providerBusy && !providerFailureCode && !staleTurnRecovery)
913
+ || (latestRoleStillUser && !providerFailureCode && !completedSource && !staleTurnRecovery);
914
+ if (poll.command?.mode === 'send' && commandMutationBlocked) return;
915
+ if (poll.command) {
916
+ if (poll.command.kind === 'recovery' && observedProviderFailureCode) {
917
+ this.providerFailureAwaitingClear.set(task.conversationId, observedProviderFailureCode);
918
+ }
919
+ await this.executeCommand(target, poll.command, task);
920
+ }
921
+ }
922
+
923
+ private async bootstrapTask(task: WorkflowSupervisorBrowserTask): Promise<void> {
924
+ const poll = this.control.bootstrapPoll(task.taskId);
925
+ const command = poll.command;
926
+ if (!command) return;
927
+ if (command.mode === 'reconcile') { await this.reconcileBootstrapTask(task, command); return; }
928
+ if (command.mode !== 'send') return;
929
+ // Bootstrap opens a provider project surface and then submits the first
930
+ // prompt. Both are provider requests, so account-wide backpressure defers the
931
+ // whole attempt instead of racing it against an explicit rate limit.
932
+ if (chatgptProviderBackpressureRemainingMs(this.deps.providerScopeKey, this.deps.nowMs()) > 0) return;
933
+ const opened = await this.deps.targetPort.openBootstrap(this.control.bootstrapProjectUrl(task.taskId), task.taskId);
934
+ if (opened.state !== 'ready') { this.noteTargetUnavailable(task, command.effectId, opened.failure); return; }
935
+ const target = opened.target;
936
+ // Bootstrap must originate on the Project/new-composer surface. A retained
937
+ // provider tab can silently reopen the exhausted /c/<id>; sending there
938
+ // burns a fresh effect and can make the migration bind its old source.
939
+ // Reject before beginning an external effect, release the stale target and
940
+ // retain the same unspent enrollment for a later exact-target attempt.
941
+ const beforeSend = opened.observation ?? await target.observe({ includeUserHistory: false, includePageText: false });
942
+ let preexistingConversation = false;
943
+ try { parseChatgptConversationIdentity(beforeSend.url); preexistingConversation = true; }
944
+ catch { /* A Project page without a conversation id is the expected entry. */ }
945
+ if (preexistingConversation) {
946
+ await this.deps.targetPort.release(target.targetId).catch(() => undefined);
947
+ this.noteTargetUnavailable(task, command.effectId, {
948
+ code: 'WORKFLOW_SUPERVISOR_BOOTSTRAP_EXISTING_CONVERSATION',
949
+ });
950
+ return;
951
+ }
952
+ if (preflightComposerObservation(beforeSend, command.effectId).classification === 'unattributed'
953
+ && beforeSend.composerReplacementContract !== 'exact-backed-up-project-draft-v1') {
954
+ // Not a draft conflict or a Send attempt: Chrome retained a stale content script.
955
+ this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId,
956
+ reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_LOADED_SCRIPT_STALE',
957
+ composerObservation: preflightComposerObservation(beforeSend, command.effectId) });
958
+ this.noteTaskFailure(SUPERVISOR_PRE_SEND_BLOCKED, task, command.effectId);
959
+ return;
960
+ }
961
+ let prepared: Awaited<ReturnType<ComputerChatgptConversationTarget['prepare']>>;
962
+ const exactReplacement = authorizedDraftReplacementPrompt(this.deps.providerScopeKey, task.taskId,
963
+ command.effectId, this.control.bootstrapProjectUrl(task.taskId), beforeSend, command.prompt);
964
+ const retiredReplacement = exactReplacement ? undefined
965
+ : retiredBootstrapDraftReplacementPrompt(this.control, task, beforeSend, command.prompt);
966
+ try {
967
+ prepared = await target.prepare(exactReplacement ?? retiredReplacement ?? command.prompt);
968
+ if (!prepared.ready && prepared.reasonCode === 'composer_not_empty'
969
+ && !exactReplacement && !retiredReplacement) {
970
+ // ChatGPT may hydrate an old Project draft after the initial tab-ready
971
+ // observation. A refused prepare never clicked Send. Read the SAME
972
+ // pinned target once more and offer a second prepare only when the
973
+ // STOPPED effect's canonical ledger proves this exact editor safe.
974
+ const hydrated = await target.observe({ includeUserHistory: false, includePageText: false }).catch(() => undefined);
975
+ const verifiedRetirement = hydrated
976
+ ? retiredBootstrapDraftReplacementPrompt(this.control, task, hydrated, command.prompt) : undefined;
977
+ if (verifiedRetirement) prepared = await target.prepare(verifiedRetirement);
978
+ }
979
+ } catch (error) {
980
+ this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId, reasonCode: consumerFailureCode(error, 'COMPUTER_PREPARE_FAILED'), composerObservation: preflightComposerObservation(beforeSend, command.effectId) });
981
+ this.noteTaskFailure(SUPERVISOR_PRE_SEND_BLOCKED, task, command.effectId); this.deps.onError(error); return;
982
+ }
983
+ if (!prepared.ready) {
984
+ const observed = await target.observe({ includeUserHistory: false, includePageText: false }).catch(() => beforeSend);
985
+ if (prepared.reasonCode === 'composer_not_empty') {
986
+ // Snapshot backups require an exact, separately authorized fingerprint.
987
+ backupAuthorizedBlockedComposer(this.deps.providerScopeKey, task.taskId, command.effectId,
988
+ this.control.bootstrapProjectUrl(task.taskId), observed);
989
+ }
990
+ this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId, reasonCode: prepared.reasonCode, composerObservation: preflightComposerObservation(observed, command.effectId) });
991
+ this.noteTaskFailure(SUPERVISOR_PRE_SEND_BLOCKED, task, command.effectId);
992
+ this.deps.onError(new Error(prepared.reasonCode));
993
+ return;
994
+ }
995
+ this.clearTaskFailure(task.taskId, command.effectId);
996
+ if (!this.control.bootstrapBeginEffect({ taskId: task.taskId, effectId: command.effectId, dispatchId: `bootstrap-${randomUUID()}`, dispatchGeneration: command.dispatchGeneration })) return;
997
+ try {
998
+ const dispatch = await withChatgptProviderDispatchLane(this.deps.providerScopeKey, () => target.dispatch(command.prompt),
999
+ (result) => result.mutation === 'attempted' ? { providerAccepted: result.confirmed === true } : { code: result.reasonCode, message: result.reasonCode });
1000
+ if (dispatch.mutation === 'not_attempted') {
1001
+ this.control.bootstrapObserveEffect({ taskId: task.taskId, effectId: command.effectId, observationId: `bootstrap-${randomUUID()}`, outcome: 'not_applied', evidence: { dispatch_mutation: 'not_attempted', send_clicked: false, reason: dispatch.reasonCode } });
1002
+ this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId,
1003
+ reasonCode: dispatch.reasonCode ?? 'COMPUTER_CHATGPT_DISPATCH_NOT_ATTEMPTED', composerObservation: preflightComposerObservation(beforeSend, command.effectId) });
1004
+ await this.deps.targetPort.release(target.targetId).catch(() => undefined);
1005
+ return;
1006
+ }
1007
+ let reason = 'bootstrap_outbound_not_confirmed';
1008
+ for (let attempt = 1; attempt <= MAX_LOCAL_OBSERVATION_ATTEMPTS; attempt += 1) {
1009
+ try {
1010
+ const snapshot = await target.observe({ includeUserHistory: true, includePageText: false });
1011
+ const identity = parseChatgptConversationIdentity(snapshot.url);
1012
+ if (!snapshotTargetMarkerPresent(snapshot, command.effectId)) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_EFFECT_MARKER_NOT_OBSERVED');
1013
+ const promoted = await this.deps.targetPort.promoteBootstrap(target.targetId, { namespace: 'chatgpt.conversation', conversationId: identity.conversationId, canonicalUrl: identity.canonicalUrl });
1014
+ if (promoted.state !== 'ready') throw new Error(promoted.failure.code);
1015
+ this.control.bindBootstrapConversation({ taskId: task.taskId, conversationId: identity.conversationId, conversationUrl: identity.canonicalUrl });
1016
+ this.control.bootstrapObserveEffect({ taskId: task.taskId, effectId: command.effectId, observationId: `bootstrap-${randomUUID()}`, outcome: 'applied' });
1017
+ this.clearTaskFailure(task.taskId, command.effectId);
1018
+ return;
1019
+ } catch (error) { reason = error instanceof Error ? error.message : String(error); if (attempt < MAX_LOCAL_OBSERVATION_ATTEMPTS) await this.deps.sleep(localObservationDelayMs(attempt, 1_000, 4_000)); }
1020
+ }
1021
+ this.control.bootstrapObserveEffect({ taskId: task.taskId, effectId: command.effectId, observationId: `bootstrap-${randomUUID()}`, outcome: 'unknown', evidence: { reconciliation: true, reason } });
1022
+ } catch (error) {
1023
+ this.control.bootstrapObserveEffect({ taskId: task.taskId, effectId: command.effectId, observationId: `bootstrap-${randomUUID()}`, outcome: 'unknown', evidence: { reconciliation: true, reason: error instanceof Error ? error.message : String(error) } });
1024
+ throw error;
1025
+ }
1026
+ }
1027
+
1028
+ private async reconcileBootstrapTask(task: WorkflowSupervisorBrowserTask, command: WorkflowSupervisorBrowserCommand): Promise<void> {
1029
+ const matches = await this.deps.targetPort.findBySubmittedMarker(
1030
+ renderEffectMarker(command.effectId),
1031
+ task.taskId,
1032
+ async () => { await this.serviceFreshSend(task.taskId); },
1033
+ );
1034
+ const ready = matches.filter((result): result is Extract<typeof result, { state: 'ready' }> => result.state === 'ready');
1035
+ const observeUnknown = (reason: string): void => this.control.bootstrapObserveEffect({ taskId: task.taskId, effectId: command.effectId, observationId: `bootstrap-reconcile-${randomUUID()}`, outcome: 'unknown', evidence: { reconciliation: true, reason } });
1036
+ if (ready.length === 0) { observeUnknown('bootstrap_target_not_observed'); return; }
1037
+ if (ready.length !== 1) { observeUnknown('bootstrap_reconcile_ambiguous'); return; }
1038
+ const match = ready[0]!;
1039
+ const snapshot = match.observation ?? await match.target.observe({ includeUserHistory: true, includePageText: false });
1040
+ let identity;
1041
+ try { identity = parseChatgptConversationIdentity(snapshot.url); } catch { observeUnknown('bootstrap_conversation_identity_unavailable'); return; }
1042
+ if (!snapshotTargetMarkerPresent(snapshot, command.effectId)) { observeUnknown('bootstrap_effect_marker_not_observed'); return; }
1043
+ // Marker recovery may already return a Computer target with this exact
1044
+ // conversation identity. Promoting it again would tombstone its own binding.
1045
+ // Only a still-bootstrap target needs the provider's identity transition.
1046
+ if (match.target.identity.namespace === 'chatgpt.conversation') {
1047
+ if (match.target.identity.conversationId !== identity.conversationId) {
1048
+ observeUnknown('bootstrap_reconcile_target_identity_mismatch'); return;
1049
+ }
1050
+ } else {
1051
+ const promoted = await this.deps.targetPort.promoteBootstrap(match.target.targetId, { namespace: 'chatgpt.conversation', conversationId: identity.conversationId, canonicalUrl: identity.canonicalUrl });
1052
+ if (promoted.state !== 'ready') { observeUnknown(promoted.failure.code); return; }
1053
+ }
1054
+ this.control.bindBootstrapConversation({ taskId: task.taskId, conversationId: identity.conversationId, conversationUrl: identity.canonicalUrl });
1055
+ this.control.bootstrapObserveEffect({ taskId: task.taskId, effectId: command.effectId, observationId: `bootstrap-reconcile-${randomUUID()}`, outcome: 'applied' });
1056
+ this.clearTaskFailure(task.taskId, command.effectId);
1057
+ }
1058
+
1059
+ private async executeCommand(target: ComputerChatgptConversationTarget, command: WorkflowSupervisorBrowserCommand, task: WorkflowSupervisorBrowserTask): Promise<void> {
1060
+ let snapshot = await target.observe({ includeUserHistory: true, includePageText: true });
1061
+ if (!exactConversation(snapshot, task) || !conversationContentAvailable(snapshot)) {
1062
+ this.noteTargetUnavailable(task, command.effectId, { code: 'COMPUTER_CHATGPT_CONVERSATION_CONTENT_UNAVAILABLE' });
1063
+ return;
1064
+ }
1065
+ if (command.mode === 'send' && snapshot.isGenerating && command.kind !== 'recovery') return;
1066
+ let mode = command.mode;
1067
+ if (mode === 'send') {
1068
+ let prepared: Awaited<ReturnType<ComputerChatgptConversationTarget['prepare']>>;
1069
+ try { prepared = await target.prepare(command.prompt, {
1070
+ ...(command.kind === 'recovery' ? { mode: 'recover' as const } : {}),
1071
+ }); }
1072
+ catch (error) {
1073
+ this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId, reasonCode: consumerFailureCode(error, 'COMPUTER_PREPARE_FAILED') });
1074
+ this.noteTaskFailure(SUPERVISOR_PRE_SEND_BLOCKED, task, command.effectId); this.deps.onError(error); return;
1075
+ }
1076
+ if (!prepared.ready) {
1077
+ // The exact recovery target still exposes its Stop control. That is a
1078
+ // provider-turn observation, not an infrastructure preparation failure:
1079
+ // preserve this one reserved recovery effect without spending a send
1080
+ // generation or the bounded preflight-failure budget.
1081
+ if (command.kind === 'recovery' && prepared.reasonCode === 'provider_recovery_stop_pending') return;
1082
+ this.control.recordProviderPreparationFailure({ taskId: task.taskId, effectId: command.effectId, reasonCode: prepared.reasonCode });
1083
+ this.noteTaskFailure(SUPERVISOR_PRE_SEND_BLOCKED, task, command.effectId);
1084
+ this.deps.onError(new Error(prepared.reasonCode));
1085
+ return;
1086
+ }
1087
+ this.clearTaskFailure(task.taskId, command.effectId);
1088
+ const begin = this.control.browserBeginEffect({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
1089
+ dispatchId: `computer-${randomUUID()}`, dispatchGeneration: command.dispatchGeneration,
1090
+ evidence: { surface: 'computer-chatgpt-target', target_id: target.targetId, latest_user_text: snapshot.latestUserText, latest_assistant_response: snapshot.latestAssistantResponse } });
1091
+ if (!begin.started) mode = 'reconcile';
1092
+ }
1093
+ if (mode === 'reconcile') {
1094
+ const exact = normalize(snapshot.latestUserText) === normalize(command.prompt.replace(/^@forge\s+(?=<<<FORGE_WORKFLOW_EFFECT_V1:(?:fx|crpe)_[a-zA-Z0-9_-]{8,120}>>>)/, ''));
1095
+ const markerPresent = snapshotTargetMarkerPresent(snapshot, command.effectId);
1096
+ if (exact || markerPresent) {
1097
+ this.control.browserObserveEffect({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
1098
+ observationId: `computer-observe-${randomUUID()}`, outcome: 'applied',
1099
+ evidence: { surface: 'computer-chatgpt-target', target_id: target.targetId, exact_user_message: exact, reconciliation: true, target_marker_present: markerPresent } });
1100
+ return;
1101
+ }
1102
+ const reason = 'submission_not_observed';
1103
+ this.control.browserObserveEffect({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
1104
+ observationId: `computer-observe-${randomUUID()}`, outcome: 'unknown',
1105
+ evidence: { surface: 'computer-chatgpt-target', target_id: target.targetId, reconciliation: true, target_marker_present: false, reason,
1106
+ observation_fingerprint: unknownObservationFingerprint(command.effectId, reason, snapshot) } });
1107
+ return;
1108
+ }
1109
+ let dispatch: Awaited<ReturnType<ComputerChatgptConversationTarget['dispatch']>>;
1110
+ try {
1111
+ dispatch = await withChatgptProviderDispatchLane(this.deps.providerScopeKey, () => target.dispatch(command.prompt, { ...(command.kind === 'recovery' ? { mode: 'recover' as const } : {}) }),
1112
+ (result) => result.mutation === 'attempted' ? { providerAccepted: result.confirmed === true } : { code: result.reasonCode, message: result.reasonCode });
1113
+ } catch (error) {
1114
+ this.control.browserObserveEffect({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
1115
+ observationId: `computer-observe-${randomUUID()}`, outcome: 'unknown',
1116
+ evidence: { surface: 'computer-chatgpt-target', target_id: target.targetId, reconciliation: true, reason: consumerFailureCode(error, 'provider_dispatch_exception') } });
1117
+ throw error;
1118
+ }
1119
+ if (dispatch.mutation === 'not_attempted') {
1120
+ this.control.browserObserveDispatchFailure({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
1121
+ observationId: `computer-observe-${randomUUID()}`, dispatchGeneration: command.dispatchGeneration, reason: dispatch.reasonCode });
1122
+ return;
1123
+ }
1124
+ if (dispatch.confirmed) {
1125
+ this.control.browserObserveEffect({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
1126
+ observationId: `computer-observe-${randomUUID()}`, outcome: 'applied', evidence: { surface: 'computer-chatgpt-target', target_id: target.targetId, provider_confirmed: true } });
1127
+ return;
1128
+ }
1129
+ let exact = false; let markerPresent = false;
1130
+ for (let attempt = 1; attempt <= MAX_LOCAL_OBSERVATION_ATTEMPTS; attempt += 1) {
1131
+ snapshot = await target.observe({ includeUserHistory: true, includePageText: true });
1132
+ exact = normalize(snapshot.latestUserText) === normalize(command.prompt.replace(/^@forge\s+(?=<<<FORGE_WORKFLOW_EFFECT_V1:(?:fx|crpe)_[a-zA-Z0-9_-]{8,120}>>>)/, '')); markerPresent = snapshotTargetMarkerPresent(snapshot, command.effectId);
1133
+ if (exact || markerPresent || attempt >= MAX_LOCAL_OBSERVATION_ATTEMPTS) break;
1134
+ await this.deps.sleep(localObservationDelayMs(attempt, 1_000, 4_000));
1135
+ }
1136
+ this.control.browserObserveEffect({ conversationId: command.conversationId, conversationUrl: command.conversationUrl, effectId: command.effectId,
1137
+ observationId: `computer-observe-${randomUUID()}`, outcome: exact || markerPresent ? 'applied' : 'unknown',
1138
+ evidence: { surface: 'computer-chatgpt-target', target_id: target.targetId, exact_user_message: exact, target_marker_present: markerPresent,
1139
+ ...(!exact && !markerPresent ? { reason: 'outbound_not_confirmed', observation_fingerprint: unknownObservationFingerprint(command.effectId, 'outbound_not_confirmed', snapshot) } : {}) } });
1140
+ }
1141
+ }
1142
+
1143
+ export function startWorkflowSupervisorNativeBrowserAdapter(
1144
+ control: WorkflowSupervisorControlPlane,
1145
+ discovery: WorkflowSupervisorEphemeralDiscovery,
1146
+ dependencies: WorkflowSupervisorComputerBrowserDependencies,
1147
+ ): WorkflowSupervisorNativeBrowserHandle {
1148
+ const adapter = new WorkflowSupervisorNativeBrowserAdapter(control, discovery, dependencies);
1149
+ adapter.start();
1150
+ return { adapter, status: () => adapter.status(), close: async () => { await adapter.close(); } };
1151
+ }