@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
@@ -22,10 +22,13 @@ import {
22
22
  inspectForgeRuntimeLaunchAgentContract,
23
23
  installForgeRuntimeService,
24
24
  readForgeRuntimeServiceConfig,
25
+ restoreRuntimeConfigFromVerifiedLaunchdContract,
26
+ verifiedRuntimePortFromLaunchdContract,
25
27
  syncForgeRuntimeActiveEntrypoint,
26
28
  uninstallForgeRuntimeService,
27
29
  } from '../root/service';
28
30
  import { removeRetiredCandidateExecutionLane } from '../root/runtime-lane';
31
+ import { inspectWorkflowSupervisorBrowserAdapter, installWorkflowSupervisorBrowserAdapter } from '../plugins/computer-chatgpt-browser-adapter-installer';
29
32
  import {
30
33
  inspectKnownGoodRecoveryBundle,
31
34
  knownGoodRecoveryBundleRoot,
@@ -57,7 +60,15 @@ import {
57
60
  type RuntimeReleaseAuthority,
58
61
  } from '../root/release-store';
59
62
  import type { RuntimeReleaseManifest } from '../root/types';
60
- import { ensurePackageConnectorService, packageConnectorAuthMode, packageConnectorServicePaths, type PackageConnectorReleaseBinding } from '../root/package-connector-service';
63
+ import {
64
+ armPackageConnectorCutoverDrain,
65
+ clearPackageConnectorCutoverDrain,
66
+ ensurePackageConnectorService,
67
+ packageConnectorAuthMode,
68
+ packageConnectorServicePaths,
69
+ readPackageConnectorServiceAuthority,
70
+ type PackageConnectorReleaseBinding,
71
+ } from '../root/package-connector-service';
61
72
  import { createRecoveryHttpTransport, type RecoveryHttpTransport } from './http-transport';
62
73
  import { observeRecoveryWatchdogHealth } from './watchdog-heartbeat';
63
74
  import { RECOVERY_DAEMON_LABEL } from './service-labels';
@@ -72,7 +83,7 @@ import {
72
83
  type ControllerHomeStorageMigration,
73
84
  } from '../../cli/repositories/controller-home';
74
85
  import { readCurrentRecoveryRelease, type RecoveryRuntimeRole } from './release';
75
- import { recoveryOperationLockPath } from './operation-lock';
86
+ import { acquireRecoveryOperationLock, recoveryOperationLockOwnerAlive, recoveryOperationLockPath, RecoveryOperationLockRaceError, type RecoveryOperationLockRecord } from './operation-lock';
76
87
  import { RECOVERY_MUTATION_IDENTITY_FIELDS, type RecoveryMutationIdentityArguments } from './mutation-identity-contract';
77
88
  import { createCandidateExecutionLane, readStableExecutionLane } from '../root/runtime-lane';
78
89
  import {
@@ -316,15 +327,7 @@ export type RecoveryMutationAction =
316
327
  | 'restart_recovery_gateway'
317
328
  | 'repair_public_tunnel';
318
329
 
319
- interface RecoveryLock {
320
- schemaVersion?: 1;
321
- pid: number;
322
- instanceId: string;
323
- processStartTime?: string;
324
- acquiredAt: string;
325
- action?: RecoveryMutationAction;
326
- requestId?: string;
327
- }
330
+ type RecoveryLock = RecoveryOperationLockRecord;
328
331
 
329
332
  interface RecoveryLockIntent {
330
333
  action: RecoveryMutationAction;
@@ -961,23 +964,6 @@ function persistWatchdogDiagnosticEvidence(config: RecoveryConfig, result: Verif
961
964
  return evidence;
962
965
  }
963
966
 
964
- function processStartTime(pid: number): string | undefined {
965
- const result = spawnSync('/bin/ps', ['-o', 'lstart=', '-p', String(pid)], {
966
- encoding: 'utf8',
967
- timeout: 2_000,
968
- maxBuffer: 4_096,
969
- });
970
- if (result.status !== 0 || typeof result.stdout !== 'string') return undefined;
971
- return result.stdout.trim() || undefined;
972
- }
973
-
974
- function recoveryLockOwnerAlive(lock: RecoveryLock): boolean {
975
- if (!pidAlive(lock.pid)) return false;
976
- if (!lock.processStartTime) return true;
977
- const observed = processStartTime(lock.pid);
978
- return observed === lock.processStartTime;
979
- }
980
-
981
967
  function recoveryLockOwnerAttributable(owner: RecoveryLock): boolean {
982
968
  return Boolean(owner.action && owner.requestId?.trim());
983
969
  }
@@ -1005,7 +991,7 @@ function recoveryBusyDetail(owner: RecoveryLock): string {
1005
991
  function liveRecoveryMutationLock(config: RecoveryConfig): RecoveryLock | undefined {
1006
992
  const owner = json<RecoveryLock>(lockPath(config));
1007
993
  if (!owner || !Number.isInteger(owner.pid) || owner.pid <= 0 || typeof owner.instanceId !== 'string' || !owner.instanceId.trim()) return undefined;
1008
- return recoveryLockOwnerAlive(owner) ? owner : undefined;
994
+ return recoveryOperationLockOwnerAlive(owner) ? owner : undefined;
1009
995
  }
1010
996
 
1011
997
  async function withLock<T>(
@@ -1013,73 +999,35 @@ async function withLock<T>(
1013
999
  intent: RecoveryLockIntent,
1014
1000
  action: (lock: RecoveryLock) => Promise<T>,
1015
1001
  ): Promise<RecoveryLockAttempt<T>> {
1016
- const path = lockPath(config);
1017
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
1018
- const instanceId = randomUUID();
1019
- const requestId = intent.requestId?.trim() || `internal:${intent.action}:${instanceId}`;
1020
- const lock: RecoveryLock = {
1021
- schemaVersion: 1,
1022
- pid: process.pid,
1023
- instanceId,
1024
- processStartTime: processStartTime(process.pid),
1025
- acquiredAt: new Date().toISOString(),
1026
- action: intent.action,
1027
- requestId,
1028
- };
1029
-
1030
- for (let attempt = 0; attempt < 2; attempt += 1) {
1031
- let fd: number | undefined;
1032
- let acquired = false;
1033
- try {
1034
- try {
1035
- fd = openSync(path, 'wx', 0o600);
1036
- writeFileSync(fd, JSON.stringify(lock));
1037
- acquired = true;
1038
- } catch (error) {
1039
- if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
1040
- const existing = json<RecoveryLock>(path);
1041
- if (!existing || !Number.isInteger(existing.pid) || typeof existing.instanceId !== 'string') {
1042
- throw new Error('RECOVERY_OPERATION_LOCK_UNCERTAIN');
1043
- }
1044
- if (recoveryLockOwnerAlive(existing)) {
1045
- assertRecoveryLockOwnerAttributable(config, existing);
1046
- return { acquired: false, owner: existing };
1047
- }
1048
- const latest = json<RecoveryLock>(path);
1049
- if (!latest || latest.instanceId !== existing.instanceId) {
1050
- if (attempt === 1) {
1051
- if (latest && recoveryLockOwnerAlive(latest)) {
1052
- assertRecoveryLockOwnerAttributable(config, latest);
1053
- return { acquired: false, owner: latest };
1054
- }
1055
- audit(config, 'recovery_operation_lock_race', {
1056
- existingInstanceId: existing.instanceId,
1057
- existingPid: existing.pid,
1058
- latestInstanceId: latest?.instanceId ?? null,
1059
- latestPid: latest?.pid ?? null,
1060
- });
1061
- throw new Error('RECOVERY_OPERATION_LOCK_RACE');
1062
- }
1063
- continue;
1064
- }
1065
- if (recoveryLockOwnerAlive(latest)) {
1066
- assertRecoveryLockOwnerAttributable(config, latest);
1067
- return { acquired: false, owner: latest };
1068
- }
1069
- try { writeFileSync(`${path}.stale-${Date.now()}-${existing.instanceId}`, readFileSync(path)); } catch { /* evidence best effort */ }
1070
- rmSync(path, { force: true });
1071
- continue;
1072
- }
1073
- return { acquired: true, value: await action(lock) };
1074
- } finally {
1075
- if (fd !== undefined) closeSync(fd);
1076
- if (acquired) {
1077
- const current = json<RecoveryLock>(path);
1078
- if (current?.instanceId === lock.instanceId) rmSync(path, { force: true });
1079
- }
1002
+ // Installer, certification and Recovery actions share one physical lock writer.
1003
+ // Core supplies only action attribution, audit and scoped handle release.
1004
+ let attempt: ReturnType<typeof acquireRecoveryOperationLock>;
1005
+ try {
1006
+ attempt = acquireRecoveryOperationLock({
1007
+ controllerHome: config.controllerHome,
1008
+ action: intent.action,
1009
+ requestId: intent.requestId,
1010
+ });
1011
+ } catch (error) {
1012
+ if (error instanceof RecoveryOperationLockRaceError) {
1013
+ audit(config, 'recovery_operation_lock_race', {
1014
+ existingInstanceId: error.existing.instanceId,
1015
+ existingPid: error.existing.pid,
1016
+ latestInstanceId: error.latest?.instanceId ?? null,
1017
+ latestPid: error.latest?.pid ?? null,
1018
+ });
1080
1019
  }
1020
+ throw error;
1021
+ }
1022
+ if (!attempt.acquired) {
1023
+ assertRecoveryLockOwnerAttributable(config, attempt.owner);
1024
+ return { acquired: false, owner: attempt.owner };
1025
+ }
1026
+ try {
1027
+ return { acquired: true, value: await action(attempt.handle.record) };
1028
+ } finally {
1029
+ attempt.handle.close();
1081
1030
  }
1082
- throw new Error('RECOVERY_OPERATION_LOCK_BUSY');
1083
1031
  }
1084
1032
 
1085
1033
  export async function runtimeStatus(config: RecoveryConfig) {
@@ -2163,6 +2111,23 @@ function quarantine(config: RecoveryConfig, release: ReleaseEvidence | undefined
2163
2111
  writeJson(quarantinePath(config), { schemaVersion: 1, releases });
2164
2112
  }
2165
2113
 
2114
+ /** Optional installed projection belongs to the whole active release. Fresh
2115
+ * Candidate B/uninstalled Homes never opt into global browser registration.
2116
+ * Loaded-tab capability and provider connectivity remain separate evidence.
2117
+ */
2118
+ function syncInstalledBrowserAdapterProjection(controllerHome: string): void {
2119
+ let projection = inspectWorkflowSupervisorBrowserAdapter(controllerHome);
2120
+ if (!existsSync(projection.extensionPath) && !existsSync(projection.nativeHostPath)) return;
2121
+ const authority = readRuntimeReleaseAuthority(controllerHome);
2122
+ if (!authority) throw new Error('RECOVERY_BROWSER_ADAPTER_RELEASE_AUTHORITY_MISSING');
2123
+ if (!projection.projectionCurrent || projection.activeReleaseId !== authority.active.releaseId) {
2124
+ projection = installWorkflowSupervisorBrowserAdapter(controllerHome);
2125
+ }
2126
+ if (!projection.projectionCurrent || projection.activeReleaseId !== authority.active.releaseId) {
2127
+ throw new Error(`RECOVERY_BROWSER_ADAPTER_PROJECTION_UNVERIFIED: ${projection.detail}`);
2128
+ }
2129
+ }
2130
+
2166
2131
  async function rollbackPreviousLocked(config: RecoveryConfig, reason: string): Promise<RollbackResult> {
2167
2132
  const before = await verifyStableRuntime(config);
2168
2133
  const active = before.releases.active;
@@ -2208,6 +2173,7 @@ async function rollbackPreviousLocked(config: RecoveryConfig, reason: string): P
2208
2173
  || committed.active.artifactIdentity !== target.artifactIdentity
2209
2174
  || committed.active.manifestSha256 !== target.manifestSha256
2210
2175
  ) throw new Error('RECOVERY_ROLLBACK_AUTHORITY_MISMATCH');
2176
+ syncInstalledBrowserAdapterProjection(config.controllerHome);
2211
2177
  quarantine(config, active, 'whole-Runtime rollback completed');
2212
2178
  audit(config, 'rollback_succeeded', {
2213
2179
  operationId,
@@ -2229,7 +2195,7 @@ async function rollbackPreviousLocked(config: RecoveryConfig, reason: string): P
2229
2195
  } catch (error) {
2230
2196
  const detail = error instanceof Error ? error.message : 'whole-Runtime rollback failed';
2231
2197
  audit(config, 'rollback_failed', { operationId, activeRevision: active.revision, targetRevision: target.revision, detail });
2232
- return { ok: false, operationId, detail };
2198
+ return { ok: false, operationId, detail, verify: await verifyStableRuntime(config) };
2233
2199
  }
2234
2200
  }
2235
2201
 
@@ -2965,6 +2931,8 @@ function primaryRuntimeServiceContractMatches(config: RecoveryConfig, owner: Pri
2965
2931
  }
2966
2932
 
2967
2933
  export interface PrimaryConnectorRecoveryDependencies {
2934
+ /** Explicit operator recovery; watchdog calls leave a healthy Connector alone. */
2935
+ forceHealthyRestart?: boolean;
2968
2936
  /** Durable caller identity propagated into the Recovery mutation lock and audit trail. */
2969
2937
  requestId?: string;
2970
2938
  platform?: NodeJS.Platform;
@@ -3045,6 +3013,7 @@ export interface AutomaticReleaseCutoverReadiness {
3045
3013
  ready: boolean;
3046
3014
  activeSessions?: number;
3047
3015
  activePosts?: number;
3016
+ oldestActivePostAgeMs?: number;
3048
3017
  activeStreams?: number;
3049
3018
  initializing?: number;
3050
3019
  latestActivityAgeMs?: number;
@@ -3053,6 +3022,7 @@ export interface AutomaticReleaseCutoverReadiness {
3053
3022
 
3054
3023
  const AUTOMATIC_RELEASE_CUTOVER_QUIET_MS = 30_000;
3055
3024
  const AUTOMATIC_RELEASE_CUTOVER_HEALTH_TIMEOUT_MS = 2_000;
3025
+ const RELEASE_SESSION_CONNECTOR_DRAIN_WAIT_MS = 180_000;
3056
3026
 
3057
3027
  function primaryConnectorHealthEndpoint(config: RecoveryConfig): string | undefined {
3058
3028
  const localMcpUrl = config.primaryConnectorService?.localMcpUrl?.trim();
@@ -3087,7 +3057,7 @@ export async function probeReleaseCutoverReadiness(
3087
3057
  signal: controller.signal,
3088
3058
  });
3089
3059
  if (!response.ok) return { ready: false, detail: `primary Connector health returned HTTP ${response.status}` };
3090
- let payload: { sessions?: { active?: unknown; activePosts?: unknown; activeStreams?: unknown; initializing?: unknown; latestActivityAgeMs?: unknown } };
3060
+ let payload: { sessions?: { active?: unknown; activePosts?: unknown; oldestActivePostAgeMs?: unknown; activeStreams?: unknown; initializing?: unknown; latestActivityAgeMs?: unknown } };
3091
3061
  try {
3092
3062
  payload = JSON.parse(response.body) as typeof payload;
3093
3063
  } catch {
@@ -3095,6 +3065,8 @@ export async function probeReleaseCutoverReadiness(
3095
3065
  }
3096
3066
  const activeSessions = Number(payload.sessions?.active);
3097
3067
  const activePosts = Number(payload.sessions?.activePosts);
3068
+ const oldestActivePostAgeMs = payload.sessions?.oldestActivePostAgeMs === undefined
3069
+ ? undefined : Number(payload.sessions.oldestActivePostAgeMs);
3098
3070
  const activeStreams = Number(payload.sessions?.activeStreams);
3099
3071
  const initializing = Number(payload.sessions?.initializing);
3100
3072
  const latestActivityAgeMs = Number(payload.sessions?.latestActivityAgeMs);
@@ -3109,6 +3081,8 @@ export async function probeReleaseCutoverReadiness(
3109
3081
  const evidence = {
3110
3082
  activeSessions,
3111
3083
  activePosts,
3084
+ ...(oldestActivePostAgeMs !== undefined && Number.isFinite(oldestActivePostAgeMs)
3085
+ ? { oldestActivePostAgeMs } : {}),
3112
3086
  ...(Number.isFinite(activeStreams) ? { activeStreams } : {}),
3113
3087
  initializing,
3114
3088
  ...(Number.isFinite(latestActivityAgeMs) ? { latestActivityAgeMs } : {}),
@@ -3117,7 +3091,7 @@ export async function probeReleaseCutoverReadiness(
3117
3091
  return {
3118
3092
  ready: false,
3119
3093
  ...evidence,
3120
- detail: `primary Connector is busy: activePosts=${activePosts}; activeStreams=${activeStreams}; initializing=${initializing}`,
3094
+ detail: `primary Connector is busy: activePosts=${activePosts}; activeStreams=${activeStreams}; initializing=${initializing}${oldestActivePostAgeMs !== undefined && Number.isFinite(oldestActivePostAgeMs) ? `; oldestActivePostAgeMs=${oldestActivePostAgeMs}` : ''}`,
3121
3095
  };
3122
3096
  }
3123
3097
  if (activeSessions > 0 && (!Number.isFinite(latestActivityAgeMs) || latestActivityAgeMs < quietMs)) {
@@ -3242,6 +3216,7 @@ export async function restartPrimaryConnector(
3242
3216
  ): Promise<PrimaryConnectorRestartResult> {
3243
3217
  const verifyLocal = dependencies.verifyLocal ?? verifyLocalRuntime;
3244
3218
  const initialLocal = await verifyLocal(config);
3219
+ const forceHealthyRestart = dependencies.forceHealthyRestart === true;
3245
3220
  // `verifyLocalRuntime` also reports the Connector's own loopback probe.
3246
3221
  // That probe is expected to be false when this recovery action is needed,
3247
3222
  // so it must not prevent a healthy canonical Runtime from repairing the
@@ -3283,7 +3258,7 @@ export async function restartPrimaryConnector(
3283
3258
  let localConnector = await probeConnectorLocal(config);
3284
3259
  let connectorOwnership = localConnector?.ok ? await probeConnectorOwnership(config) : undefined;
3285
3260
  let observed = localConnector?.ok && connectorOwnership?.ok ? await reconnect(config) : undefined;
3286
- if (observed?.ok) {
3261
+ if (observed?.ok && !forceHealthyRestart) {
3287
3262
  return {
3288
3263
  ok: true,
3289
3264
  attempted: bindingRepair.attempted,
@@ -3299,7 +3274,7 @@ export async function restartPrimaryConnector(
3299
3274
  // A healthy local OAuth endpoint proves the Connector is not the broken hop.
3300
3275
  // If a distinct primary tunnel is configured, skip the pointless Gateway
3301
3276
  // restart and repair the public hop directly.
3302
- const restartConnector = !localConnector?.ok || !connectorOwnership?.ok || !tunnelConfigured;
3277
+ const restartConnector = forceHealthyRestart || !localConnector?.ok || !connectorOwnership?.ok || !tunnelConfigured;
3303
3278
  if (restartConnector) {
3304
3279
  const restarted = await ensurePrimaryConnectorServiceStarted(service, runCommand);
3305
3280
  if (!restarted.ok) {
@@ -3331,7 +3306,8 @@ export async function restartPrimaryConnector(
3331
3306
  }
3332
3307
  }
3333
3308
 
3334
- observed ??= await reconnect(config);
3309
+ if (restartConnector) observed = await reconnect(config);
3310
+ else observed ??= await reconnect(config);
3335
3311
  const primaryTunnel = configuredPrimaryPublicTunnel(config);
3336
3312
  if (!observed.ok && localConnector?.ok && connectorOwnership?.ok && primaryTunnel) {
3337
3313
  let tunnelRestarted: { ok: boolean; attempted: boolean; detail: string; serviceTarget: string } | undefined;
@@ -3445,7 +3421,9 @@ async function observePrimaryRuntimePort(config: RecoveryConfig, runCommand: Com
3445
3421
  try {
3446
3422
  port = readForgeRuntimeServiceConfig(paths.configPath).port;
3447
3423
  } catch {
3448
- return { port: -1, pids: [], uncertain: 'primary Runtime service port could not be read' };
3424
+ const provenPort = verifiedRuntimePortFromLaunchdContract(config.controllerHome);
3425
+ if (!provenPort) return { port: -1, pids: [], uncertain: 'primary Runtime service port could not be read from config or exact launchd contract' };
3426
+ port = provenPort;
3449
3427
  }
3450
3428
  const result = await runCommand('lsof', ['-nP', '-Fp', `-iTCP:${port}`, '-sTCP:LISTEN'], 3_000);
3451
3429
  if (result.status === 1 && result.stderr.trim().length === 0) return { port, pids: [] };
@@ -3694,6 +3672,16 @@ async function stopPrimaryRuntimeForReleaseTransition(input: {
3694
3672
  staleListenerCleanup,
3695
3673
  };
3696
3674
  }
3675
+ // A corrupt config is reconstructed through the original validated writer
3676
+ // only after the Runtime service, listener, owner and Supervisor are quiescent.
3677
+ try {
3678
+ readForgeRuntimeServiceConfig(forgeRuntimeServicePaths(input.config.controllerHome).configPath);
3679
+ } catch {
3680
+ if (input.service.platform !== 'launchd'
3681
+ || !restoreRuntimeConfigFromVerifiedLaunchdContract(input.config.controllerHome)) {
3682
+ return { ok: false, detail: 'stopped Runtime config cannot be restored from an exact launchd contract', staleListenerCleanup };
3683
+ }
3684
+ }
3697
3685
  return {
3698
3686
  ok: true,
3699
3687
  detail: `primary Runtime ${input.service.platform} service, owner, TCP listener, and Workflow Supervisor writer are quiescent`,
@@ -3836,6 +3824,7 @@ async function rebindStartAndVerifyPrimaryRuntime(input: {
3836
3824
 
3837
3825
  try {
3838
3826
  input.beforeStart?.();
3827
+ syncInstalledBrowserAdapterProjection(input.config.controllerHome);
3839
3828
  } catch (error) {
3840
3829
  return {
3841
3830
  ok: false,
@@ -5711,7 +5700,14 @@ export async function cutoverConfiguredRuntimeReleaseSession(
5711
5700
  const reconciliationDeadline = Date.now() + reconciliationTimeoutMs;
5712
5701
  let stableNow = observeRuntimeStatus(config.controllerHome);
5713
5702
  const stableIdentity = () => {
5714
- const liveRuntimeIdentityVerified = stableNow.running && stableNow.ready && !stableNow.stale;
5703
+ // A crash after authority publication must not bypass projection
5704
+ // verification. Reconciliation observes only; the release transaction
5705
+ // owns any projection mutation before starting the Runtime.
5706
+ const projection = inspectWorkflowSupervisorBrowserAdapter(config.controllerHome);
5707
+ const installed = existsSync(projection.extensionPath) || existsSync(projection.nativeHostPath);
5708
+ const projectionVerified = !installed || (projection.projectionCurrent
5709
+ && projection.activeReleaseId === stableNow.snapshot?.releaseId);
5710
+ const liveRuntimeIdentityVerified = stableNow.running && stableNow.ready && !stableNow.stale && projectionVerified;
5715
5711
  return {
5716
5712
  candidateIsStable: Boolean(
5717
5713
  liveRuntimeIdentityVerified
@@ -5806,6 +5802,10 @@ export async function cutoverConfiguredRuntimeReleaseSession(
5806
5802
  };
5807
5803
  }
5808
5804
 
5805
+ const projection = inspectWorkflowSupervisorBrowserAdapter(config.controllerHome);
5806
+ const projectionFailure = (existsSync(projection.extensionPath) || existsSync(projection.nativeHostPath))
5807
+ && (!projection.projectionCurrent || projection.activeReleaseId !== stableNow.snapshot?.releaseId)
5808
+ ? `; installed browser projection is unverified: ${projection.detail}` : '';
5809
5809
  session = advanceReleaseSession({
5810
5810
  controllerHome: config.controllerHome,
5811
5811
  sessionId,
@@ -5814,7 +5814,7 @@ export async function cutoverConfiguredRuntimeReleaseSession(
5814
5814
  receipts: [{
5815
5815
  id: 'cutover_failed',
5816
5816
  kind: 'cutover',
5817
- summary: `${reason}; Stable A outcome is not fully verified; Candidate B is retired`.slice(0, 500),
5817
+ summary: `${reason}; Stable A outcome is not fully verified${projectionFailure}; Candidate B is retired`.slice(0, 500),
5818
5818
  }],
5819
5819
  });
5820
5820
  const cleanup = cleanupRetiredCandidateLane(config, session);
@@ -5828,7 +5828,7 @@ export async function cutoverConfiguredRuntimeReleaseSession(
5828
5828
  return {
5829
5829
  ok: false as const,
5830
5830
  attempted: true,
5831
- detail: `${reason}; Candidate B is retired but Stable A cutover/rollback outcome is not fully verified${cleanup.ok ? '' : `; Candidate B cleanup failed: ${cleanup.detail}`}`,
5831
+ detail: `${reason}; Candidate B is retired but Stable A cutover/rollback outcome is not fully verified${projectionFailure}${cleanup.ok ? '' : `; Candidate B cleanup failed: ${cleanup.detail}`}`,
5832
5832
  releaseSession: session,
5833
5833
  ...(activation ? { activation } : {}),
5834
5834
  };
@@ -5912,79 +5912,94 @@ export async function cutoverConfiguredRuntimeReleaseSession(
5912
5912
  ) throw new Error('RELEASE_SESSION_STABLE_CHANGED_DURING_PROMOTION');
5913
5913
 
5914
5914
  // Connector quietness is a cutover invariant, not an automatic-release
5915
- // policy. Every caller, including explicit/local-recovery ReleaseSession
5916
- // progression, must prove that no MCP request/stream is active and that
5917
- // remaining sessions have been idle for the bounded quiet window before
5918
- // Stable A ownership can move. Otherwise a perfectly valid release switch
5919
- // can tear down the ChatGPT turn that requested it.
5920
- let readiness = await probeReleaseCutoverReadiness(config);
5921
- // The post-promotion verification above may itself create MCP activity
5922
- // on the primary Connector. Give that activity a bounded opportunity
5923
- // to become quiet before deferring the session; otherwise every retry
5924
- // re-verifies the Runtime, refreshes latestActivityAgeMs, and can defer
5925
- // forever without ever reaching the cutover attempt.
5926
- const quietWaitDeadline = Date.now() + AUTOMATIC_RELEASE_CUTOVER_QUIET_MS + 5_000;
5927
- while (
5928
- !readiness.ready
5929
- && readiness.activePosts === 0
5930
- && readiness.activeStreams === 0
5931
- && readiness.initializing === 0
5932
- && Number.isFinite(readiness.latestActivityAgeMs)
5933
- && (readiness.latestActivityAgeMs ?? AUTOMATIC_RELEASE_CUTOVER_QUIET_MS) < AUTOMATIC_RELEASE_CUTOVER_QUIET_MS
5934
- && Date.now() < quietWaitDeadline
5935
- ) {
5936
- const remainingMs = Math.max(
5937
- 50,
5938
- AUTOMATIC_RELEASE_CUTOVER_QUIET_MS - (readiness.latestActivityAgeMs ?? 0) + 25,
5939
- );
5940
- await sleep(Math.min(2_000, remainingMs));
5941
- readiness = await probeReleaseCutoverReadiness(config);
5942
- }
5943
- if (!readiness.ready) {
5944
- audit(config, 'release_session_cutover_deferred_for_mcp_activity', {
5915
+ // policy. Passive observation alone can starve forever under continuous
5916
+ // valid MCP traffic, so the existing Package Connector generation is
5917
+ // mechanically fenced from admitting new POSTs while already-admitted
5918
+ // responses drain. The marker is scoped to this exact Stable A Connector
5919
+ // authority generation; Candidate/rollback rebinds change that generation
5920
+ // and therefore remain able to perform post-cutover verification.
5921
+ const connectorAuthority = readPackageConnectorServiceAuthority(config.controllerHome);
5922
+ const connectorDrain = connectorAuthority
5923
+ ? armPackageConnectorCutoverDrain({
5924
+ controllerHome: config.controllerHome,
5925
+ sessionId,
5926
+ stableReleaseId: session.stableRelease.releaseId,
5927
+ connectorAuthority,
5928
+ })
5929
+ : undefined;
5930
+ try {
5931
+ let readiness = await probeReleaseCutoverReadiness(config);
5932
+ // The caller that requested cutover may itself still be one admitted MCP
5933
+ // POST until a detached Recovery process takes over. Keep the fence up
5934
+ // long enough for that response plus other already-admitted POSTs to
5935
+ // settle, then preserve the existing 30s session-idle invariant.
5936
+ const quietWaitDeadline = Date.now() + RELEASE_SESSION_CONNECTOR_DRAIN_WAIT_MS;
5937
+ while (!readiness.ready && Date.now() < quietWaitDeadline) {
5938
+ const hasInFlightActivity = (readiness.activePosts ?? 0) > 0
5939
+ || (readiness.activeStreams ?? 0) > 0
5940
+ || (readiness.initializing ?? 0) > 0;
5941
+ const awaitingQuietWindow = readiness.activePosts === 0
5942
+ && readiness.activeStreams === 0
5943
+ && readiness.initializing === 0
5944
+ && Number.isFinite(readiness.latestActivityAgeMs)
5945
+ && (readiness.latestActivityAgeMs ?? AUTOMATIC_RELEASE_CUTOVER_QUIET_MS) < AUTOMATIC_RELEASE_CUTOVER_QUIET_MS;
5946
+ if (!(connectorDrain && hasInFlightActivity) && !awaitingQuietWindow) break;
5947
+ const remainingMs = awaitingQuietWindow
5948
+ ? Math.max(50, AUTOMATIC_RELEASE_CUTOVER_QUIET_MS - (readiness.latestActivityAgeMs ?? 0) + 25)
5949
+ : 500;
5950
+ await sleep(Math.min(2_000, remainingMs));
5951
+ readiness = await probeReleaseCutoverReadiness(config);
5952
+ }
5953
+ if (!readiness.ready) {
5954
+ audit(config, 'release_session_cutover_deferred_for_mcp_activity', {
5955
+ sessionId,
5956
+ detail: readiness.detail,
5957
+ activeSessions: readiness.activeSessions,
5958
+ activePosts: readiness.activePosts,
5959
+ oldestActivePostAgeMs: readiness.oldestActivePostAgeMs,
5960
+ activeStreams: readiness.activeStreams,
5961
+ initializing: readiness.initializing,
5962
+ latestActivityAgeMs: readiness.latestActivityAgeMs,
5963
+ connectorDrainArmed: Boolean(connectorDrain),
5964
+ requestKind: requestId?.startsWith('recovery-auto-release:') ? 'automatic' : 'explicit',
5965
+ });
5966
+ return {
5967
+ ok: true as const,
5968
+ attempted: false,
5969
+ noOp: true,
5970
+ detail: `RELEASE_SESSION_CUTOVER_DEFERRED_FOR_MCP_ACTIVITY: ${readiness.detail}`,
5971
+ releaseSession: session,
5972
+ };
5973
+ }
5974
+
5975
+ session = advanceReleaseSession({
5976
+ controllerHome: config.controllerHome,
5945
5977
  sessionId,
5946
- detail: readiness.detail,
5947
- activeSessions: readiness.activeSessions,
5948
- activePosts: readiness.activePosts,
5949
- activeStreams: readiness.activeStreams,
5950
- initializing: readiness.initializing,
5951
- latestActivityAgeMs: readiness.latestActivityAgeMs,
5952
- requestKind: requestId?.startsWith('recovery-auto-release:') ? 'automatic' : 'explicit',
5978
+ expectedRevision: session.revision,
5979
+ phase: 'cutover_attempting',
5980
+ receipts: [{
5981
+ id: 'cutover_attempt',
5982
+ kind: 'cutover',
5983
+ summary: `byte-identical Candidate B ${candidateRelease.releaseId} promoted to Stable A release storage; beginning the single fenced cutover attempt`,
5984
+ }],
5953
5985
  });
5954
- return {
5955
- ok: true as const,
5956
- attempted: false,
5957
- noOp: true,
5958
- detail: `RELEASE_SESSION_CUTOVER_DEFERRED_FOR_MCP_ACTIVITY: ${readiness.detail}`,
5959
- releaseSession: session,
5960
- };
5961
- }
5962
-
5963
- session = advanceReleaseSession({
5964
- controllerHome: config.controllerHome,
5965
- sessionId,
5966
- expectedRevision: session.revision,
5967
- phase: 'cutover_attempting',
5968
- receipts: [{
5969
- id: 'cutover_attempt',
5970
- kind: 'cutover',
5971
- summary: `byte-identical Candidate B ${candidateRelease.releaseId} promoted to Stable A release storage; beginning the single fenced cutover attempt`,
5972
- }],
5973
- });
5974
5986
 
5975
- const activation = await activateRuntimeReleaseInternal(config, promoted.manifestPath, {}, {
5976
- ...(requestId?.trim() ? { requestId: requestId.trim() } : {}),
5977
- expectedAuthorityRevision: session.stableRelease.authorityRevision,
5978
- expectedActiveReleaseId: session.stableRelease.releaseId,
5979
- releaseSessionId: session.sessionId,
5980
- }, releaseSessionLock);
5981
-
5982
- return reconcileCutoverAttempt(
5983
- activation.ok
5984
- ? 'cutover activation completed; reconciling canonical Runtime identity before phase progression'
5985
- : `cutover activation returned failure: ${activation.detail}`,
5986
- activation,
5987
- );
5987
+ const activation = await activateRuntimeReleaseInternal(config, promoted.manifestPath, {}, {
5988
+ ...(requestId?.trim() ? { requestId: requestId.trim() } : {}),
5989
+ expectedAuthorityRevision: session.stableRelease.authorityRevision,
5990
+ expectedActiveReleaseId: session.stableRelease.releaseId,
5991
+ releaseSessionId: session.sessionId,
5992
+ }, releaseSessionLock);
5993
+
5994
+ return reconcileCutoverAttempt(
5995
+ activation.ok
5996
+ ? 'cutover activation completed; reconciling canonical Runtime identity before phase progression'
5997
+ : `cutover activation returned failure: ${activation.detail}`,
5998
+ activation,
5999
+ );
6000
+ } finally {
6001
+ if (connectorDrain) clearPackageConnectorCutoverDrain(config.controllerHome);
6002
+ }
5988
6003
  } catch (error) {
5989
6004
  const detail = error instanceof Error ? error.message : String(error);
5990
6005
  const latest = readReleaseSession(config.controllerHome, sessionId) ?? session;