@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
1
2
|
import { createHash, randomUUID } from 'crypto';
|
|
2
3
|
import { existsSync } from 'fs';
|
|
3
4
|
import { createServer, type IncomingMessage, type ServerResponse } from 'http';
|
|
@@ -42,7 +43,6 @@ import {
|
|
|
42
43
|
runtimeStatus,
|
|
43
44
|
verifyStableRuntime,
|
|
44
45
|
verifyConfiguredRuntimeReleaseSessionStaticGates,
|
|
45
|
-
probeReleaseCutoverReadiness,
|
|
46
46
|
watchdogTick,
|
|
47
47
|
type WatchdogState,
|
|
48
48
|
type RecoveryConfig,
|
|
@@ -64,6 +64,8 @@ import {
|
|
|
64
64
|
} from './release';
|
|
65
65
|
import { RECOVERY_MUTATION_IDENTITY_CONTRACT, RECOVERY_MUTATION_IDENTITY_FIELDS } from './mutation-identity-contract';
|
|
66
66
|
import { readReleaseSession } from '../release/release-session';
|
|
67
|
+
import { readRuntimeOwner } from '../root/ownership';
|
|
68
|
+
import { observeRuntimeStatus } from '../root/status';
|
|
67
69
|
import { migrateReleaseDurableState } from '../release/release-state-migration';
|
|
68
70
|
import {
|
|
69
71
|
advanceConfiguredRuntimeRelease,
|
|
@@ -91,6 +93,37 @@ const RECOVERY_AUTOMATIC_RELEASE_INTERVAL_MS = 15_000;
|
|
|
91
93
|
const RECOVERY_AUTOMATIC_RELEASE_FAILURE_BACKOFF_MAX_MS = 15 * 60_000;
|
|
92
94
|
const RECOVERY_AUTOMATIC_RELEASE_STEP_TIMEOUT_MS = 15 * 60_000;
|
|
93
95
|
|
|
96
|
+
/**
|
|
97
|
+
* A CLI launched from the primary Runtime's Process Runtime cannot own its
|
|
98
|
+
* cutover: stopping Stable A cancels that very CLI before the release
|
|
99
|
+
* transaction can settle. The persistent Recovery daemon/MCP is independent.
|
|
100
|
+
* Compare the serving Runtime's existing process owner with the actual OS
|
|
101
|
+
* parent chain, not a caller-supplied scope or model-provided permission flag.
|
|
102
|
+
*/
|
|
103
|
+
function assertIndependentReleaseCliOwner(controllerHome: string): void {
|
|
104
|
+
const runtime = observeRuntimeStatus(controllerHome);
|
|
105
|
+
const owner = runtime.running ? readRuntimeOwner(controllerHome) : undefined;
|
|
106
|
+
if (!owner) return;
|
|
107
|
+
if (process.platform === 'win32') throw new Error('RECOVERY_CUTOVER_PROCESS_ANCESTRY_UNVERIFIED');
|
|
108
|
+
let pid = process.ppid;
|
|
109
|
+
const visited = new Set<number>();
|
|
110
|
+
for (let depth = 0; depth < 32 && pid > 1; depth += 1) {
|
|
111
|
+
if (pid === owner.pid) {
|
|
112
|
+
throw new Error('RECOVERY_CUTOVER_REQUIRES_INDEPENDENT_OWNER: invoke the Recovery daemon/MCP, not a command owned by the primary Runtime');
|
|
113
|
+
}
|
|
114
|
+
if (visited.has(pid)) throw new Error('RECOVERY_CUTOVER_PROCESS_ANCESTRY_UNVERIFIED');
|
|
115
|
+
visited.add(pid);
|
|
116
|
+
const inspected = spawnSync('ps', ['-o', 'ppid=', '-p', String(pid)], {
|
|
117
|
+
encoding: 'utf8', timeout: 2_000, maxBuffer: 4 * 1024,
|
|
118
|
+
});
|
|
119
|
+
if (inspected.status !== 0) throw new Error('RECOVERY_CUTOVER_PROCESS_ANCESTRY_UNVERIFIED');
|
|
120
|
+
const parent = Number.parseInt(inspected.stdout.trim(), 10);
|
|
121
|
+
if (!Number.isInteger(parent) || parent < 1) throw new Error('RECOVERY_CUTOVER_PROCESS_ANCESTRY_UNVERIFIED');
|
|
122
|
+
pid = parent;
|
|
123
|
+
}
|
|
124
|
+
if (pid > 1) throw new Error('RECOVERY_CUTOVER_PROCESS_ANCESTRY_UNVERIFIED');
|
|
125
|
+
}
|
|
126
|
+
|
|
94
127
|
function option(name: string): string | undefined {
|
|
95
128
|
const index = process.argv.indexOf(name);
|
|
96
129
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
@@ -194,6 +227,7 @@ async function cli(): Promise<void> {
|
|
|
194
227
|
config,
|
|
195
228
|
RECOVERY_RUNTIME_RELEASE_PROVIDER,
|
|
196
229
|
`recovery-auto-release:${process.pid}:${Date.now()}`,
|
|
230
|
+
decision,
|
|
197
231
|
);
|
|
198
232
|
output({ schemaVersion: 1, ok: result.ok, attempted: result.attempted, decision, result });
|
|
199
233
|
} catch (error) {
|
|
@@ -219,11 +253,12 @@ async function cli(): Promise<void> {
|
|
|
219
253
|
}
|
|
220
254
|
case 'list-releases': output(await listReleases(config)); return;
|
|
221
255
|
case 'attest-known-good': output(await attestKnownGood(config)); return;
|
|
222
|
-
case 'rollback-previous': output(await rollbackPrevious(config)); return;
|
|
223
|
-
case 'restart-primary-runtime': output(await restartPrimaryRuntime(config, { forceHealthyRestart: true })); return;
|
|
224
|
-
case 'restart-primary-connector': output(await restartPrimaryConnector(config)); return;
|
|
225
|
-
case 'recover-primary-runtime': output(await recoverPrimaryRuntime(config)); return;
|
|
256
|
+
case 'rollback-previous': assertIndependentReleaseCliOwner(config.controllerHome); output(await rollbackPrevious(config)); return;
|
|
257
|
+
case 'restart-primary-runtime': assertIndependentReleaseCliOwner(config.controllerHome); output(await restartPrimaryRuntime(config, { forceHealthyRestart: true })); return;
|
|
258
|
+
case 'restart-primary-connector': output(await restartPrimaryConnector(config, { forceHealthyRestart: true, requestId: `recovery-cli:${process.pid}:${Date.now()}` })); return;
|
|
259
|
+
case 'recover-primary-runtime': assertIndependentReleaseCliOwner(config.controllerHome); output(await recoverPrimaryRuntime(config)); return;
|
|
226
260
|
case 'activate-runtime-release': {
|
|
261
|
+
assertIndependentReleaseCliOwner(config.controllerHome);
|
|
227
262
|
const releasePath = option('--release-manifest') ?? option('--release-path');
|
|
228
263
|
const expectedActiveReleaseId = option('--expected-active-release');
|
|
229
264
|
const expectedAuthorityRevisionRaw = option('--expected-authority-revision');
|
|
@@ -238,14 +273,18 @@ async function cli(): Promise<void> {
|
|
|
238
273
|
}));
|
|
239
274
|
return;
|
|
240
275
|
}
|
|
241
|
-
case 'stage-and-activate-runtime-release': output(await stageAndActivateConfiguredRuntimeRelease(config, {}, `recovery-cli:${process.pid}:${Date.now()}`)); return;
|
|
276
|
+
case 'stage-and-activate-runtime-release': assertIndependentReleaseCliOwner(config.controllerHome); output(await stageAndActivateConfiguredRuntimeRelease(config, {}, `recovery-cli:${process.pid}:${Date.now()}`)); return;
|
|
242
277
|
case 'release-session-status': {
|
|
243
278
|
const sessionId = option('--session-id');
|
|
244
279
|
if (!sessionId) throw new Error('RECOVERY_RELEASE_SESSION_ID_REQUIRED');
|
|
245
280
|
output(readReleaseSession(config.controllerHome, sessionId) ?? null);
|
|
246
281
|
return;
|
|
247
282
|
}
|
|
248
|
-
case 'release-session-advance':
|
|
283
|
+
case 'release-session-advance': {
|
|
284
|
+
assertIndependentReleaseCliOwner(config.controllerHome);
|
|
285
|
+
output(await advanceConfiguredRuntimeRelease(config, RECOVERY_RUNTIME_RELEASE_PROVIDER, `recovery-cli:${process.pid}:${Date.now()}`));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
249
288
|
case 'release-session-prepare': output(await prepareConfiguredRuntimeReleaseSession(config, {}, `recovery-cli:${process.pid}:${Date.now()}`)); return;
|
|
250
289
|
case 'release-session-static-verify': {
|
|
251
290
|
const sessionId = option('--session-id');
|
|
@@ -260,6 +299,7 @@ async function cli(): Promise<void> {
|
|
|
260
299
|
return;
|
|
261
300
|
}
|
|
262
301
|
case 'release-session-cutover': {
|
|
302
|
+
assertIndependentReleaseCliOwner(config.controllerHome);
|
|
263
303
|
const sessionId = option('--session-id');
|
|
264
304
|
if (!sessionId) throw new Error('RECOVERY_RELEASE_SESSION_ID_REQUIRED');
|
|
265
305
|
output(await cutoverConfiguredRuntimeReleaseSession(config, sessionId, `recovery-cli:${process.pid}:${Date.now()}`));
|
|
@@ -272,6 +312,7 @@ async function cli(): Promise<void> {
|
|
|
272
312
|
return;
|
|
273
313
|
}
|
|
274
314
|
case 'release-session-rollback': {
|
|
315
|
+
assertIndependentReleaseCliOwner(config.controllerHome);
|
|
275
316
|
const sessionId = option('--session-id');
|
|
276
317
|
if (!sessionId) throw new Error('RECOVERY_RELEASE_SESSION_ID_REQUIRED');
|
|
277
318
|
output(await rollbackConfiguredRuntimeReleaseSession(config, sessionId, {}, `recovery-cli:${process.pid}:${Date.now()}`));
|
|
@@ -439,7 +480,7 @@ export const RECOVERY_TOOLS = [
|
|
|
439
480
|
{ name: 'attest_known_good', description: 'Record the active release as known-good only after full independent verification succeeds.', inputSchema: mutationInputSchema() },
|
|
440
481
|
{ name: 'rollback_previous', description: 'While Canonical Runtime is stopped, atomically restore its attested previous whole-Runtime release and SQLite backup.', inputSchema: mutationInputSchema() },
|
|
441
482
|
{ name: 'restart_primary_runtime', description: 'Explicitly restart the installed canonical Forge Runtime service, including when currently healthy, then require whole-Runtime verification after exact Recovery machine identity matches.', inputSchema: mutationInputSchema() },
|
|
442
|
-
{ name: 'restart_primary_connector', description: '
|
|
483
|
+
{ name: 'restart_primary_connector', description: 'Explicitly restart the configured primary OAuth/Connector service, including when endpoint probes pass, only after exact Recovery machine identity and local Canonical Runtime verification succeed; verify the listener and public endpoint again afterward.', inputSchema: mutationInputSchema() },
|
|
443
484
|
{ name: 'recover_primary_runtime', description: 'Stop the canonical Runtime, restore the attested previous whole-Runtime release and SQLite backup, restart it, and require verification.', inputSchema: mutationInputSchema() },
|
|
444
485
|
{ name: 'activate_runtime_release', description: 'Activate an already staged immutable Runtime release only if machine identity and caller-observed active release/authority revision are still current. Reverse activation of current.previous is rejected; use rollback_previous/recover_primary_runtime instead.', inputSchema: mutationInputSchema({ release_path: { type: 'string', minLength: 8, maxLength: 1024, description: 'Absolute path to the staged immutable Runtime release directory.' }, expected_active_release_id: { type: 'string', minLength: 1, maxLength: 256 }, expected_authority_revision: { type: 'integer', minimum: 1 } }, ['release_path', 'expected_active_release_id', 'expected_authority_revision']) },
|
|
445
486
|
{ name: 'pin_runtime_release', description: 'Pin one extant legacy/home-bound immutable Runtime release so retention preserves it for explicit Runtime-only recovery activation. Portable source candidates are rejected and must use ReleaseSession.', inputSchema: mutationInputSchema({ release_path: { type: 'string', minLength: 8, maxLength: 1024, description: 'Absolute path to the immutable Runtime release directory or manifest.' } }, ['release_path']) },
|
|
@@ -781,7 +822,7 @@ export async function dispatchRecoveryTool(config: RecoveryConfig, name: string,
|
|
|
781
822
|
case 'restart_primary_connector': {
|
|
782
823
|
if (!requestId(args.request_id)) throw new Error('RECOVERY_REQUEST_ID_REQUIRED');
|
|
783
824
|
assertRecoveryGatewayMutationIdentity(config, args);
|
|
784
|
-
return mutationResponse(config, await restartPrimaryConnector(config, { requestId: `recovery-gateway:${args.request_id}
|
|
825
|
+
return mutationResponse(config, await restartPrimaryConnector(config, { requestId: `recovery-gateway:${args.request_id}`, forceHealthyRestart: true }));
|
|
785
826
|
}
|
|
786
827
|
case 'recover_primary_runtime': {
|
|
787
828
|
if (!requestId(args.request_id)) throw new Error('RECOVERY_REQUEST_ID_REQUIRED');
|
|
@@ -995,6 +1036,14 @@ async function runAutomaticReleaseReconciliationStep(
|
|
|
995
1036
|
: 'automatic release reconciliation failed';
|
|
996
1037
|
throw new Error(`RECOVERY_AUTOMATIC_RELEASE_STEP_FAILED: ${detail.slice(0, 500)}`);
|
|
997
1038
|
}
|
|
1039
|
+
// An idle-window timeout is a legitimate no-cutover outcome, but not
|
|
1040
|
+
// forward progress. Feed it into the existing phase/revision backoff so
|
|
1041
|
+
// repeated drains cannot monopolize the Connector every 15 seconds.
|
|
1042
|
+
if (parsed.decision?.action === 'cutover'
|
|
1043
|
+
&& typeof parsed.result?.detail === 'string'
|
|
1044
|
+
&& parsed.result.detail.startsWith('RELEASE_SESSION_CUTOVER_DEFERRED_FOR_MCP_ACTIVITY')) {
|
|
1045
|
+
throw new Error(`RECOVERY_AUTOMATIC_RELEASE_CUTOVER_DEFERRED: ${parsed.result.detail.slice(0, 500)}`);
|
|
1046
|
+
}
|
|
998
1047
|
if (parsed.decision?.action) {
|
|
999
1048
|
process.stdout.write(JSON.stringify({
|
|
1000
1049
|
at: new Date().toISOString(),
|
|
@@ -1017,27 +1066,10 @@ async function startAutomaticReleaseReconciliation(config: RecoveryConfig): Prom
|
|
|
1017
1066
|
() => configuredRuntimeReleaseSourceState(config),
|
|
1018
1067
|
);
|
|
1019
1068
|
observed.fingerprint = decision.required ? releaseReconciliationFingerprint(decision) : undefined;
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
at: new Date().toISOString(),
|
|
1025
|
-
action: 'automatic_release_cutover_deferred',
|
|
1026
|
-
reason: gate.detail,
|
|
1027
|
-
sessionId: decision.session?.sessionId,
|
|
1028
|
-
phase: decision.session?.phase,
|
|
1029
|
-
activeSessions: gate.activeSessions,
|
|
1030
|
-
activePosts: gate.activePosts,
|
|
1031
|
-
activeStreams: gate.activeStreams,
|
|
1032
|
-
initializing: gate.initializing,
|
|
1033
|
-
latestActivityAgeMs: gate.latestActivityAgeMs,
|
|
1034
|
-
}) + '\n');
|
|
1035
|
-
} else {
|
|
1036
|
-
await runAutomaticReleaseReconciliationStep(config, observed, decision);
|
|
1037
|
-
}
|
|
1038
|
-
} else {
|
|
1039
|
-
await runAutomaticReleaseReconciliationStep(config, observed, decision);
|
|
1040
|
-
}
|
|
1069
|
+
// ReleaseSession cutover owns the one Connector drain/quietness fence.
|
|
1070
|
+
// A read-only busy pre-gate here would prevent that drain from ever
|
|
1071
|
+
// starting under sustained MCP traffic, starving the verified candidate.
|
|
1072
|
+
if (decision.required) await runAutomaticReleaseReconciliationStep(config, observed, decision);
|
|
1041
1073
|
} catch (error) {
|
|
1042
1074
|
failure = error instanceof Error ? error.message : String(error);
|
|
1043
1075
|
process.stderr.write(`automatic release reconciliation failed: ${failure}\n`);
|
|
@@ -24,6 +24,16 @@ export type RecoveryOperationLockAttempt =
|
|
|
24
24
|
| { acquired: true; handle: RecoveryOperationLockHandle }
|
|
25
25
|
| { acquired: false; owner: RecoveryOperationLockRecord };
|
|
26
26
|
|
|
27
|
+
/** Preserve the competing owner identities when lock acquisition detects a race. */
|
|
28
|
+
export class RecoveryOperationLockRaceError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
readonly existing: RecoveryOperationLockRecord,
|
|
31
|
+
readonly latest: RecoveryOperationLockRecord | undefined,
|
|
32
|
+
) {
|
|
33
|
+
super('RECOVERY_OPERATION_LOCK_RACE');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
function processStartTime(pid: number): string | undefined {
|
|
28
38
|
const result = spawnSync('/bin/ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
29
39
|
encoding: 'utf8',
|
|
@@ -123,7 +133,7 @@ export function acquireRecoveryOperationLock(input: {
|
|
|
123
133
|
if (!latest || latest.instanceId !== existing.instanceId) {
|
|
124
134
|
if (attempt === 1) {
|
|
125
135
|
if (latest && recoveryOperationLockOwnerAlive(latest)) return { acquired: false, owner: latest };
|
|
126
|
-
throw new
|
|
136
|
+
throw new RecoveryOperationLockRaceError(existing, latest);
|
|
127
137
|
}
|
|
128
138
|
continue;
|
|
129
139
|
}
|
|
@@ -51,6 +51,7 @@ import type {
|
|
|
51
51
|
const execFileAsync = promisify(execFile);
|
|
52
52
|
const LIVE_MAINTENANCE_OPERATION = 'runtime_maintenance_apply';
|
|
53
53
|
const EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
|
|
54
|
+
const CONTROLLER_PROGRESSION_OPERATION = 'controller_progression';
|
|
54
55
|
const BROWSER_PROBE_OPERATION = 'browser_probe';
|
|
55
56
|
const GITHUB_ISSUE_WATCH_OPERATION = 'github_issue_watch';
|
|
56
57
|
const DEFAULT_EXTERNAL_CONTROLLER_WAKE_TIMEOUT_MS = 60_000;
|
|
@@ -60,6 +61,7 @@ function isDeterministicSchedule(schedule: RepositorySchedule): boolean {
|
|
|
60
61
|
// Kernel executes only typed, bounded dispatch operations.
|
|
61
62
|
return schedule.action.operation === LIVE_MAINTENANCE_OPERATION
|
|
62
63
|
|| schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
64
|
+
|| schedule.action.operation === CONTROLLER_PROGRESSION_OPERATION
|
|
63
65
|
|| schedule.action.operation === BROWSER_PROBE_OPERATION
|
|
64
66
|
|| schedule.action.operation === GITHUB_ISSUE_WATCH_OPERATION;
|
|
65
67
|
}
|
|
@@ -87,6 +89,7 @@ async function workspaceDirty(controllerHome: string, repoId: string): Promise<b
|
|
|
87
89
|
|
|
88
90
|
function workBoundScheduleWorkId(schedule: RepositorySchedule): string | undefined {
|
|
89
91
|
if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
92
|
+
&& schedule.action.operation !== CONTROLLER_PROGRESSION_OPERATION
|
|
90
93
|
&& schedule.action.operation !== BROWSER_PROBE_OPERATION) return undefined;
|
|
91
94
|
const value = schedule.action.arguments?.work_id;
|
|
92
95
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
@@ -246,7 +249,7 @@ export function classifyChatgptWakeFailure(reason: string): ChatgptWakeFailureCl
|
|
|
246
249
|
return 'ordinary_failure';
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
async function
|
|
252
|
+
async function executeControllerProgression(
|
|
250
253
|
controllerHome: string,
|
|
251
254
|
schedule: RepositorySchedule,
|
|
252
255
|
occurrence: ScheduleOccurrence,
|
|
@@ -311,12 +314,20 @@ async function executeExternalControllerWake(
|
|
|
311
314
|
);
|
|
312
315
|
}
|
|
313
316
|
|
|
317
|
+
if (schedule.action.operation === CONTROLLER_PROGRESSION_OPERATION
|
|
318
|
+
&& (typeof args.controller_binding_id !== 'string' || !args.controller_binding_id.trim())) {
|
|
319
|
+
throw new Error(`SCHEDULE_CONTROLLER_PROGRESSION_BINDING_REQUIRED:${workId}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
314
322
|
const hadLegacySessionIdentity = typeof args.controller_session_id === 'string' && args.controller_session_id.trim().length > 0;
|
|
315
323
|
|
|
316
324
|
// Explicit compatibility migration for schedules written before Kernel V2 B4.
|
|
317
325
|
// Provider metadata is copied once into the adapter-owned binding and the
|
|
318
326
|
// Schedule is rewritten to exact Work/ControllerSession/ControllerBinding ids.
|
|
319
327
|
if (!bindingRecord) {
|
|
328
|
+
if (schedule.action.operation === CONTROLLER_PROGRESSION_OPERATION) {
|
|
329
|
+
throw new Error(`SCHEDULE_CONTROLLER_PROGRESSION_BINDING_NOT_FOUND:${workId}`);
|
|
330
|
+
}
|
|
320
331
|
const binding = ensureScheduledControllerBinding(
|
|
321
332
|
{ controllerHome, repoId: schedule.repoId },
|
|
322
333
|
{ workId, session: retainedSession, scheduleName: schedule.name, args },
|
|
@@ -388,6 +399,8 @@ async function executeExternalControllerWake(
|
|
|
388
399
|
continuationHint,
|
|
389
400
|
allowSemanticWaitRecovery: controllerType === 'chatgpt',
|
|
390
401
|
scheduleName: schedule.name,
|
|
402
|
+
scheduleId: schedule.scheduleId,
|
|
403
|
+
scheduleOccurrenceId: occurrence.occurrenceId,
|
|
391
404
|
bindingArgs: args,
|
|
392
405
|
},
|
|
393
406
|
);
|
|
@@ -442,13 +455,17 @@ async function executeExternalControllerWake(
|
|
|
442
455
|
|
|
443
456
|
const supervisorEnrollment = progression.status === 'chatgpt_enrolled'
|
|
444
457
|
? progression.supervisorEnrollment
|
|
445
|
-
:
|
|
458
|
+
: progression.status === 'scheduled_supervisor_woken'
|
|
459
|
+
? { status: 'enrolled' as const, taskId: progression.taskId, effectId: progression.effectId }
|
|
460
|
+
: { status: 'not_eligible' as const };
|
|
461
|
+
const supervisorOwnsTurn = progression.status === 'chatgpt_enrolled' || progression.status === 'scheduled_supervisor_woken';
|
|
446
462
|
updateSchedule(controllerHome, schedule.repoId, schedule.scheduleId, (current) => ({
|
|
447
463
|
lastTriggeredAt: timestamp,
|
|
448
464
|
lastOccurrenceId: occurrence.occurrenceId,
|
|
449
465
|
consecutiveFailures: 0,
|
|
450
466
|
nextEligibleAt: undefined,
|
|
451
467
|
...(supervisorEnrollment.status === 'enrolled'
|
|
468
|
+
&& schedule.action.operation !== CONTROLLER_PROGRESSION_OPERATION
|
|
452
469
|
? { enabled: false, pausedReason: 'workflow_supervisor_owns_outer_turn' }
|
|
453
470
|
: current.enabled ? { pausedReason: undefined } : {}),
|
|
454
471
|
}));
|
|
@@ -457,18 +474,22 @@ async function executeExternalControllerWake(
|
|
|
457
474
|
status: 'dispatched',
|
|
458
475
|
reason: progression.status === 'chatgpt_enrolled'
|
|
459
476
|
? `Workflow Supervisor reserved the exact ChatGPT effect ${progression.supervisorEnrollment.effectId ?? progression.relayScopeId}; Scheduler issued no provider send.`
|
|
460
|
-
: progression.
|
|
461
|
-
? `
|
|
462
|
-
:
|
|
477
|
+
: progression.status === 'scheduled_supervisor_woken'
|
|
478
|
+
? `Scheduler occurrence ${occurrence.occurrenceId} woke the same Workflow Supervisor task and reserved effect ${progression.effectId}; Scheduler issued no provider send.`
|
|
479
|
+
: progression.reused
|
|
480
|
+
? `Controller continuation occurrence ${occurrence.occurrenceId} was already durably dispatched; external replay suppressed and semantic round closure is still pending.`
|
|
481
|
+
: `ControllerHost accepted durable binding ${bindingRecord.binding.bindingId} for Work ${workId}; dispatch ${progression.providerDispatchReceiptId ?? progression.relayScopeId}; semantic round closure is still pending.`,
|
|
463
482
|
});
|
|
464
483
|
appendWorkEvidence(workStore, workId, {
|
|
465
484
|
evidenceId: dispatchedOccurrence.occurrenceId,
|
|
466
|
-
title:
|
|
485
|
+
title: supervisorOwnsTurn
|
|
467
486
|
? 'scheduled Workflow Supervisor effect reserved'
|
|
468
487
|
: 'scheduled Controller continuation dispatched',
|
|
469
|
-
summary: progression.status === '
|
|
470
|
-
? `Schedule ${schedule.scheduleId}
|
|
471
|
-
:
|
|
488
|
+
summary: progression.status === 'scheduled_supervisor_woken'
|
|
489
|
+
? `Schedule ${schedule.scheduleId} occurrence ${dispatchedOccurrence.occurrenceId} woke the same Work-bound Supervisor task; Supervisor owns ChatGPT delivery.`
|
|
490
|
+
: progression.status === 'chatgpt_enrolled'
|
|
491
|
+
? `Schedule ${schedule.scheduleId} materialized the exact ControllerRound occurrence and Supervisor effect; Supervisor owns ChatGPT delivery.`
|
|
492
|
+
: `Schedule ${schedule.scheduleId} occurrence ${dispatchedOccurrence.occurrenceId} resumed the exact Work-bound ControllerSession through ControllerHost.`,
|
|
472
493
|
detailLevel: 'summary',
|
|
473
494
|
});
|
|
474
495
|
return dispatchedOccurrence;
|
|
@@ -724,8 +745,9 @@ export async function evaluateSchedule(
|
|
|
724
745
|
}
|
|
725
746
|
|
|
726
747
|
const args = schedule.action.arguments ?? {};
|
|
727
|
-
if (schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
728
|
-
|
|
748
|
+
if (schedule.action.operation === EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
749
|
+
|| schedule.action.operation === CONTROLLER_PROGRESSION_OPERATION) {
|
|
750
|
+
return executeControllerProgression(controllerHome, schedule, occurrence, timestamp, args);
|
|
729
751
|
}
|
|
730
752
|
|
|
731
753
|
if (schedule.action.operation === GITHUB_ISSUE_WATCH_OPERATION) {
|
|
@@ -772,7 +794,7 @@ export async function evaluateSchedule(
|
|
|
772
794
|
? `${args.continuation_prompt}\n\nGitHub issue watcher detected: ${issueSummary}`
|
|
773
795
|
: `GitHub issue watcher detected actionable issue changes: ${issueSummary}. Read current Work/Plan/evidence and process these issues through the bounded repair workflow.`,
|
|
774
796
|
};
|
|
775
|
-
const wake = await
|
|
797
|
+
const wake = await executeControllerProgression(controllerHome, schedule, occurrence, timestamp, wakeArgs, evidence, { transientWakeFailure: true });
|
|
776
798
|
if (wake.status !== 'failed') persistObservation();
|
|
777
799
|
return wake;
|
|
778
800
|
} catch (error) {
|
|
@@ -836,7 +858,7 @@ export async function evaluateSchedule(
|
|
|
836
858
|
: `Scheduled browser watcher ${schedule.scheduleId} detected that authentication is required (${probe.authReason ?? 'login marker'}). Inspect the external dependency and request user login only if it cannot be restored safely.`;
|
|
837
859
|
if (workId) {
|
|
838
860
|
const wakeArgs = { ...args, continuation_prompt: authPrompt };
|
|
839
|
-
return
|
|
861
|
+
return executeControllerProgression(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, authReason: probe.authReason });
|
|
840
862
|
}
|
|
841
863
|
const controllerType = typeof args.controller_type === 'string' ? args.controller_type.trim() : 'chatgpt';
|
|
842
864
|
if (controllerType !== 'chatgpt') {
|
|
@@ -917,7 +939,7 @@ export async function evaluateSchedule(
|
|
|
917
939
|
? args.continuation_prompt
|
|
918
940
|
: `Scheduled browser watcher ${schedule.scheduleId} detected a changed external observation. Read the current external state, correlate it with Work ${workId}, and continue only the bounded Work objective.`,
|
|
919
941
|
};
|
|
920
|
-
return
|
|
942
|
+
return executeControllerProgression(controllerHome, observedSchedule, occurrence, timestamp, wakeArgs, { ...observationEvidence, observationStatus });
|
|
921
943
|
} catch (error) {
|
|
922
944
|
const reason = error instanceof Error ? error.message : String(error);
|
|
923
945
|
const failed = applyScheduleFailure(controllerHome, schedule.scheduleId, schedule.repoId, occurrence.occurrenceId, {
|
|
@@ -5,6 +5,7 @@ import { resolveControllerHome } from '../../../cli/repositories/controller-home
|
|
|
5
5
|
import { getSchedule, saveSchedule } from '../../../../packages/kernel/scheduler/api/index';
|
|
6
6
|
|
|
7
7
|
const EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
|
|
8
|
+
const CONTROLLER_PROGRESSION_OPERATION = 'controller_progression';
|
|
8
9
|
|
|
9
10
|
export interface PendingContinuationActivationMarker {
|
|
10
11
|
schemaVersion: 1;
|
|
@@ -53,8 +54,9 @@ export function queuePendingContinuationActivation(
|
|
|
53
54
|
scheduleId: string,
|
|
54
55
|
): PendingContinuationActivationMarker {
|
|
55
56
|
const schedule = getSchedule(controllerHome, repoId, scheduleId);
|
|
56
|
-
if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
57
|
-
|
|
57
|
+
if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
58
|
+
&& schedule.action.operation !== CONTROLLER_PROGRESSION_OPERATION) {
|
|
59
|
+
throw new Error('PENDING_CONTINUATION_ACTIVATION_REQUIRES_WORK_CONTINUATION');
|
|
58
60
|
}
|
|
59
61
|
const workId = schedule.action.arguments?.work_id;
|
|
60
62
|
if (typeof workId !== 'string' || !workId.trim()) {
|
|
@@ -108,7 +110,8 @@ export function applyPendingContinuationActivations(controllerHome: string): Pen
|
|
|
108
110
|
const marker = readMarker(path);
|
|
109
111
|
const schedule = getSchedule(controllerHome, marker.repoId, marker.scheduleId);
|
|
110
112
|
const workId = schedule.action.arguments?.work_id;
|
|
111
|
-
if (schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
113
|
+
if ((schedule.action.operation !== EXTERNAL_CONTROLLER_WAKE_OPERATION
|
|
114
|
+
&& schedule.action.operation !== CONTROLLER_PROGRESSION_OPERATION) || workId !== marker.workId) {
|
|
112
115
|
results.push({ ...marker, status: 'failed', reason: 'Schedule no longer targets the queued Work continuation.' });
|
|
113
116
|
continue;
|
|
114
117
|
}
|
|
@@ -122,7 +125,11 @@ export function applyPendingContinuationActivations(controllerHome: string): Pen
|
|
|
122
125
|
results.push({ ...marker, status: 'already_active' });
|
|
123
126
|
continue;
|
|
124
127
|
}
|
|
125
|
-
saveSchedule(controllerHome, {
|
|
128
|
+
saveSchedule(controllerHome, {
|
|
129
|
+
...schedule,
|
|
130
|
+
action: { ...schedule.action, operation: CONTROLLER_PROGRESSION_OPERATION },
|
|
131
|
+
policy: { ...schedule.policy, shadowMode: false },
|
|
132
|
+
});
|
|
126
133
|
rmSync(path, { force: true });
|
|
127
134
|
results.push({ ...marker, status: 'activated' });
|
|
128
135
|
} catch (error) {
|
|
@@ -28,6 +28,13 @@ import type {
|
|
|
28
28
|
export type ContinuationControllerType = Exclude<ControllerType, 'human'>;
|
|
29
29
|
export type WorkScheduleMode = 'continuation' | 'browser_watch' | 'browser_keepalive';
|
|
30
30
|
|
|
31
|
+
const CONTROLLER_PROGRESSION_OPERATION = 'controller_progression';
|
|
32
|
+
const LEGACY_EXTERNAL_CONTROLLER_WAKE_OPERATION = 'external_controller_wake';
|
|
33
|
+
|
|
34
|
+
function isWorkContinuationOperation(operation: string): boolean {
|
|
35
|
+
return operation === CONTROLLER_PROGRESSION_OPERATION || operation === LEGACY_EXTERNAL_CONTROLLER_WAKE_OPERATION;
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
export interface WorkContinuationScheduleInput {
|
|
32
39
|
workId?: string;
|
|
33
40
|
scheduleMode?: WorkScheduleMode;
|
|
@@ -93,7 +100,7 @@ function activeWork(controllerHome: string, repoId: string, workId: string): Wor
|
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
function continuationSchedule(schedule: RepositorySchedule): RepositorySchedule {
|
|
96
|
-
if (!
|
|
103
|
+
if (!isWorkContinuationOperation(schedule.action.operation) && schedule.action.operation !== 'browser_probe') {
|
|
97
104
|
throw new Error(`SCHEDULE_NOT_WORK_CONTINUATION: ${schedule.scheduleId}`);
|
|
98
105
|
}
|
|
99
106
|
return schedule;
|
|
@@ -194,7 +201,7 @@ export function createWorkContinuationSchedule(
|
|
|
194
201
|
}
|
|
195
202
|
if (!requestedWorkId && scheduleMode === 'browser_keepalive' && controllerType !== 'chatgpt') throw new Error('STANDALONE_BROWSER_KEEPALIVE_CHATGPT_REQUIRED');
|
|
196
203
|
const trigger = normalizedTrigger(input);
|
|
197
|
-
const operation = scheduleMode === 'continuation' ?
|
|
204
|
+
const operation = scheduleMode === 'continuation' ? CONTROLLER_PROGRESSION_OPERATION : 'browser_probe';
|
|
198
205
|
const name = input.scheduleName?.trim()
|
|
199
206
|
|| (scheduleMode === 'browser_watch' ? `Watch external state for Work ${work!.workId}`
|
|
200
207
|
: scheduleMode === 'browser_keepalive'
|
|
@@ -240,7 +247,7 @@ export function createWorkContinuationSchedule(
|
|
|
240
247
|
// Work may legitimately observe several independent external targets.
|
|
241
248
|
if (scheduleMode === 'continuation') {
|
|
242
249
|
const existing = listSchedules(controllerHome, repoId)
|
|
243
|
-
.filter((candidate) => candidate.action.operation
|
|
250
|
+
.filter((candidate) => isWorkContinuationOperation(candidate.action.operation))
|
|
244
251
|
.filter((candidate) => candidate.action.arguments?.work_id === work!.workId);
|
|
245
252
|
const authoritative = [...existing].reverse().find((candidate) => candidate.enabled) ?? existing.at(-1);
|
|
246
253
|
if (authoritative) {
|
|
@@ -302,7 +309,7 @@ export function listWorkContinuationSchedules(
|
|
|
302
309
|
options: WorkContinuationScheduleListOptions = {},
|
|
303
310
|
): { schedules: RepositorySchedule[]; occurrences?: ScheduleOccurrence[] } {
|
|
304
311
|
const schedules = listSchedules(controllerHome, repoId)
|
|
305
|
-
.filter((entry) =>
|
|
312
|
+
.filter((entry) => isWorkContinuationOperation(entry.action.operation) || entry.action.operation === 'browser_probe')
|
|
306
313
|
.filter((entry) => !options.workId || entry.action.arguments?.work_id === options.workId);
|
|
307
314
|
if (!options.includeOccurrences) return { schedules };
|
|
308
315
|
const scheduleIds = new Set(schedules.map((entry) => entry.scheduleId));
|
|
@@ -342,6 +349,44 @@ export function resumeWorkContinuationSchedule(controllerHome: string, repoId: s
|
|
|
342
349
|
const schedule = continuationSchedule(getSchedule(controllerHome, repoId, scheduleId));
|
|
343
350
|
const workId = typeof schedule.action.arguments?.work_id === 'string' ? schedule.action.arguments.work_id : '';
|
|
344
351
|
activeWork(controllerHome, repoId, workId);
|
|
352
|
+
if (schedule.action.operation === LEGACY_EXTERNAL_CONTROLLER_WAKE_OPERATION) {
|
|
353
|
+
const trigger = schedule.trigger;
|
|
354
|
+
const migrated = createWorkContinuationSchedule(controllerHome, repoId, {
|
|
355
|
+
workId,
|
|
356
|
+
scheduleName: schedule.name,
|
|
357
|
+
controllerType: typeof schedule.action.arguments?.controller_type === 'string'
|
|
358
|
+
? schedule.action.arguments.controller_type as ContinuationControllerType
|
|
359
|
+
: undefined,
|
|
360
|
+
continuationPrompt: typeof schedule.action.arguments?.continuation_hint === 'string'
|
|
361
|
+
? schedule.action.arguments.continuation_hint
|
|
362
|
+
: typeof schedule.action.arguments?.continuation_prompt === 'string'
|
|
363
|
+
? schedule.action.arguments.continuation_prompt
|
|
364
|
+
: undefined,
|
|
365
|
+
triggerType: trigger.type,
|
|
366
|
+
everyMinutes: trigger.everyMinutes,
|
|
367
|
+
cronExpression: trigger.cronExpression,
|
|
368
|
+
timezone: trigger.timezone,
|
|
369
|
+
catchUpMinutes: trigger.catchUpMinutes,
|
|
370
|
+
calendarAt: trigger.calendarAt,
|
|
371
|
+
condition: trigger.condition,
|
|
372
|
+
eventName: trigger.eventName,
|
|
373
|
+
dependencyJobIds: trigger.dependencyJobIds,
|
|
374
|
+
maxFailures: schedule.policy.maxFailures,
|
|
375
|
+
cooldownMinutes: schedule.policy.cooldownMinutes,
|
|
376
|
+
dailyBudgetMinutes: schedule.policy.dailyBudgetMinutes,
|
|
377
|
+
shadowMode: schedule.policy.shadowMode,
|
|
378
|
+
backoffBaseMinutes: schedule.policy.backoffBaseMinutes,
|
|
379
|
+
backoffMaxMinutes: schedule.policy.backoffMaxMinutes,
|
|
380
|
+
stopConditions: schedule.stopConditions,
|
|
381
|
+
}).schedule;
|
|
382
|
+
return saveSchedule(controllerHome, {
|
|
383
|
+
...migrated,
|
|
384
|
+
enabled: true,
|
|
385
|
+
pausedReason: undefined,
|
|
386
|
+
consecutiveFailures: 0,
|
|
387
|
+
nextEligibleAt: undefined,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
345
390
|
return saveSchedule(controllerHome, {
|
|
346
391
|
...schedule,
|
|
347
392
|
enabled: true,
|
|
@@ -378,7 +423,7 @@ export async function triggerWorkContinuationRepositoryEvent(
|
|
|
378
423
|
): Promise<Array<{ scheduleId: string; occurrenceId?: string; status?: string }>> {
|
|
379
424
|
const schedules = listWorkContinuationSchedules(controllerHome, repoId, { workId: input.workId }).schedules
|
|
380
425
|
.filter((schedule) => schedule.enabled)
|
|
381
|
-
.filter((schedule) => schedule.action.operation
|
|
426
|
+
.filter((schedule) => isWorkContinuationOperation(schedule.action.operation))
|
|
382
427
|
.filter((schedule) => schedule.trigger.type === 'repository-event' && schedule.trigger.eventName === eventName);
|
|
383
428
|
const triggered: Array<{ scheduleId: string; occurrenceId?: string; status?: string }> = [];
|
|
384
429
|
for (const schedule of schedules) {
|
|
@@ -404,7 +449,7 @@ export function eventDrivenContinuationSchedule(
|
|
|
404
449
|
): RepositorySchedule {
|
|
405
450
|
const workSchedules = listWorkContinuationSchedules(controllerHome, repoId, { workId: input.workId }).schedules;
|
|
406
451
|
const existing = workSchedules
|
|
407
|
-
.filter((schedule) => schedule.action.operation
|
|
452
|
+
.filter((schedule) => isWorkContinuationOperation(schedule.action.operation))
|
|
408
453
|
.reverse()
|
|
409
454
|
.find((schedule) => schedule.enabled)
|
|
410
455
|
?? workSchedules.at(-1);
|
package/supervisor/client.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { createConnection } from 'node:net';
|
|
3
3
|
import { workflowSupervisorSocketPath } from './paths';
|
|
4
|
-
import type {
|
|
4
|
+
import type { WorkflowSupervisorConsumerStatus, WorkflowSupervisorContinuationProof, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTaskStall, WorkflowSupervisorTerminalState } from './types';
|
|
5
5
|
|
|
6
6
|
interface RpcResponse<T> { id: string; ok: boolean; result?: T; error?: { code?: string; message?: string } }
|
|
7
7
|
|
|
@@ -63,12 +63,16 @@ export async function getWorkflowSupervisorTask(forgeHome: string, taskId: strin
|
|
|
63
63
|
|
|
64
64
|
export async function reviseWorkflowSupervisorTaskObjective(forgeHome: string, input: {
|
|
65
65
|
taskId: string; objective: string; expectedObjectiveSha256: string; requestId: string; authorizedBy?: string;
|
|
66
|
-
|
|
66
|
+
workId?: string; requirementId?: string; expectedCompletionContractSha256?: string;
|
|
67
|
+
}): Promise<{ task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; completionContractSha256: string; semanticBindingChanged: boolean; refreshedEffect?: WorkflowSupervisorEffect }> {
|
|
67
68
|
return await rpc(forgeHome, 'task_revise_objective', {
|
|
68
69
|
task_id: input.taskId,
|
|
69
70
|
objective: input.objective,
|
|
70
71
|
expected_objective_sha256: input.expectedObjectiveSha256,
|
|
71
72
|
request_id: input.requestId,
|
|
73
|
+
...(input.workId?.trim() ? { work_id: input.workId.trim() } : {}),
|
|
74
|
+
...(input.requirementId?.trim() ? { requirement_id: input.requirementId.trim() } : {}),
|
|
75
|
+
...(input.expectedCompletionContractSha256?.trim() ? { expected_completion_contract_sha256: input.expectedCompletionContractSha256.trim() } : {}),
|
|
72
76
|
...(input.authorizedBy?.trim() ? { authorized_by: input.authorizedBy.trim() } : {}),
|
|
73
77
|
}, SUPERVISOR_RPC_MUTATION_TIMEOUT_MS);
|
|
74
78
|
}
|
|
@@ -89,6 +93,13 @@ export async function listWorkflowSupervisorTasks(forgeHome: string, activeOnly
|
|
|
89
93
|
const result = await rpc<{ tasks: WorkflowSupervisorTask[] }>(forgeHome, 'task_list', { active_only: activeOnly });
|
|
90
94
|
return result.tasks;
|
|
91
95
|
}
|
|
96
|
+
/** Explicit read-only terminal history. Native ChatGPT archive is not inferred. */
|
|
97
|
+
export async function listWorkflowSupervisorTerminalTasks(forgeHome: string): Promise<Array<{ task: WorkflowSupervisorTask; terminal: WorkflowSupervisorTerminalState }>> {
|
|
98
|
+
const result = await rpc<{ terminalTasks: Array<{ task: WorkflowSupervisorTask; terminal: WorkflowSupervisorTerminalState }> }>(
|
|
99
|
+
forgeHome, 'task_list', { terminal_only: true },
|
|
100
|
+
);
|
|
101
|
+
return result.terminalTasks;
|
|
102
|
+
}
|
|
92
103
|
/** Operator-only recovery; never exposed to the browser Native Messaging host. */
|
|
93
104
|
export async function recoverWorkflowSupervisorTask(forgeHome: string, input: { taskId: string; sourceEffectId?: string; requestId: string; reason: string; supersedeUnknown?: boolean; authorizedBy?: string }): Promise<{ recoveryEffect: WorkflowSupervisorEffect; action: 'retry_authorized' | 'recovery_reserved' | 'unknown_superseded' }> {
|
|
94
105
|
return await rpc(forgeHome, 'task_recover', {
|
|
@@ -106,6 +117,24 @@ export async function getWorkflowSupervisorTaskStall(forgeHome: string, taskId:
|
|
|
106
117
|
return await rpc<WorkflowSupervisorTaskStall>(forgeHome, 'task_stall', { task_id: taskId });
|
|
107
118
|
}
|
|
108
119
|
|
|
120
|
+
/** One-way binding of a Work-owned Supervisor task to its canonical Scheduler lane. */
|
|
121
|
+
export async function bindWorkflowSupervisorScheduledWake(forgeHome: string, input: {
|
|
122
|
+
taskId: string; repoId: string; workId: string; scheduleId: string;
|
|
123
|
+
}): Promise<{ task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean }> {
|
|
124
|
+
return await rpc(forgeHome, 'task_bind_scheduled_wake', {
|
|
125
|
+
task_id: input.taskId, repo_id: input.repoId, work_id: input.workId, schedule_id: input.scheduleId,
|
|
126
|
+
}, SUPERVISOR_RPC_MUTATION_TIMEOUT_MS);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Scheduler occurrence is the idempotency identity; an unknown send outcome is never replayed. */
|
|
130
|
+
export async function wakeWorkflowSupervisorScheduledTask(forgeHome: string, input: {
|
|
131
|
+
taskId: string; scheduleId: string; occurrenceId: string;
|
|
132
|
+
}): Promise<{ effect: WorkflowSupervisorEffect; deduplicated: boolean }> {
|
|
133
|
+
return await rpc(forgeHome, 'task_wake_scheduled', {
|
|
134
|
+
task_id: input.taskId, schedule_id: input.scheduleId, occurrence_id: input.occurrenceId,
|
|
135
|
+
}, SUPERVISOR_RPC_MUTATION_TIMEOUT_MS);
|
|
136
|
+
}
|
|
137
|
+
|
|
109
138
|
/**
|
|
110
139
|
* Operator-only conversation migration for a standalone Supervisor task whose
|
|
111
140
|
* exact conversation became permanently unusable. It is explicit and bounded by
|
|
@@ -163,18 +192,6 @@ export async function reserveWorkflowSupervisorEnrollment(forgeHome: string, tas
|
|
|
163
192
|
}, SUPERVISOR_RPC_MUTATION_TIMEOUT_MS);
|
|
164
193
|
}
|
|
165
194
|
|
|
166
|
-
export async function recordWorkflowSupervisorAutomationReceipt(
|
|
167
|
-
forgeHome: string,
|
|
168
|
-
input: { taskId: string; conversationId: string; status: WorkflowSupervisorAutomationStatus; receiptId: string },
|
|
169
|
-
): Promise<unknown> {
|
|
170
|
-
return await rpc(forgeHome, 'automation_receipt', {
|
|
171
|
-
task_id: input.taskId,
|
|
172
|
-
conversation_id: input.conversationId,
|
|
173
|
-
automation_status: input.status,
|
|
174
|
-
receipt_id: input.receiptId,
|
|
175
|
-
}, SUPERVISOR_RPC_MUTATION_TIMEOUT_MS);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
195
|
export interface WorkflowSupervisorEffectDispatchBudget {
|
|
179
196
|
effectId: string;
|
|
180
197
|
generations: number;
|