@moretea-labs/forge 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -6,9 +6,25 @@ const PROVIDER_ID = 'browser.chrome-extension';
6
6
  const PROVIDER_TICK_MS = 750;
7
7
  const DISCOVERY_MIN_INTERVAL_MS = 2_000;
8
8
  const RPC_TIMEOUT_MS = 30_000;
9
+ // The broker budgets 12 seconds for a claimed command; a hung content
10
+ // callback must settle earlier without claiming a Send never happened.
11
+ const TAB_MESSAGE_TIMEOUT_MS = 7_000;
9
12
  const bootstrapTabs = new Map();
10
13
  const randomId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
11
- const PROVIDER_INSTANCE_ID = randomId();
14
+ let providerInstancePromise;
15
+ function providerInstanceId() {
16
+ // Profile-local storage survives worker and browser restarts. It identifies
17
+ // this Chrome profile, never a task or an outbound effect.
18
+ // A fresh worker UUID would invalidate every Controller-pinned tab.
19
+ providerInstancePromise ??= (async () => {
20
+ const saved = await chrome.storage.local.get('forgeComputerProviderInstanceId');
21
+ if (typeof saved.forgeComputerProviderInstanceId === 'string' && saved.forgeComputerProviderInstanceId) return saved.forgeComputerProviderInstanceId;
22
+ const instanceId = randomId();
23
+ await chrome.storage.local.set({ forgeComputerProviderInstanceId: instanceId });
24
+ return instanceId;
25
+ })().catch((error) => { providerInstancePromise = undefined; throw error; });
26
+ return providerInstancePromise;
27
+ }
12
28
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
29
  function browserProduct() {
14
30
  const ua = String(globalThis.navigator?.userAgent ?? '');
@@ -56,11 +72,24 @@ function nativeRpc(method, params = {}) {
56
72
  });
57
73
  }
58
74
  function tabMessage(tabId, message) {
59
- return new Promise((resolve, reject) => chrome.tabs.sendMessage(tabId, message, (response) => {
60
- const error = chrome.runtime.lastError;
61
- if (error) { reject(new Error(error.message)); return; }
62
- resolve(response ?? {});
63
- }));
75
+ return new Promise((resolve, reject) => {
76
+ let settled = false;
77
+ const finish = (error, response) => {
78
+ if (settled) return;
79
+ settled = true;
80
+ clearTimeout(timer);
81
+ if (error) reject(error); else resolve(response ?? {});
82
+ };
83
+ const timer = setTimeout(() => finish(new Error('COMPUTER_CHATGPT_EXTENSION_TAB_MESSAGE_TIMEOUT')), TAB_MESSAGE_TIMEOUT_MS);
84
+ try {
85
+ chrome.tabs.sendMessage(tabId, message, (response) => {
86
+ // Read lastError inside the callback, even if the request timed out,
87
+ // so Chrome does not report an unhandled transport error.
88
+ const error = chrome.runtime.lastError;
89
+ finish(error ? new Error(error.message) : undefined, response);
90
+ });
91
+ } catch (error) { finish(error); }
92
+ });
64
93
  }
65
94
  function tabGet(tabId) {
66
95
  return new Promise((resolve, reject) => chrome.tabs.get(tabId, (tab) => {
@@ -76,10 +105,27 @@ function tabCreate(url) {
76
105
  resolve(tab);
77
106
  }));
78
107
  }
79
- function tabRemove(tabId) { return new Promise((resolve) => chrome.tabs.remove(tabId, () => resolve())); }
108
+ function tabRemove(tabId) { return new Promise((resolve, reject) => chrome.tabs.remove(tabId, () => {
109
+ const error = chrome.runtime.lastError;
110
+ if (error) reject(new Error(error.message)); else resolve();
111
+ })); }
80
112
  function tabReload(tabId) { return new Promise((resolve) => chrome.tabs.reload(tabId, () => resolve())); }
81
113
  async function chatgptTabs() { return await chrome.tabs.query({ url: 'https://chatgpt.com/*' }); }
82
- function binding(tab, instanceId) {
114
+ // Controller binding owns the durable source identity. This browser-session
115
+ // attestation proves that the exact physical tab was created by this extension;
116
+ // browser restarts invalidate it rather than permitting tab-id reuse to close
117
+ // an unrelated restored/user tab.
118
+ function ownedTabKey(tabId) { return `forgeComputerOwnedTabV1:${tabId}`; }
119
+ async function attestedOwnerToken(tab, pinnedBinding) {
120
+ if (!pinnedBinding?.ownerToken || String(pinnedBinding.tabId) !== String(tab.id)
121
+ || String(pinnedBinding.windowId) !== String(tab.windowId)) return;
122
+ try {
123
+ const key = ownedTabKey(tab.id);
124
+ const stored = await chrome.storage.session.get(key);
125
+ if (stored[key] === pinnedBinding.ownerToken) return pinnedBinding.ownerToken;
126
+ } catch { /* missing browser-session provenance is never closure authority */ }
127
+ }
128
+ function binding(tab, instanceId, ownerToken) {
83
129
  return {
84
130
  providerId: PROVIDER_ID,
85
131
  providerSessionId: instanceId,
@@ -87,27 +133,72 @@ function binding(tab, instanceId) {
87
133
  browserProduct: browserProduct(),
88
134
  windowId: String(tab.windowId),
89
135
  tabId: String(tab.id),
136
+ ...(ownerToken ? { ownerToken } : {}),
90
137
  };
91
138
  }
92
139
  async function snapshot(tabId, options = {}) {
93
140
  return await tabMessage(tabId, { type: 'forge-computer-chatgpt-snapshot', options });
94
141
  }
95
- async function waitForContent(tabId) {
142
+ async function restoreExactContentReceiver(tabId, identity) {
143
+ // The physical tab is already resolved by the one Computer target authority.
144
+ // A missing MV3 message receiver is not permission to reload/navigate that tab,
145
+ // replace its draft, or create another tab. Inject only read-only listeners on
146
+ // this SAME proven conversation/Project, then verify its identity again.
147
+ const matches = (tab) => identity?.namespace === 'chatgpt.conversation'
148
+ ? core.parseConversation(tab.url ?? '')?.conversationId === identity.conversationId
149
+ : identity?.namespace === 'chatgpt.bootstrap'
150
+ && core.projectId(tab.url ?? '') === core.projectId(identity.projectUrl)
151
+ && !core.parseConversation(tab.url ?? '');
152
+ const before = await tabGet(tabId);
153
+ if (before.discarded || !matches(before)) throw new Error('COMPUTER_CHATGPT_EXTENSION_RECEIVER_TARGET_CHANGED');
154
+ await new Promise((resolve, reject) => chrome.scripting.executeScript({
155
+ target: { tabId }, files: ['core.js', 'content.js'],
156
+ }, () => {
157
+ const error = chrome.runtime.lastError;
158
+ if (error) reject(new Error(error.message)); else resolve();
159
+ }));
160
+ const after = await tabGet(tabId);
161
+ if (!matches(after)) throw new Error('COMPUTER_CHATGPT_EXTENSION_RECEIVER_TARGET_CHANGED');
162
+ }
163
+ async function waitForContent(tabId, requireBootstrapUi = false, recoverReceiver) {
96
164
  let lastError;
97
- for (let attempt = 0; attempt < 40; attempt += 1) {
165
+ let recoveryAttempted = false;
166
+ // Bootstrap is a new provider page: seeing its URL proves location, not
167
+ // that the React composer is hydrated. The Supervisor uses the user's
168
+ // exact target page readiness. Bound this wait below the
169
+ // broker's 12-second timeout; missing composer never authorizes Send.
170
+ const attempts = requireBootstrapUi ? 20 : 40;
171
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
98
172
  try {
99
173
  const tab = await tabGet(tabId);
100
174
  if (tab?.discarded) await tabReload(tabId);
101
175
  const observation = await snapshot(tabId);
102
- if (typeof observation?.url === 'string' && observation.url) return observation;
103
- } catch (error) { lastError = error; }
176
+ if (typeof observation?.url === 'string' && observation.url
177
+ && (!requireBootstrapUi || typeof observation.composerText === 'string')) return observation;
178
+ } catch (error) {
179
+ lastError = error;
180
+ if (!recoveryAttempted && recoverReceiver
181
+ && /Receiving end does not exist|Could not establish connection/i.test(String(error?.message ?? error))) {
182
+ recoveryAttempted = true;
183
+ // Reattach the receiver only after exact tab/Project revalidation.
184
+ // Never retry the original Send: this is an observation-only preflight.
185
+ await recoverReceiver();
186
+ }
187
+ }
104
188
  await sleep(250);
105
189
  }
106
190
  throw lastError ?? new Error('COMPUTER_CHATGPT_TAB_NOT_READY');
107
191
  }
108
192
  function matchingTabs(tabs, identity, pinnedBinding) {
109
193
  if (identity?.namespace === 'chatgpt.conversation') {
110
- return tabs.filter((tab) => core.parseConversation(tab.url ?? '')?.conversationId === identity.conversationId);
194
+ const exact = tabs.filter((tab) => core.parseConversation(tab.url ?? '')?.conversationId === identity.conversationId);
195
+ if (pinnedBinding?.providerId === PROVIDER_ID && pinnedBinding.tabId && pinnedBinding.windowId) {
196
+ const pinned = exact.filter((tab) => String(tab.id) === String(pinnedBinding.tabId));
197
+ if (pinned.length === 1) return pinned;
198
+ }
199
+ // Duplicate exact URLs without a verifiable pin must never select an
200
+ // arbitrary foreground tab (or silently create another one).
201
+ return exact;
111
202
  }
112
203
  if (identity?.namespace === 'chatgpt.bootstrap') {
113
204
  // The Project is a scope, not a tab identity. Multiple tasks share the
@@ -117,8 +208,7 @@ function matchingTabs(tabs, identity, pinnedBinding) {
117
208
  const belongsToProject = (tab) => String(tab.url ?? '') === identity.projectUrl
118
209
  || Boolean(projectId && core.projectId(tab.url ?? '') === projectId);
119
210
  if (pinnedBinding?.providerId === PROVIDER_ID && pinnedBinding.tabId && pinnedBinding.windowId) {
120
- const exact = tabs.filter((tab) => String(tab.id) === String(pinnedBinding.tabId)
121
- && String(tab.windowId) === String(pinnedBinding.windowId));
211
+ const exact = tabs.filter((tab) => String(tab.id) === String(pinnedBinding.tabId));
122
212
  if (exact.length && !belongsToProject(exact[0])) throw new Error('COMPUTER_CHATGPT_BOOTSTRAP_PINNED_TAB_IDENTITY_CHANGED');
123
213
  return exact;
124
214
  }
@@ -129,12 +219,28 @@ function matchingTabs(tabs, identity, pinnedBinding) {
129
219
  }
130
220
  async function resolveTarget(identity, createIfMissing, pinnedBinding) {
131
221
  const tabs = await chatgptTabs();
222
+ if (identity?.namespace === 'chatgpt.conversation' && pinnedBinding?.providerId === PROVIDER_ID && pinnedBinding.tabId) {
223
+ // Window moves preserve Chrome's profile-local tab id. A vanished id may
224
+ // be recovered by exact conversation, but an existing tab at another URL
225
+ // is identity drift and must never be silently replaced.
226
+ let pinned;
227
+ try { pinned = await tabGet(Number(pinnedBinding.tabId)); }
228
+ catch (error) {
229
+ if (!/No tab with id/i.test(String(error?.message ?? error))) throw error;
230
+ }
231
+ if (pinned) {
232
+ if (core.parseConversation(pinned.url ?? '')?.conversationId !== identity.conversationId) {
233
+ throw new Error('COMPUTER_CHATGPT_EXTENSION_PINNED_TAB_IDENTITY_CHANGED');
234
+ }
235
+ return { tab: pinned, created: false, ownerToken: await attestedOwnerToken(pinned, pinnedBinding) };
236
+ }
237
+ }
132
238
  const matches = matchingTabs(tabs, identity, pinnedBinding);
133
239
  if (matches.length > 1) throw new Error('COMPUTER_CHATGPT_EXTENSION_EXACT_TARGET_AMBIGUOUS');
134
240
  if (matches.length === 1) {
135
241
  const tab = matches[0];
136
242
  if (identity.namespace === 'chatgpt.bootstrap') bootstrapTabs.set(identity.bootstrapKey, tab.id);
137
- return { tab, created: false };
243
+ return { tab, created: false, ownerToken: await attestedOwnerToken(tab, pinnedBinding) };
138
244
  }
139
245
  if (identity?.namespace === 'chatgpt.bootstrap' && pinnedBinding?.providerId === PROVIDER_ID
140
246
  && pinnedBinding.tabId && pinnedBinding.windowId) {
@@ -144,9 +250,14 @@ async function resolveTarget(identity, createIfMissing, pinnedBinding) {
144
250
  const url = identity.namespace === 'chatgpt.conversation' ? identity.canonicalUrl : identity.projectUrl;
145
251
  const tab = await tabCreate(url);
146
252
  if (identity.namespace === 'chatgpt.bootstrap') bootstrapTabs.set(identity.bootstrapKey, tab.id);
147
- return { tab, created: true };
253
+ const token = randomId();
254
+ let ownerToken;
255
+ try {
256
+ await chrome.storage.session.set({ [ownedTabKey(tab.id)]: token });
257
+ ownerToken = token;
258
+ } catch { /* still preserve the created tab's exact binding, but never auto-close without provenance */ }
259
+ return { tab, created: true, ownerToken };
148
260
  }
149
- function normalize(value) { return core.normalizeText(value); }
150
261
  function dispatchTransportNotReached(error) {
151
262
  const text = String(error?.message ?? error);
152
263
  return /Receiving end does not exist|Could not establish connection|No tab with id/i.test(text);
@@ -155,15 +266,25 @@ async function executeEnsure(command, instanceId) {
155
266
  let target;
156
267
  try {
157
268
  target = await resolveTarget(command.identity, true, command.providerBinding);
158
- const observation = await waitForContent(target.tab.id);
269
+ // Created and already-bound tabs share one exact-identity receiver repair.
270
+ // The editor must hydrate before 'ensured'; on failure the existing typed
271
+ // failed receipt still carries this tab's binding without permitting Send.
272
+ const observation = await waitForContent(target.tab.id, command.identity.namespace === 'chatgpt.bootstrap',
273
+ () => restoreExactContentReceiver(target.tab.id, command.identity));
159
274
  const refreshed = await tabGet(target.tab.id);
160
- return { kind: 'ensured', providerBinding: binding(refreshed, instanceId), observation };
275
+ return { kind: 'ensured', providerBinding: binding(refreshed, instanceId, target.ownerToken), observation };
161
276
  } catch (error) {
162
277
  return {
163
278
  kind: 'failed',
164
279
  code: String(error?.message ?? error).split(':')[0] || 'COMPUTER_CHATGPT_EXTENSION_ENSURE_FAILED',
165
280
  retryable: true,
166
- failoverSafe: target?.created !== true,
281
+ // A hydration failure must retain the created tab's physical attachment.
282
+ // This receipt grants neither a ready page nor permission to Send.
283
+ ...(target ? { providerBinding: binding(target.tab, instanceId, target.ownerToken) } : {}),
284
+ // Bootstrap tabs can exist before this command even when no new tab was
285
+ // created now. A worker failure cannot prove that creating a different
286
+ // native tab would preserve the one-task/one-tab binding.
287
+ failoverSafe: false,
167
288
  };
168
289
  }
169
290
  }
@@ -173,76 +294,76 @@ async function executeObserve(command, instanceId) {
173
294
  if (!target) return { kind: 'failed', code: 'COMPUTER_CHATGPT_EXTENSION_TARGET_MISSING', retryable: true, failoverSafe: true };
174
295
  const observation = await snapshot(target.tab.id, command.options ?? {});
175
296
  const refreshed = await tabGet(target.tab.id);
176
- return { kind: 'observation', providerBinding: binding(refreshed, instanceId), observation };
297
+ return { kind: 'observation', providerBinding: binding(refreshed, instanceId, await attestedOwnerToken(refreshed, command.providerBinding)), observation };
177
298
  } catch (error) {
178
299
  return { kind: 'failed', code: String(error?.message ?? error).split(':')[0] || 'COMPUTER_CHATGPT_EXTENSION_OBSERVE_FAILED', retryable: true, failoverSafe: true };
179
300
  }
180
301
  }
181
302
  async function executePrepare(command) {
182
- const target = await resolveTarget(command.identity, false).catch(() => undefined);
303
+ const target = await resolveTarget(command.identity, false, command.providerBinding).catch(() => undefined);
183
304
  if (!target) return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_EXTENSION_TARGET_MISSING' };
184
- if (command.reasoning !== undefined) {
305
+ const expectedProjectId = command.identity?.namespace === 'chatgpt.bootstrap' ? core.projectId(command.identity.projectUrl) : undefined;
306
+ if (command.identity?.namespace === 'chatgpt.bootstrap' && !expectedProjectId)
307
+ return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_PROJECT_UNPROVEN' };
308
+ const requiresDraftReplacement = typeof command.prompt === 'string' && command.prompt.startsWith('<<<FORGE_DRAFT_REPLACE_V1:');
309
+ if (expectedProjectId || requiresDraftReplacement) {
185
310
  let capability;
186
311
  try { capability = await tabMessage(target.tab.id, { type: 'forge-computer-chatgpt-capabilities' }); }
187
- catch { return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_REASONING_PREFLIGHT_UNAVAILABLE' }; }
188
- if (capability?.reasoningPreflight !== 'verified_before_send_v1')
189
- return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_REASONING_PREFLIGHT_UNSUPPORTED' };
312
+ catch { return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_CONTENT_CAPABILITIES_UNAVAILABLE' }; }
313
+ // Older content scripts ignore expectedProjectId. No bootstrap draft/send
314
+ // may cross this version boundary without target verification.
315
+ if (expectedProjectId && capability?.bootstrapTargetGuard !== 'project-checked-before-send-v1')
316
+ return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_PROJECT_GUARD_UNSUPPORTED' };
317
+ if (requiresDraftReplacement && capability?.composerReplacementContract !== 'exact-backed-up-project-draft-v1')
318
+ return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_LOADED_SCRIPT_STALE' };
190
319
  }
191
320
  try {
192
321
  const result = await tabMessage(target.tab.id, {
193
322
  type: 'forge-computer-chatgpt-prepare', prompt: command.prompt, mode: command.mode,
194
- reasoning: command.reasoning,
195
323
  ...(command.identity?.namespace === 'chatgpt.conversation' ? { expectedConversationId: command.identity.conversationId } : {}),
324
+ ...(expectedProjectId ? { expectedProjectId } : {}),
196
325
  });
197
- return result?.prepared === true
198
- ? { kind: 'prepared', ready: true }
199
- : { kind: 'prepared', ready: false, reasonCode: String(result?.reason ?? 'COMPUTER_CHATGPT_EXTENSION_PREPARE_REJECTED') };
326
+ if (result?.prepared !== true) return { kind: 'prepared', ready: false, reasonCode: String(result?.reason ?? 'COMPUTER_CHATGPT_EXTENSION_PREPARE_REJECTED') };
327
+ return { kind: 'prepared', ready: true };
200
328
  } catch {
201
329
  // A prepare command contains no Send action. Failure is not a submitted effect.
202
330
  return { kind: 'prepared', ready: false, reasonCode: 'COMPUTER_CHATGPT_EXTENSION_PREPARE_UNAVAILABLE' };
203
331
  }
204
332
  }
205
333
  async function executeDispatch(command) {
206
- const target = await resolveTarget(command.identity, false).catch(() => undefined);
334
+ const target = await resolveTarget(command.identity, false, command.providerBinding).catch(() => undefined);
207
335
  if (!target) return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_EXTENSION_TARGET_MISSING' };
336
+ const expectedProjectId = command.identity?.namespace === 'chatgpt.bootstrap' ? core.projectId(command.identity.projectUrl) : undefined;
337
+ if (command.identity?.namespace === 'chatgpt.bootstrap' && !expectedProjectId)
338
+ return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_PROJECT_UNPROVEN' };
208
339
  // An old tab may still contain a pre-upgrade content script after the
209
340
  // extension/runtime has been upgraded. Probe the *exact target tab* before
210
341
  // submitting; a background/version heartbeat alone cannot prove it.
211
- if (command.reasoning !== undefined) {
342
+ if (expectedProjectId) {
212
343
  let capability;
213
344
  try { capability = await tabMessage(target.tab.id, { type: 'forge-computer-chatgpt-capabilities' }); }
214
- catch { return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_REASONING_PREFLIGHT_UNAVAILABLE' }; }
215
- if (capability?.reasoningPreflight !== 'verified_before_send_v1') {
216
- return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_REASONING_PREFLIGHT_UNSUPPORTED' };
217
- }
345
+ catch { return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_CONTENT_CAPABILITIES_UNAVAILABLE' }; }
346
+ if (expectedProjectId && capability?.bootstrapTargetGuard !== 'project-checked-before-send-v1')
347
+ return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_BOOTSTRAP_PROJECT_GUARD_UNSUPPORTED' };
218
348
  }
219
349
  let result;
220
350
  try { result = await tabMessage(target.tab.id, {
221
351
  type: 'forge-computer-chatgpt-dispatch', prompt: command.prompt, mode: command.mode,
222
- reasoning: command.reasoning,
223
352
  ...(command.identity?.namespace === 'chatgpt.conversation' ? { expectedConversationId: command.identity.conversationId } : {}),
353
+ ...(expectedProjectId ? { expectedProjectId } : {}),
224
354
  }); }
225
355
  catch (error) {
226
356
  return dispatchTransportNotReached(error)
227
357
  ? { kind: 'dispatch', mutation: 'not_attempted', reasonCode: 'COMPUTER_CHATGPT_EXTENSION_CONTENT_UNAVAILABLE' }
228
358
  : { kind: 'dispatch', mutation: 'attempted' };
229
359
  }
230
- if (result?.dispatched !== true) return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: String(result?.reason ?? 'COMPUTER_CHATGPT_EXTENSION_PRE_MUTATION_REJECTION') };
231
- // A changed/stale implementation between preflight and send may have clicked
232
- // without attesting the reasoning choice. It is still a possible mutation,
233
- // never a safe not_attempted result or verified reasoning claim.
234
- if (command.reasoning !== undefined && !['medium', 'high', 'xhigh'].includes(result.reasoningVerified)) {
235
- return { kind: 'dispatch', mutation: 'attempted', confirmed: false };
236
- }
237
- let observation;
238
- for (let attempt = 1; attempt <= 4; attempt += 1) {
239
- await sleep(attempt * 200);
240
- try {
241
- observation = await snapshot(target.tab.id, { includeUserHistory: false, includePageText: false });
242
- if (normalize(observation.latestUserText) === normalize(command.prompt)) return { kind: 'dispatch', mutation: 'attempted', confirmed: true, ...(result.reasoningVerified ? { reasoningVerified: result.reasoningVerified } : {}), observation };
243
- } catch { /* mutation already happened; only confirmation is missing */ }
244
- }
245
- return { kind: 'dispatch', mutation: 'attempted', confirmed: false, ...(result.reasoningVerified ? { reasoningVerified: result.reasoningVerified } : {}), ...(observation ? { observation } : {}) };
360
+ if (result?.dispatched === false) return { kind: 'dispatch', mutation: 'not_attempted', reasonCode: String(result?.reason ?? 'COMPUTER_CHATGPT_EXTENSION_PRE_MUTATION_REJECTION') };
361
+ // A missing/malformed reply after dispatch crossed the content-script boundary
362
+ // is not an attestation that Send never ran.
363
+ if (result?.dispatched !== true) return { kind: 'dispatch', mutation: 'attempted', confirmed: false };
364
+ // The content script observes the committed user turn within the same page.
365
+ // A missing/negative receipt is an outcome-unknown submission, not a retry grant.
366
+ return { kind: 'dispatch', mutation: 'attempted', confirmed: result.confirmed === true };
246
367
  }
247
368
  async function executeFindMarker(command, instanceId) {
248
369
  const matches = [];
@@ -259,12 +380,37 @@ async function executeFindMarker(command, instanceId) {
259
380
  }
260
381
  return { kind: 'marker_matches', matches };
261
382
  }
262
- async function executeClose(command) {
263
- if (command.identity?.namespace === 'chatgpt.bootstrap') {
264
- const tabId = bootstrapTabs.get(command.identity.bootstrapKey);
265
- bootstrapTabs.delete(command.identity.bootstrapKey);
266
- if (tabId) await tabRemove(tabId).catch(() => undefined);
383
+ async function executeClose(command, instanceId) {
384
+ const pinned = command.providerBinding;
385
+ if (!pinned?.ownerToken || pinned.providerId !== PROVIDER_ID
386
+ || pinned.providerSessionId !== instanceId || !pinned.tabId || !pinned.windowId)
387
+ return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_OWNERSHIP_UNPROVEN', retryable: false };
388
+ const tabId = Number(pinned.tabId);
389
+ if (!Number.isSafeInteger(tabId) || tabId <= 0)
390
+ return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_ID_INVALID', retryable: false };
391
+ let tab;
392
+ try { tab = await tabGet(tabId); }
393
+ catch (error) {
394
+ // This exact Chrome profile has already proved the physical tab absent.
395
+ if (/No tab with id/i.test(String(error?.message ?? error))) return { kind: 'closed' };
396
+ return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_CLOSE_UNVERIFIED', retryable: true };
267
397
  }
398
+ if (String(tab.windowId) !== String(pinned.windowId)
399
+ || await attestedOwnerToken(tab, pinned) !== pinned.ownerToken)
400
+ return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_OWNERSHIP_UNPROVEN', retryable: false };
401
+ const identity = command.identity;
402
+ const exactConversation = identity?.namespace === 'chatgpt.conversation'
403
+ && core.parseConversation(tab.url ?? '')?.conversationId === identity.conversationId;
404
+ const exactBootstrap = identity?.namespace === 'chatgpt.bootstrap'
405
+ && (tab.url === identity.projectUrl || (core.projectId(identity.projectUrl)
406
+ && core.projectId(tab.url ?? '') === core.projectId(identity.projectUrl)));
407
+ if (!exactConversation && !exactBootstrap)
408
+ return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_IDENTITY_CHANGED', retryable: false };
409
+ try { await tabRemove(tabId); }
410
+ catch { return { kind: 'failed', code: 'COMPUTER_CHATGPT_TAB_CLOSE_UNVERIFIED', retryable: true }; }
411
+ await chrome.storage.session.remove(ownedTabKey(tabId)).catch(() => undefined);
412
+ if (identity.namespace === 'chatgpt.bootstrap' && bootstrapTabs.get(identity.bootstrapKey) === tabId)
413
+ bootstrapTabs.delete(identity.bootstrapKey);
268
414
  return { kind: 'closed' };
269
415
  }
270
416
  async function executeCommand(command, instanceId) {
@@ -274,7 +420,7 @@ async function executeCommand(command, instanceId) {
274
420
  if (command.kind === 'prepare') return await executePrepare(command);
275
421
  if (command.kind === 'dispatch') return await executeDispatch(command);
276
422
  if (command.kind === 'find_marker') return await executeFindMarker(command, instanceId);
277
- if (command.kind === 'close') return await executeClose(command);
423
+ if (command.kind === 'close') return await executeClose(command, instanceId);
278
424
  return { kind: 'failed', code: 'COMPUTER_CHATGPT_EXTENSION_COMMAND_UNKNOWN', retryable: false, failoverSafe: true };
279
425
  }
280
426
  async function publishHeartbeat(instanceId) {
@@ -295,18 +441,26 @@ async function publishHeartbeat(instanceId) {
295
441
  await nativeRpc('computer_extension_heartbeat', { providerId: PROVIDER_ID, providerInstanceId: instanceId, observedAt: new Date().toISOString(), conversations });
296
442
  }
297
443
  let providerTickInFlight;
444
+ let providerHeartbeatInFlight;
445
+ function heartbeatTick() {
446
+ providerHeartbeatInFlight ??= providerInstanceId().then(publishHeartbeat)
447
+ .finally(() => { providerHeartbeatInFlight = undefined; });
448
+ return providerHeartbeatInFlight;
449
+ }
298
450
  async function providerTick() {
299
- if (providerTickInFlight) return await providerTickInFlight;
451
+ // One claimed command retains the sole mutation lane; it must not also
452
+ // suppress profile heartbeat when a page/Chrome callback is slow or hangs.
453
+ if (providerTickInFlight) { await heartbeatTick(); return; }
300
454
  providerTickInFlight = (async () => {
301
- const instanceId = PROVIDER_INSTANCE_ID;
302
- await publishHeartbeat(instanceId);
303
- for (let index = 0; index < 8; index += 1) {
304
- const claim = await nativeRpc('computer_extension_claim', { provider_instance_id: instanceId });
305
- const command = claim?.command;
306
- if (!command) break;
307
- const result = await executeCommand(command, instanceId);
308
- await nativeRpc('computer_extension_complete', { provider_instance_id: instanceId, command_id: command.commandId, result });
309
- }
455
+ const instanceId = await providerInstanceId();
456
+ await heartbeatTick();
457
+ // One command per tick keeps heartbeats and other tasks fair. The broker
458
+ // remains the only authority for a claimed Send and its unknown outcome.
459
+ const claim = await nativeRpc('computer_extension_claim', { provider_instance_id: instanceId });
460
+ const command = claim?.command;
461
+ if (!command) return;
462
+ const result = await executeCommand(command, instanceId);
463
+ await nativeRpc('computer_extension_complete', { provider_instance_id: instanceId, command_id: command.commandId, result });
310
464
  })().finally(() => { providerTickInFlight = undefined; });
311
465
  return await providerTickInFlight;
312
466
  }
@@ -352,7 +506,9 @@ async function refreshDiscovery() {
352
506
  const title = String(tab.title ?? '').trim();
353
507
  conversations.push({ conversation_id: identity.conversationId, canonical_url: identity.canonicalUrl, ...projectByConversation.get(identity.conversationId), ...(title ? { title: title.slice(0, 512) } : {}), ...(tab.id === current?.id ? { is_current: true } : {}) });
354
508
  }
355
- await nativeRpc('browser_discovery_update', { source: `chrome-extension:${PROVIDER_INSTANCE_ID}`, conversations }).catch(() => undefined);
509
+ // Discovery and command routing share one persisted Chrome-profile identity.
510
+ const instanceId = await providerInstanceId();
511
+ await nativeRpc('browser_discovery_update', { source: `chrome-extension:${instanceId}`, conversations }).catch(() => undefined);
356
512
  }
357
513
  let discoveryInFlight;
358
514
  let discoveryTimer;