@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.
- package/AGENTS.md +5 -2
- package/CHANGELOG.md +20 -0
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
- package/adapters/chatgpt/provider-delivery.ts +1 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
- package/adapters/computer/chatgpt-extension-target.ts +295 -50
- package/adapters/computer/chrome-extension/background.js +227 -71
- package/adapters/computer/chrome-extension/content.js +181 -157
- package/adapters/computer/chrome-extension/manifest.json +3 -1
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +39 -9
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -6
- package/adapters/mcp/instructions.ts +2 -2
- package/adapters/mcp/multi-repository.ts +7 -28
- package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
- package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
- package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
- package/adapters/mcp/runtime-gateway/router.ts +3 -5
- package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
- package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
- package/adapters/mcp/server.ts +24 -3
- package/adapters/mcp/setup.ts +4 -10
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
- package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
- package/adapters/mcp/toolset-names.ts +11 -42
- package/adapters/mcp/toolset.ts +16 -90
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +18 -6
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +54 -14
- package/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +1 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +114 -30
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +5 -5
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +14 -2
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -54
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +24 -12
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +1 -12
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
- package/packages/kernel/scheduler/application/eligibility.ts +4 -1
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
- package/packages/kernel/work/application/work-service.ts +1 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +9 -2
- package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
- package/scripts/assemble-template.ts +11 -72
- package/scripts/check-mcp-compatibility.ts +55 -74
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +305 -28
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +3 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +0 -1
- package/scripts/run-governed-gate.ts +2 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +7 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
- package/scripts/sync-generated-authority.mjs +0 -1
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/agent-jobs/integration.ts +2 -2
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- package/src/cli/commands/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +2 -14
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/setup-execution.ts +41 -0
- package/src/cli/controller/completion-orchestrator.ts +1 -1
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +2 -2
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +97 -76
- package/src/cli/controller/operational-plan.ts +1 -1
- package/src/cli/controller/systemd-user.ts +4 -10
- package/src/cli/editing/edit-session.ts +17 -5
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +1 -1
- package/src/cli/local-bridge/console-view-models.ts +1 -1
- package/src/cli/local-bridge/facade-api.ts +0 -2
- package/src/cli/repositories/command-classifier.ts +0 -9
- package/src/cli/repositories/command-scope.ts +2 -2
- package/src/cli/repositories/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +2 -1
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +39 -3
- package/src/cli/repository/inspector.ts +185 -343
- package/src/effects/process-runner.ts +17 -1
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +7 -0
- package/src/runtime/context/semantic-navigation.ts +1 -1
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
- package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
- package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
- package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
- package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
- package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
- package/src/runtime/control-plane/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
- package/src/runtime/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
- package/src/runtime/execution/process-runtime/runtime.ts +20 -16
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +1 -2
- package/src/runtime/maintenance/cleanup.ts +41 -5
- package/src/runtime/plugins/action-application.ts +12 -1
- package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
- package/src/runtime/plugins/browser-adapter.ts +381 -313
- package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
- package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
- package/src/runtime/plugins/browser-registration.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +26 -35
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
- package/src/runtime/plugins/computer-registration.ts +76 -9
- package/src/runtime/plugins/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +166 -1
- package/src/runtime/plugins/google-shared.ts +10 -5
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +45 -40
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/local-system-adapter.ts +1 -99
- package/src/runtime/plugins/store.ts +80 -7
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/recovery/actions.ts +1 -1
- package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/release/release-coordinator.ts +20 -4
- package/src/runtime/root/controller-progression-composition.ts +67 -5
- package/src/runtime/root/package-connector-service.ts +123 -11
- package/src/runtime/root/package-runtime-release.ts +6 -18
- package/src/runtime/root/package-runtime-service.ts +7 -13
- package/src/runtime/root/release-execution-canary.ts +18 -5
- package/src/runtime/root/release-manifest.ts +0 -34
- package/src/runtime/root/release-materialize.ts +0 -78
- package/src/runtime/root/runtime.ts +1 -2
- package/src/runtime/root/scheduled-controller-composition.ts +2 -1
- package/src/runtime/root/service.ts +90 -11
- package/src/runtime/root/types.ts +0 -6
- package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
- package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
- package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/json-files.ts +8 -3
- package/src/runtime/standalone-recovery/core.ts +189 -174
- package/src/runtime/standalone-recovery/entry.ts +62 -30
- package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
- package/src/runtime/workflow/schedules/engine.ts +36 -14
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
- package/supervisor/client.ts +31 -14
- package/supervisor/control-plane.ts +210 -115
- package/supervisor/forge-validators.ts +88 -21
- package/supervisor/native-browser-adapter.ts +393 -28
- package/supervisor/native-messaging/host.ts +21 -2
- package/supervisor/protocol.ts +74 -26
- package/supervisor/server.ts +16 -8
- package/supervisor/store.ts +476 -57
- package/supervisor/types.ts +19 -13
- package/SKILL.md +0 -427
- package/adapters/computer/chatgpt-target-router.ts +0 -103
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- package/scripts/check-skill-version.ts +0 -5
- package/scripts/ensure-codegraph.sh +0 -16
- package/scripts/run-skill-hook.ts +0 -244
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- package/src/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
- 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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
5916
|
-
//
|
|
5917
|
-
//
|
|
5918
|
-
//
|
|
5919
|
-
//
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
const
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
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
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
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
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
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;
|