@moretea-labs/forge 1.7.1 → 1.8.0
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 +58 -62
- package/CHANGELOG.md +39 -0
- package/CLAUDE.md +42 -62
- package/README.md +2 -2
- package/SKILL.md +0 -1
- package/THIRD_PARTY_NOTICES.md +5 -2
- package/adapters/browser/index.ts +2 -0
- package/adapters/chatgpt/browser-delivery-host.ts +71 -0
- package/adapters/chatgpt/browser-delivery-runtime.ts +783 -0
- package/adapters/chatgpt/controller-binding-store.ts +55 -0
- package/adapters/chatgpt/controller-host.ts +119 -0
- package/adapters/chatgpt/controller-round-host.ts +78 -0
- package/adapters/chatgpt/controller-round-settlement-store.ts +45 -0
- package/adapters/chatgpt/provider-delivery.ts +113 -0
- package/adapters/chatgpt/work-conversation-binding-store.ts +192 -0
- package/adapters/chatgpt/wsl-bridge-delivery-host.ts +63 -0
- package/adapters/computer/desktop-operator-contract.ts +7 -0
- package/adapters/computer/desktop-operator-discovery.ts +201 -0
- package/adapters/computer/desktop-operator-negotiation.ts +174 -0
- package/adapters/computer/desktop-operator-provider.ts +312 -0
- package/adapters/computer/index.ts +1 -0
- package/adapters/computer/interaction-target-authority.ts +944 -0
- package/adapters/controller-process/binding-store.ts +34 -0
- package/adapters/controller-process/controller-host.ts +26 -0
- package/adapters/mcp/access-mode.ts +132 -0
- package/adapters/mcp/audit.ts +32 -0
- package/adapters/mcp/auth.ts +407 -0
- package/adapters/mcp/controller-round-compatibility.ts +184 -0
- package/adapters/mcp/frozen-client-semantic-compatibility.ts +292 -0
- package/adapters/mcp/instructions.ts +36 -0
- package/adapters/mcp/legacy-context.ts +18 -0
- package/adapters/mcp/multi-repository.ts +437 -0
- package/adapters/mcp/oauth.ts +391 -0
- package/adapters/mcp/paths.ts +140 -0
- package/adapters/mcp/policy.ts +232 -0
- package/adapters/mcp/proxy-env.ts +62 -0
- package/adapters/mcp/readiness-projection.ts +24 -0
- package/adapters/mcp/redaction.ts +21 -0
- package/adapters/mcp/repo.ts +24 -0
- package/adapters/mcp/runtime-gateway/artifact-adapter.ts +52 -0
- package/adapters/mcp/runtime-gateway/context-adapter.ts +829 -0
- package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +191 -0
- package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +147 -0
- package/adapters/mcp/runtime-gateway/execution-control-adapter.ts +28 -0
- package/adapters/mcp/runtime-gateway/execution-tools.ts +207 -0
- package/adapters/mcp/runtime-gateway/filesystem-adapter.ts +38 -0
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +411 -0
- package/adapters/mcp/runtime-gateway/legacy-ios-tool-adapter.ts +105 -0
- package/adapters/mcp/runtime-gateway/maintenance-adapter.ts +44 -0
- package/adapters/mcp/runtime-gateway/model-adapter.ts +63 -0
- package/adapters/mcp/runtime-gateway/persisted-check-process.ts +3 -0
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +308 -0
- package/adapters/mcp/runtime-gateway/process-tools.ts +289 -0
- package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +76 -0
- package/adapters/mcp/runtime-gateway/recovery-adapter.ts +268 -0
- package/adapters/mcp/runtime-gateway/recovery-client-adapter.ts +70 -0
- package/adapters/mcp/runtime-gateway/recovery-tool-contract.ts +99 -0
- package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +110 -0
- package/adapters/mcp/runtime-gateway/result-adapter.ts +108 -0
- package/adapters/mcp/runtime-gateway/router.ts +728 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1251 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +634 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-shared.ts +27 -0
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +68 -0
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +186 -0
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +25 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +870 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +3186 -0
- package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +502 -0
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +1 -0
- package/adapters/mcp/server.ts +1048 -0
- package/adapters/mcp/setup.ts +791 -0
- package/adapters/mcp/tool-capability-cache.ts +87 -0
- package/adapters/mcp/tool-mapping/access-tools.ts +387 -0
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +5265 -0
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +150 -0
- package/adapters/mcp/tool-mapping/repository-tools.ts +1691 -0
- package/adapters/mcp/tool-mapping/tools.ts +9 -0
- package/adapters/mcp/tool-mapping/umbrella-tools.ts +117 -0
- package/adapters/mcp/toolset-names.ts +74 -0
- package/adapters/mcp/toolset-selection.ts +41 -0
- package/adapters/mcp/toolset.ts +336 -0
- package/adapters/mcp/transports/http.ts +1456 -0
- package/adapters/mcp/transports/session-registry.ts +513 -0
- package/adapters/mcp/transports/stdio.ts +8 -0
- package/adapters/mcp/tunnels/openai-secure-tunnel.ts +139 -0
- package/adapters/mcp/types.ts +41 -0
- package/assets/AGENTS.md +2 -16
- package/assets/CLAUDE.md +2 -16
- package/assets/hooks/AGENTS.md +3 -14
- package/assets/hooks/CLAUDE.md +3 -14
- package/assets/hooks/hook-input.sh +9 -2
- package/assets/hooks/lib/workflow-state.sh +28 -10
- package/assets/hooks/prompt-guard.sh +63 -37
- package/assets/hooks/security-sentinel.sh +2 -1
- package/assets/hooks/session-start-context.sh +7 -5
- package/assets/hooks/stop-orchestrator.sh +13 -3
- package/assets/initializer-question-pack.v4.json +11 -7
- package/assets/partials/05-workflow.partial.md +18 -73
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -2
- package/assets/partials-agents/04-task-protocol.partial.md +14 -59
- package/assets/partials-agents/06-quality-safety.partial.md +11 -16
- package/assets/partials-agents/08-deep-docs.partial.md +1 -2
- package/assets/plan-map.json +11 -11
- package/assets/plugin-registry.v1.json +9 -3
- package/assets/plugin-trust-policy.v1.json +6 -0
- package/assets/recovery/ForgeRecovery.ps1 +100 -0
- package/assets/recovery/forge-wsl-rescue +271 -0
- package/assets/reference-configs/development-protocol.md +11 -15
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/global-working-rules.md +9 -1
- package/assets/reference-configs/harness-overview.md +22 -112
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/architecture-event.ts +3 -34
- package/assets/templates/helpers/architecture-queue.sh +61 -10
- package/assets/templates/helpers/archive-workflow.sh +90 -180
- package/assets/templates/helpers/capability-config.ts +8 -11
- package/assets/templates/helpers/capability-resolver.ts +37 -19
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +1 -9
- package/assets/templates/helpers/check-task-sync.sh +82 -11
- package/assets/templates/helpers/check-task-workflow.sh +1 -3
- package/assets/templates/helpers/context-contract-sync.sh +1 -12
- package/assets/templates/helpers/contract-worktree.sh +26 -24
- package/assets/templates/helpers/ensure-task-workflow.sh +6 -8
- package/assets/templates/helpers/inspect-project-state.ts +1 -1
- package/assets/templates/helpers/migrate-project-template.sh +1 -1
- package/assets/templates/helpers/migrate-workflow-docs.ts +53 -239
- package/assets/templates/helpers/new-plan.sh +2 -2
- package/assets/templates/helpers/plan-to-todo.sh +3 -31
- package/assets/templates/helpers/sprint-backlog.sh +1 -1
- package/assets/templates/helpers/switch-plan.sh +1 -1
- package/assets/templates/helpers/workflow-contract.ts +1 -0
- package/assets/workflow-contract.v1.json +63 -171
- package/assets/workflows/xiaohongshu/xiaohongshu.collect-metrics.v1.draft.json +118 -0
- package/assets/workflows/xiaohongshu/xiaohongshu.publish-image-note.v1.draft.json +316 -0
- package/assets/workflows/xiaohongshu/xiaohongshu.publish-long-text.v1.draft.json +324 -0
- package/docs/architecture/CURRENT.md +148 -13
- package/docs/architecture/EVOLUTION.md +13 -0
- package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +2 -1
- package/docs/architecture/decisions/20260831-independent-windows-wsl-host-rescue.md +59 -0
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +120 -0
- package/docs/architecture/decisions/20260904-repository-runtime-data-boundary.md +42 -0
- package/docs/architecture/decisions/20260907-assistant-learning-loop.md +35 -0
- package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +296 -0
- package/docs/architecture/decisions/20260907-maintenance-diagnosis-bounded-read.md +39 -0
- package/docs/architecture/domains/verification.md +7 -0
- package/docs/architecture/domains/workflow-engine.md +1 -1
- package/docs/architecture/index.md +3 -3
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +19 -8
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +124 -0
- package/docs/forge-chatgpt-mcp-setup.md +5 -3
- package/docs/forge-plugin-management.md +20 -4
- package/docs/forge-runtime-self-healing-loop.md +2 -0
- package/docs/operations/benchmark-cross-version-freeze.md +38 -0
- package/docs/operations/controller-browser-plugin.md +3 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/platform-support.md +16 -8
- package/docs/operations/platform-support.zh-CN.md +2 -2
- package/docs/operations/standalone-disaster-recovery.md +26 -1
- package/docs/operations/uu-remote-rescue.md +41 -0
- package/docs/operations/windows-wsl-host-rescue.md +133 -0
- package/docs/public-usage-guide.md +1 -1
- package/docs/public-usage-guide.zh-CN.md +1 -1
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/02-connect-chatgpt.md +4 -4
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +4 -4
- package/docs/wiki/Implementation.md +1 -1
- package/package.json +23 -14
- package/packages/kernel/controller/api/index.ts +2 -0
- package/packages/kernel/controller/application/controller-round-service.ts +29 -0
- package/packages/kernel/controller/application/controller-service.ts +30 -0
- package/packages/kernel/controller/domain/controller-round-transition-policy.ts +308 -0
- package/packages/kernel/controller/domain/controller-round.ts +107 -0
- package/packages/kernel/controller/domain/execution-quality.ts +133 -0
- package/packages/kernel/controller/domain/types.ts +56 -0
- package/packages/kernel/controller/index.ts +1 -0
- package/packages/kernel/controller/infrastructure/controller-binding-store.ts +84 -0
- package/packages/kernel/controller/infrastructure/controller-round-store.ts +1384 -0
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +578 -0
- package/packages/kernel/controller/ports/controller-host.ts +16 -0
- package/packages/kernel/goal/api/index.ts +1 -0
- package/packages/kernel/goal/domain/types.ts +40 -0
- package/packages/kernel/goal/index.ts +1 -0
- package/packages/kernel/identity/api/index.ts +5 -0
- package/packages/kernel/identity/application/identity-service.ts +67 -0
- package/packages/kernel/identity/domain/scope.ts +63 -0
- package/packages/kernel/identity/domain/semantic-sync.ts +81 -0
- package/packages/kernel/identity/domain/types.ts +44 -0
- package/packages/kernel/identity/domain/workspace.ts +56 -0
- package/packages/kernel/identity/index.ts +1 -0
- package/packages/kernel/identity/infrastructure/identity-store.ts +75 -0
- package/packages/kernel/memory/api/index.ts +5 -0
- package/packages/kernel/memory/application/experience-service.ts +118 -0
- package/packages/kernel/memory/application/outcome-service.ts +77 -0
- package/packages/kernel/memory/domain/experience.ts +103 -0
- package/packages/kernel/memory/domain/operational-prior.ts +92 -0
- package/packages/kernel/memory/index.ts +1 -0
- package/packages/kernel/progression/api/index.ts +2 -0
- package/packages/kernel/progression/application/projector.ts +211 -0
- package/packages/kernel/progression/domain/types.ts +116 -0
- package/packages/kernel/progression/index.ts +1 -0
- package/packages/kernel/scheduler/api/index.ts +6 -0
- package/packages/kernel/scheduler/application/continuation-service.ts +212 -0
- package/packages/kernel/scheduler/application/eligibility.ts +252 -0
- package/packages/kernel/scheduler/application/schedule-service.ts +24 -0
- package/packages/kernel/scheduler/application/settlement.ts +197 -0
- package/packages/kernel/scheduler/domain/continuation.ts +38 -0
- package/packages/kernel/scheduler/domain/schedule.ts +149 -0
- package/packages/kernel/scheduler/index.ts +1 -0
- package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +38 -0
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +785 -0
- package/packages/kernel/work/api/index.ts +15 -0
- package/packages/kernel/work/application/work-service.ts +148 -0
- package/packages/kernel/work/domain/admission-policy.ts +34 -0
- package/packages/kernel/work/domain/check-receipt.ts +43 -0
- package/packages/kernel/work/domain/context-closure-receipt.ts +32 -0
- package/packages/kernel/work/domain/engineering-contracts.ts +157 -0
- package/packages/kernel/work/domain/engineering-design.ts +281 -0
- package/packages/kernel/work/domain/engineering-profile.ts +242 -0
- package/packages/kernel/work/domain/execution-concurrency.ts +254 -0
- package/packages/kernel/work/domain/execution-snapshot.ts +33 -0
- package/packages/kernel/work/domain/implementation-review.ts +510 -0
- package/packages/kernel/work/domain/project-engineering-contract.ts +202 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +73 -0
- package/packages/kernel/work/domain/state-machine.ts +259 -0
- package/packages/kernel/work/domain/types.ts +528 -0
- package/packages/kernel/work/index.ts +1 -0
- package/packages/kernel/work/infrastructure/work-contract-store.ts +1876 -0
- package/packages/kernel/work/ports/work-contract-store.ts +18 -0
- package/packages/plugin-runtime/browser/index.ts +3 -0
- package/packages/plugin-runtime/browser/provider-registry.ts +110 -0
- package/packages/plugin-runtime/browser/runtime-contract.ts +150 -0
- package/packages/plugin-runtime/browser/session-authority.ts +61 -0
- package/packages/plugin-runtime/computer/index.ts +6 -0
- package/packages/plugin-runtime/computer/provider-error.ts +26 -0
- package/packages/plugin-runtime/computer/provider-registration.ts +52 -0
- package/packages/plugin-runtime/computer/provider-registry.ts +63 -0
- package/packages/plugin-runtime/computer/provider.ts +19 -0
- package/packages/plugin-runtime/computer/target-authority.ts +190 -0
- package/packages/plugin-runtime/computer/target-persistence.ts +54 -0
- package/packages/plugin-runtime/external/index.ts +1 -0
- package/packages/plugin-runtime/external/unix-jsonl-transport.ts +457 -0
- package/packages/plugin-runtime/index.ts +3 -0
- package/packages/protocols/browser/index.ts +1 -0
- package/packages/protocols/browser/session.ts +98 -0
- package/packages/protocols/computer/contract.ts +199 -0
- package/packages/protocols/computer/index.ts +1 -0
- package/packages/protocols/failure/contract.ts +158 -0
- package/packages/protocols/failure/index.ts +1 -0
- package/packages/protocols/handoff/index.ts +1 -0
- package/packages/protocols/handoff/status.ts +22 -0
- package/packages/protocols/mcp/execution-context.ts +11 -0
- package/packages/protocols/mcp/index.ts +2 -0
- package/packages/protocols/mcp/tool-contract.ts +42 -0
- package/packages/workflow-runtime/api/index.ts +2 -0
- package/packages/workflow-runtime/application/interpreter.ts +342 -0
- package/packages/workflow-runtime/domain/workflow-asset.ts +223 -0
- package/packages/workflow-runtime/index.ts +1 -0
- package/scripts/assemble-template.ts +1 -1
- package/scripts/benchmark-computer-hotpath.ts +255 -0
- package/scripts/benchmark-controller-round2.ts +54 -2
- package/scripts/benchmark-ios-agent-device.ts +3 -6
- package/scripts/benchmark-mcp-transport.ts +3 -4
- package/scripts/benchmark-operational-memory-activation.ts +235 -0
- package/scripts/benchmark-operational-shadow.ts +28 -0
- package/scripts/bootstrap-runtime-maintenance-recovery.sh +4 -1
- package/scripts/canary-chatgpt-provider-delivery.ts +97 -0
- package/scripts/canary-controller-semantic-wait.ts +195 -0
- package/scripts/canary-global-hook.sh +2 -2
- package/scripts/check-mcp-compatibility.ts +173 -7
- package/scripts/check-platform-support.mjs +28 -2
- package/scripts/check-public-docs.sh +1 -1
- package/scripts/check-release-readiness.sh +1 -1
- package/scripts/check-repository-hygiene.mjs +92 -0
- package/scripts/check-runtime-architecture.mjs +1206 -95
- package/scripts/check-source-duplication.mjs +9 -1
- package/scripts/check-tarball-install-smoke.sh +98 -15
- package/scripts/controller-ngrok-rotation.sh +5 -4
- package/scripts/create-project-dirs.sh +10 -74
- package/scripts/forge-local-recovery-helper.mjs +204 -0
- package/scripts/forge-uu-remote-rescue-helper.d.mts +31 -0
- package/scripts/forge-uu-remote-rescue-helper.mjs +409 -0
- package/scripts/forge-windows-host-recovery-helper.mjs +136 -0
- package/scripts/hook-shim.sh +2 -2
- package/scripts/init-project.sh +12 -70
- package/scripts/install-desktop-operator-registration.ts +4 -2
- package/scripts/install-independent-windows-wsl-recovery.ts +125 -0
- package/scripts/install-standalone-recovery.ts +122 -10
- package/scripts/install-uu-remote-rescue-registration.ts +74 -0
- package/scripts/install-windows-host-recovery-registration.ts +22 -0
- package/scripts/lib/controller-home-env.sh +6 -3
- package/scripts/lib/project-init-lib.sh +39 -582
- package/scripts/migrate-chatgpt-automation-schedules.ts +2 -2
- package/scripts/migrate-windows-wsl-controller-home.ts +306 -0
- package/scripts/open-source-audit-allowlist.txt +12 -1
- package/scripts/portability-bindings.mjs +70 -0
- package/scripts/run-bounded-command.mjs +55 -0
- package/scripts/run-bun-test-file.ts +22 -6
- package/scripts/run-governed-gate.ts +4 -1
- package/scripts/smoke-mcp-http-runtime.ts +1 -1
- package/scripts/smoke-runtime-recovery.ts +34 -1
- package/scripts/smoke-schedule-engine.ts +1 -1
- package/scripts/stage-runtime-release.ts +6 -1
- package/scripts/test-governance.ts +51 -2
- package/src/cli/agent-jobs/job-manager.ts +10 -1
- package/src/cli/chatgpt-browser/bind-server.ts +2 -1
- package/src/cli/chatgpt-browser/binding.ts +20 -10
- package/src/cli/chatgpt-browser/bridge-extension.ts +75 -0
- package/src/cli/chatgpt-browser/bridge-provider.ts +376 -11
- package/src/cli/chatgpt-browser/engine.ts +10 -11
- package/src/cli/chatgpt-browser/native-provider.ts +24 -83
- package/src/cli/chatgpt-browser/oracle-provider.ts +5 -3
- package/src/cli/chatgpt-browser/types.ts +1 -0
- package/src/cli/commands/bootstrap-control.ts +334 -0
- package/src/cli/commands/brain-assistant.ts +38 -0
- package/src/cli/commands/brain.ts +193 -40
- package/src/cli/commands/chatgpt.ts +127 -0
- package/src/cli/commands/computer.ts +437 -0
- package/src/cli/commands/doctor.ts +18 -1
- package/src/cli/commands/init-hook.ts +71 -61
- package/src/cli/commands/init.ts +3 -1
- package/src/cli/commands/mcp.ts +16 -10
- package/src/cli/commands/plugin.ts +38 -12
- package/src/cli/commands/recovery.ts +493 -80
- package/src/cli/commands/runtime.ts +9 -4
- package/src/cli/commands/setup-profile.ts +89 -39
- package/src/cli/commands/status.ts +2 -1
- package/src/cli/controller/check-runner.ts +487 -98
- package/src/cli/controller/context/types.ts +7 -1
- package/src/cli/controller/context-pack.ts +23 -3
- package/src/cli/controller/issue-store.ts +1 -1
- package/src/cli/controller/systemd-user.ts +110 -0
- package/src/cli/controller/types.ts +10 -70
- package/src/cli/controller/worklog.ts +67 -13
- package/src/cli/editing/edit-operation-contract.ts +92 -0
- package/src/cli/editing/edit-session.ts +289 -13
- package/src/cli/github/plugin.ts +17 -12
- package/src/cli/hook/route-registry.ts +3 -5
- package/src/cli/hook/runtime.ts +49 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/local-bridge/connector-freshness.ts +2 -2
- package/src/cli/local-bridge/console-automations.ts +2 -2
- package/src/cli/local-bridge/facade-api.ts +41 -14
- package/src/cli/local-bridge/job-store.ts +1 -1
- package/src/cli/local-bridge/server.ts +24 -27
- package/src/cli/mcp/access-mode.ts +2 -132
- package/src/cli/mcp/access-tools.ts +2 -387
- package/src/cli/mcp/audit.ts +2 -32
- package/src/cli/mcp/auth.ts +2 -404
- package/src/cli/mcp/instructions.ts +2 -34
- package/src/cli/mcp/legacy-context.ts +2 -16
- package/src/cli/mcp/legacy-tool-service.ts +2 -5308
- package/src/cli/mcp/multi-repository.ts +2 -436
- package/src/cli/mcp/oauth.ts +2 -375
- package/src/cli/mcp/openai-secure-tunnel.ts +2 -0
- package/src/cli/mcp/paths.ts +2 -140
- package/src/cli/mcp/policy.ts +2 -232
- package/src/cli/mcp/post-finalize-work-attribution.ts +2 -150
- package/src/cli/mcp/proxy-env.ts +2 -62
- package/src/cli/mcp/readiness-projection.ts +2 -24
- package/src/cli/mcp/redaction.ts +2 -21
- package/src/cli/mcp/repo.ts +2 -24
- package/src/cli/mcp/repository-tools.ts +2 -1585
- package/src/cli/mcp/server.ts +2 -951
- package/src/cli/mcp/setup.ts +2 -737
- package/src/cli/mcp/tool-capability-cache.ts +2 -87
- package/src/cli/mcp/tool-contract.ts +2 -14
- package/src/cli/mcp/tools.ts +2 -9
- package/src/cli/mcp/toolset-names.ts +2 -70
- package/src/cli/mcp/toolset-selection.ts +2 -41
- package/src/cli/mcp/toolset.ts +2 -336
- package/src/cli/mcp/transports/AGENTS.md +1 -10
- package/src/cli/mcp/transports/CLAUDE.md +1 -10
- package/src/cli/mcp/transports/http.ts +2 -1414
- package/src/cli/mcp/transports/session-registry.ts +2 -490
- package/src/cli/mcp/transports/stdio.ts +2 -8
- package/src/cli/mcp/types.ts +2 -41
- package/src/cli/mcp/umbrella-tools.ts +2 -117
- package/src/cli/repositories/command-executor.ts +64 -18
- package/src/cli/repositories/command-scope.ts +117 -1
- package/src/cli/repositories/controller-home.ts +119 -1
- package/src/cli/repositories/identity.ts +13 -0
- package/src/cli/repositories/registry.ts +68 -49
- package/src/cli/repositories/repository-snapshot.ts +53 -7
- package/src/cli/repositories/runtime-storage.ts +27 -2
- package/src/cli/repositories/safe-patch.ts +34 -38
- package/src/cli/repositories/selected-path-actions.ts +22 -2
- package/src/cli/repositories/structured-git.ts +105 -25
- package/src/cli/runtime/helper-runner.ts +7 -0
- package/src/cli/runtime-invocation.ts +8 -1
- package/src/cli/tools/codegraph.ts +44 -25
- package/src/core/adoption/manifest-templates.ts +1 -1
- package/src/core/adoption/plan.ts +3 -26
- package/src/core/adoption/workflow-contract-plan.ts +17 -9
- package/src/effects/process-runner.ts +8 -14
- package/src/runtime/context/assistant-context.ts +154 -0
- package/src/runtime/context/assistant-work-context.ts +147 -0
- package/src/runtime/context/codegraph-cache-boundary.ts +255 -0
- package/src/runtime/context/codegraph-read-provider.ts +55 -22
- package/src/runtime/context/codegraph-sidecar.cjs +8 -1
- package/src/runtime/context/context-closure.ts +268 -0
- package/src/runtime/context/context-plane.ts +211 -0
- package/src/runtime/context/project-engineering-contract.ts +60 -0
- package/src/runtime/control-plane/bootstrap/capability-provider.ts +74 -0
- package/src/runtime/control-plane/bootstrap/control-api.ts +57 -0
- package/src/runtime/control-plane/bootstrap/index.ts +5 -0
- package/src/runtime/control-plane/bootstrap/state-machine.ts +141 -0
- package/src/runtime/control-plane/bootstrap/store.ts +21 -0
- package/src/runtime/control-plane/bootstrap/types.ts +87 -0
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +328 -0
- package/src/runtime/control-plane/codegraph-cache-retention.ts +164 -0
- package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +320 -0
- package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +169 -0
- package/src/runtime/control-plane/execution/controller-authority-recovery.ts +350 -0
- package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +85 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +417 -100
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +103 -15
- package/src/runtime/control-plane/execution/implementation-review-content.ts +95 -0
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +2 -2
- package/src/runtime/control-plane/execution/retained-work-resume.ts +336 -6
- package/src/runtime/control-plane/execution/validation.ts +28 -3
- package/src/runtime/control-plane/execution/work-authority-reconciler.ts +116 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +44 -2
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +34 -3
- package/src/runtime/control-plane/execution/work-execution-support.ts +42 -22
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1103 -166
- package/src/runtime/control-plane/execution/work-handle-authority.ts +267 -16
- package/src/runtime/control-plane/execution/work-handle-store.ts +77 -0
- package/src/runtime/control-plane/execution/work-head-settlement.ts +75 -0
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +145 -23
- package/src/runtime/control-plane/execution/work-operation-service.ts +49 -13
- package/src/runtime/control-plane/execution/work-preparation-service.ts +31 -15
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +1 -1
- package/src/runtime/control-plane/execution/work-task-receipt.ts +20 -1
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +407 -76
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +88 -18
- package/src/runtime/control-plane/execution/work-verification-context.ts +1 -1
- package/src/runtime/control-plane/execution/work-verification-service.ts +258 -4
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +200 -15
- package/src/runtime/control-plane/facade/capability-registry.ts +21 -10
- package/src/runtime/control-plane/facade/controller-round-relay.ts +3 -1118
- package/src/runtime/control-plane/facade/controller-session-store.ts +2 -488
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +6 -3
- package/src/runtime/control-plane/facade/goal-workloop.ts +972 -137
- package/src/runtime/control-plane/facade/handoff-inbox-application.ts +158 -0
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +10 -0
- package/src/runtime/control-plane/facade/index.ts +4 -2
- package/src/runtime/control-plane/facade/operation-digest.ts +22 -58
- package/src/runtime/control-plane/facade/plan-contract-store.ts +1004 -38
- package/src/runtime/control-plane/facade/repository-work-admission.ts +19 -1
- package/src/runtime/control-plane/facade/requirement-authority.ts +218 -0
- package/src/runtime/control-plane/facade/requirement-board.ts +6 -1
- package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +48 -0
- package/src/runtime/control-plane/facade/semantic-admission.ts +8 -16
- package/src/runtime/control-plane/facade/suggested-actions.ts +4 -1
- package/src/runtime/control-plane/facade/types.ts +189 -532
- package/src/runtime/control-plane/facade/work-admission-policy.ts +84 -35
- package/src/runtime/control-plane/facade/work-contract-store.ts +2 -1043
- package/src/runtime/control-plane/facade/work-implementation-review.ts +2 -0
- package/src/runtime/control-plane/facade/work-state-machine.ts +2 -250
- package/src/runtime/control-plane/generated-cache-retention.ts +239 -18
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +12 -1
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +76 -13
- package/src/runtime/control-plane/global-scheduler/periodic-cleanup-entry.ts +55 -0
- package/src/runtime/control-plane/global-scheduler/periodic-cleanup-process.ts +98 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -1
- package/src/runtime/control-plane/global-scheduler/scheduler-worker-entry.ts +10 -0
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +173 -81
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +18 -6
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +397 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +2 -177
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +301 -560
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +20 -4
- package/src/runtime/control-plane/launcher/thin-launcher.ts +50 -4
- package/src/runtime/control-plane/lifecycle-retention-metrics.ts +60 -0
- package/src/runtime/control-plane/persistence/experience-store.ts +169 -0
- package/src/runtime/control-plane/persistence/operational-prior-store.ts +348 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +30 -22
- package/src/runtime/control-plane/persistence/sqlite-store.ts +545 -76
- package/src/runtime/control-plane/persistence/workflow-content-store.ts +98 -0
- package/src/runtime/control-plane/persistence/workflow-registry-store.ts +186 -0
- package/src/runtime/control-plane/persistence/workflow-run-store.ts +123 -0
- package/src/runtime/control-plane/release-retention.ts +56 -16
- package/src/runtime/control-plane/repository-namespace-retention.ts +208 -0
- package/src/runtime/control-plane/routing/route-policy.ts +19 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +574 -37
- package/src/runtime/control-plane/runtime-generation.ts +6 -0
- package/src/runtime/control-plane/workspace/semantic-sync-service.ts +465 -0
- package/src/runtime/control-plane/workspace/workspace-store.ts +80 -0
- package/src/runtime/diagnostics/incident-repair.ts +406 -0
- package/src/runtime/diagnostics/mcp-timing.ts +125 -6
- package/src/runtime/diagnostics/performance.ts +6 -14
- package/src/runtime/diagnostics/read-only-tool.ts +1 -1
- package/src/runtime/evidence/artifact-store.ts +65 -2
- package/src/runtime/evidence/operational-shadow.ts +38 -0
- package/src/runtime/evidence/process-check-receipt.ts +5 -43
- package/src/runtime/evidence/sensitive-output.ts +4 -4
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -1
- package/src/runtime/execution/jobs/store.ts +121 -2
- package/src/runtime/execution/managed-workspace.ts +55 -5
- package/src/runtime/execution/process-runtime/canary.ts +19 -5
- package/src/runtime/execution/process-runtime/check-classification.ts +48 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +23 -9
- package/src/runtime/execution/process-runtime/check-receipt.ts +1 -1
- package/src/runtime/execution/process-runtime/check-result-retention.ts +132 -0
- package/src/runtime/execution/process-runtime/check-result.ts +73 -2
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -3
- package/src/runtime/execution/process-runtime/check-storage.ts +187 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +38 -87
- package/src/runtime/execution/process-runtime/index.ts +1 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +2 -1
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +32 -1
- package/src/runtime/execution/process-runtime/persisted-check.ts +128 -21
- package/src/runtime/execution/process-runtime/resource-claims.ts +60 -3
- package/src/runtime/execution/process-runtime/runtime.ts +132 -15
- package/src/runtime/execution/process-runtime/types.ts +22 -0
- package/src/runtime/execution/workers/executor.ts +16 -13
- package/src/runtime/execution/workers/worker-entry.ts +1 -1
- package/src/runtime/gateway/mcp/execution-tools.ts +2 -207
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +2 -61
- package/src/runtime/gateway/mcp/persisted-check-process.ts +2 -3
- package/src/runtime/gateway/mcp/process-tools.ts +2 -289
- package/src/runtime/gateway/mcp/router.ts +2 -1125
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +2 -565
- package/src/runtime/gateway/mcp/runtime-tools.ts +2 -6651
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +2 -1
- package/src/runtime/maintenance/cleanup.ts +38 -24
- package/src/runtime/platform/browser-product-discovery.ts +174 -0
- package/src/runtime/platform/computer-platform.ts +14 -0
- package/src/runtime/platform/executable-discovery.ts +54 -0
- package/src/runtime/platform/fetch-http-server.ts +90 -0
- package/src/runtime/platform/service-manager.ts +138 -0
- package/src/runtime/platform/windows-host-discovery.ts +118 -0
- package/src/runtime/platform/windows-recovery-deployment.ts +67 -0
- package/src/runtime/plugins/action-application.ts +135 -0
- package/src/runtime/plugins/app-store-connect-adapter.ts +9 -21
- package/src/runtime/plugins/browser-adapter.ts +140 -100
- package/src/runtime/plugins/browser-automation-service.ts +15 -36
- package/src/runtime/plugins/browser-macos-bridge.ts +69 -21
- package/src/runtime/plugins/browser-manifest-surface.ts +30 -6
- package/src/runtime/plugins/browser-native-session-service.ts +1 -1
- package/src/runtime/plugins/browser-provider-registry.ts +2 -109
- package/src/runtime/plugins/browser-registration.ts +16 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -154
- package/src/runtime/plugins/browser-runtime.ts +41 -9
- package/src/runtime/plugins/browser-session-authority.ts +101 -291
- package/src/runtime/plugins/browser-session-legacy-migration.ts +203 -0
- package/src/runtime/plugins/browser-session-store.ts +229 -36
- package/src/runtime/plugins/browser-session-types.ts +2 -98
- package/src/runtime/plugins/capability-authorization-grants.ts +17 -0
- package/src/runtime/plugins/catalog-trust.ts +82 -0
- package/src/runtime/plugins/computer-registration.ts +839 -0
- package/src/runtime/plugins/config-store.ts +119 -0
- package/src/runtime/plugins/desktop-operator-external-policy.ts +932 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +111 -8
- package/src/runtime/plugins/errors.ts +8 -1
- package/src/runtime/plugins/external-adapter.ts +45 -322
- package/src/runtime/plugins/external-provider-policy.ts +48 -0
- package/src/runtime/plugins/external-registration.ts +9 -1
- package/src/runtime/plugins/external-unix-socket-probe.cjs +9 -3
- package/src/runtime/plugins/external-unix-socket.ts +64 -193
- package/src/runtime/plugins/first-party-external-registration.ts +163 -0
- package/src/runtime/plugins/first-party-registry.ts +3 -1
- package/src/runtime/plugins/github-adapter.ts +1 -2
- package/src/runtime/plugins/gmail-adapter.ts +6 -6
- package/src/runtime/plugins/google-calendar-adapter.ts +6 -6
- package/src/runtime/plugins/google-shared.ts +31 -46
- package/src/runtime/plugins/google-tasks-adapter.ts +6 -6
- package/src/runtime/plugins/ios-adapter.ts +1 -2
- package/src/runtime/plugins/local-system-adapter.ts +180 -0
- package/src/runtime/plugins/macos-capability-broker.ts +15 -105
- package/src/runtime/plugins/resend-adapter.ts +15 -24
- package/src/runtime/plugins/store.ts +493 -51
- package/src/runtime/plugins/types.ts +8 -1
- package/src/runtime/plugins/uu-remote-rescue-registration.ts +155 -0
- package/src/runtime/plugins/windows-host-recovery-install.ts +68 -0
- package/src/runtime/plugins/windows-host-recovery-registration.ts +85 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +86 -430
- package/src/runtime/projections/controller-context.ts +12 -6
- package/src/runtime/projections/git-status-sampler.ts +73 -22
- package/src/runtime/projections/materialized-view.ts +14 -8
- package/src/runtime/recovery/capability-action-application.ts +228 -0
- package/src/runtime/recovery/index.ts +3 -0
- package/src/runtime/recovery/lifecycle-inventory.ts +338 -0
- package/src/runtime/recovery/local-jobs-repair.ts +14 -14
- package/src/runtime/recovery/maintenance-executor.ts +358 -76
- package/src/runtime/recovery/quarantine-retention.ts +256 -0
- package/src/runtime/release/identity.ts +6 -0
- package/src/runtime/resources/leases/store.ts +1 -1
- package/src/runtime/root/active-execution-power-assertion.ts +139 -0
- package/src/runtime/root/assistant-learning-loop.ts +45 -0
- package/src/runtime/root/browser-session-composition.ts +59 -0
- package/src/runtime/root/computer-composition.ts +191 -0
- package/src/runtime/root/computer-target-composition.ts +16 -0
- package/src/runtime/root/computer-target-persistence.ts +39 -0
- package/src/runtime/root/controller-round-composition.ts +107 -0
- package/src/runtime/root/controller-services.ts +2 -0
- package/src/runtime/root/entry.ts +4 -3
- package/src/runtime/root/gateway-adapter.ts +3 -3
- package/src/runtime/root/local-bridge.ts +5 -2
- package/src/runtime/root/mcp-transport.ts +5 -5
- package/src/runtime/root/package-connector-service.ts +108 -56
- package/src/runtime/root/package-runtime-release.ts +86 -18
- package/src/runtime/root/package-runtime-service.ts +63 -86
- package/src/runtime/root/release-execution-canary.ts +71 -0
- package/src/runtime/root/release-loader.ts +68 -0
- package/src/runtime/root/release-manifest.ts +106 -2
- package/src/runtime/root/release-materialize.ts +103 -4
- package/src/runtime/root/release-store.ts +31 -2
- package/src/runtime/root/runtime.ts +240 -25
- package/src/runtime/root/scheduled-controller-composition.ts +51 -0
- package/src/runtime/root/scheduler.ts +12 -4
- package/src/runtime/root/service-runner.ts +1 -1
- package/src/runtime/root/service.ts +41 -16
- package/src/runtime/root/status.ts +1 -0
- package/src/runtime/root/types.ts +24 -1
- package/src/runtime/shared/local-bridge-surface.ts +1 -1
- package/src/runtime/shared/process-environment.ts +19 -4
- package/src/runtime/shared/storage-capacity.ts +68 -0
- package/src/runtime/standalone-recovery/controller-home-migration.ts +1116 -0
- package/src/runtime/standalone-recovery/core.ts +984 -230
- package/src/runtime/standalone-recovery/entry.ts +182 -51
- package/src/runtime/standalone-recovery/independent-host-rescue.ts +172 -0
- package/src/runtime/standalone-recovery/installer.ts +212 -104
- package/src/runtime/standalone-recovery/mcp-server.ts +243 -0
- package/src/runtime/standalone-recovery/mutation-identity-contract.ts +21 -0
- package/src/runtime/standalone-recovery/operation-lock.ts +160 -0
- package/src/runtime/standalone-recovery/performance.ts +135 -0
- package/src/runtime/standalone-recovery/release.ts +5 -0
- package/src/runtime/watchdog/workflow-watchdog.ts +1 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +191 -343
- package/src/runtime/workflow/schedules/pending-activation.ts +1 -1
- package/src/runtime/workflow/schedules/settlement.ts +2 -191
- package/src/runtime/workflow/schedules/store.ts +2 -607
- package/src/runtime/workflow/schedules/types.ts +2 -127
- package/src/runtime/workflow/schedules/work-continuation.ts +180 -15
- package/src/runtime/workflows/first-party/xiaohongshu.ts +122 -0
- package/src/runtime/workflows/runtime.ts +486 -0
- package/src/testing/test-governance.ts +90 -29
- package/assets/templates/helpers/archive-architecture-request.sh +0 -244
- package/docs/architecture/history/README.md +0 -7
- package/docs/architecture/history/chatgpt-handoff-facade.md +0 -397
- package/docs/architecture/history/global-hook-runtime.md +0 -166
- package/docs/architecture/history/ios-semantic-automation-provider-v2.md +0 -170
- package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +0 -63
- package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +0 -68
- package/docs/architecture/requests/archive/2026/root.md +0 -68
- package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +0 -76
- package/docs/architecture/requests/archive/2026/verification-evals-checks.md +0 -68
- package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +0 -69
- package/scripts/archive-architecture-request.sh +0 -4
- package/scripts/run-skill-evals.ts +0 -1022
|
@@ -0,0 +1,1384 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import { withControllerLock } from '../../../../src/cli/repositories/locks';
|
|
3
|
+
import { readRequirement } from '../../../../src/runtime/control-plane/persistence/requirement-store';
|
|
4
|
+
import {
|
|
5
|
+
listControlPlaneRecords,
|
|
6
|
+
readControlPlaneRecord,
|
|
7
|
+
readControlPlaneRecordWithinTransaction,
|
|
8
|
+
withControlPlaneTransaction,
|
|
9
|
+
writeControlPlaneRecord,
|
|
10
|
+
writeControlPlaneRecordWithinTransaction,
|
|
11
|
+
type ControlPlaneRecord,
|
|
12
|
+
} from '../../../../src/runtime/control-plane/persistence/sqlite-store';
|
|
13
|
+
import { workHasActiveExecution } from '../../../../src/runtime/execution/work-activity';
|
|
14
|
+
import { controllerSessionBlocksRecovery, getControllerSession } from './controller-session-store';
|
|
15
|
+
import { getHandoffItem, listHandoffItems } from '../../../../src/runtime/control-plane/facade/handoff-inbox-store';
|
|
16
|
+
import { getWorkContract, readWorkContractStore, isTerminalWorkContractStatus, type WorkContract } from '../../work/api/index';
|
|
17
|
+
import { isTerminalHandoffStatus } from '../../../protocols/handoff/index';
|
|
18
|
+
import type { ControllerSession, ControllerType } from '../domain/types';
|
|
19
|
+
import { deriveClosedRoundQualitySignals, type AssistantContextSnapshot, type AssistantContextUsage, type ClosedRoundObservation, type ExecutionQualityAdjustmentResult, type ExecutionQualityDecision, type ExecutionQualitySignal } from '../domain/execution-quality';
|
|
20
|
+
import {
|
|
21
|
+
CONTROLLER_ROUND_DISPOSITIONS,
|
|
22
|
+
CONTROLLER_RELAY_ABANDONED_RELEASE_ERROR,
|
|
23
|
+
type ControllerRoundDisposition,
|
|
24
|
+
type ControllerRoundRelayIdentity,
|
|
25
|
+
type ControllerRoundRelayRecord,
|
|
26
|
+
type ControllerRoundRelayStatus,
|
|
27
|
+
} from '../domain/controller-round';
|
|
28
|
+
import {
|
|
29
|
+
controllerRoundBlockerClass,
|
|
30
|
+
controllerRoundProviderEffectId,
|
|
31
|
+
decideControllerRoundTransition,
|
|
32
|
+
type ControllerRoundTransitionDecision,
|
|
33
|
+
type ControllerRoundTransitionEvent,
|
|
34
|
+
} from '../domain/controller-round-transition-policy';
|
|
35
|
+
|
|
36
|
+
export interface ControllerRoundRelayStoreOptions {
|
|
37
|
+
controllerHome: string;
|
|
38
|
+
repoId: string;
|
|
39
|
+
now?: () => string;
|
|
40
|
+
/** Trusted composition supplies advisory data; Kernel does not read project files. */
|
|
41
|
+
prepareAssistantContext?: (workId: string) => string | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SubmitControllerRoundDispositionInput {
|
|
45
|
+
workId: string;
|
|
46
|
+
identity: ControllerRoundRelayIdentity;
|
|
47
|
+
disposition: ControllerRoundDisposition;
|
|
48
|
+
executionQualityDecisions?: ExecutionQualityDecision[];
|
|
49
|
+
executionQualityAdjustmentResults?: Array<Omit<ExecutionQualityAdjustmentResult, 'verifiedAt'>>;
|
|
50
|
+
assistantContextDigest?: string;
|
|
51
|
+
assistantContextUsage?: AssistantContextUsage[];
|
|
52
|
+
relayScopeId?: string;
|
|
53
|
+
requirementId?: string;
|
|
54
|
+
handoffId?: string;
|
|
55
|
+
stateFingerprint?: string;
|
|
56
|
+
reason?: string;
|
|
57
|
+
/** Opaque provider binding owned by a ControllerHost adapter. */
|
|
58
|
+
bindingId?: string;
|
|
59
|
+
maxRounds?: number;
|
|
60
|
+
maxRepeatedState?: number;
|
|
61
|
+
maxFailures?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BeginInitialControllerRoundDispatchInput {
|
|
65
|
+
workId: string;
|
|
66
|
+
identity: ControllerRoundRelayIdentity;
|
|
67
|
+
relayScopeId?: string;
|
|
68
|
+
requirementId?: string;
|
|
69
|
+
/** Opaque provider binding owned by a ControllerHost adapter. */
|
|
70
|
+
bindingId?: string;
|
|
71
|
+
maxRounds?: number;
|
|
72
|
+
maxRepeatedState?: number;
|
|
73
|
+
maxFailures?: number;
|
|
74
|
+
/** Required for any later schedule/manual/replan occurrence after a prior semantic state exists. */
|
|
75
|
+
occurrenceId?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface RecoverControllerRoundRelayAuthorityInput {
|
|
79
|
+
workId: string;
|
|
80
|
+
requestedBy?: string;
|
|
81
|
+
identity: ControllerRoundRelayIdentity;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Legacy ControllerRound rows predate explicit controllerType and were ChatGPT-only. */
|
|
85
|
+
function relayControllerType(record: Pick<ControllerRoundRelayRecord, 'controllerType'>): ControllerType {
|
|
86
|
+
return record.controllerType ?? 'chatgpt';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const NAMESPACE = 'controller_round_relay';
|
|
90
|
+
const SCHEMA_VERSION = 1;
|
|
91
|
+
const DEFAULT_MAX_ROUNDS = 8;
|
|
92
|
+
const DEFAULT_MAX_REPEATED_STATE = 2;
|
|
93
|
+
const DEFAULT_MAX_FAILURES = 3;
|
|
94
|
+
const DEFAULT_UNCLOSED_ROUND_GRACE_MS = 10 * 60_000;
|
|
95
|
+
const MAX_UNCLOSED_ROUND_GRACE_MS = 60 * 60_000;
|
|
96
|
+
const DEFAULT_STALLED_RECOVERY_BACKOFF_MS = 60_000;
|
|
97
|
+
const MAX_STALLED_RECOVERY_BACKOFF_MS = 15 * 60_000;
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
function transitionDecisionOrThrow(decision: ControllerRoundTransitionDecision): { record: ControllerRoundRelayRecord; action?: string; changed: boolean } {
|
|
101
|
+
if (decision.kind === 'accept') return { record: decision.next, action: decision.action, changed: true };
|
|
102
|
+
if (decision.kind === 'accept_atomic') throw new Error('CONTROLLER_RELAY_ATOMIC_TRANSITION_REQUIRES_TRANSACTION');
|
|
103
|
+
if (decision.kind === 'no_op') return { record: decision.current, changed: false };
|
|
104
|
+
if (decision.kind === 'needs_evidence') throw new Error(decision.code);
|
|
105
|
+
throw new Error(decision.code);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function atomicTransitionDecisionOrThrow(decision: ControllerRoundTransitionDecision): Extract<ControllerRoundTransitionDecision, { kind: 'accept_atomic' }> {
|
|
109
|
+
if (decision.kind === 'accept_atomic') return decision;
|
|
110
|
+
if (decision.kind === 'needs_evidence' || decision.kind === 'reject') throw new Error(decision.code);
|
|
111
|
+
throw new Error('CONTROLLER_RELAY_ATOMIC_TRANSITION_REQUIRED');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function applyControllerRoundTransition(
|
|
115
|
+
options: ControllerRoundRelayStoreOptions,
|
|
116
|
+
current: ControlPlaneRecord<ControllerRoundRelayRecord> | undefined,
|
|
117
|
+
event: ControllerRoundTransitionEvent,
|
|
118
|
+
): ControllerRoundRelayRecord {
|
|
119
|
+
const decided = transitionDecisionOrThrow(decideControllerRoundTransition(current?.value, event));
|
|
120
|
+
if (!decided.changed) return decided.record;
|
|
121
|
+
const key = current?.value.originWorkId ?? (event.type === 'occurrence_requested' ? event.originWorkId : undefined);
|
|
122
|
+
if (!key) throw new Error('CONTROLLER_RELAY_TRANSITION_KEY_REQUIRED');
|
|
123
|
+
writeControlPlaneRecord(options.controllerHome, {
|
|
124
|
+
namespace: NAMESPACE, scope: options.repoId, key, schemaVersion: SCHEMA_VERSION, value: decided.record,
|
|
125
|
+
action: decided.action ?? `controller_round_transition_${event.type}`, expectedRevision: current?.revision ?? null,
|
|
126
|
+
});
|
|
127
|
+
return decided.record;
|
|
128
|
+
}
|
|
129
|
+
function nowIso(options: ControllerRoundRelayStoreOptions): string {
|
|
130
|
+
return options.now?.() ?? new Date().toISOString();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function newControllerRoundAuthorityId(): string {
|
|
134
|
+
return `cra_${randomUUID().replace(/-/g, '')}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function bounded(value: string | undefined, max: number): string | undefined {
|
|
138
|
+
const normalized = value?.trim();
|
|
139
|
+
return normalized ? normalized.slice(0, max) : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeScopeId(value: string): string {
|
|
143
|
+
const normalized = value.trim();
|
|
144
|
+
if (!normalized || /[\r\n]/.test(normalized)) throw new Error('CONTROLLER_RELAY_SCOPE_INVALID');
|
|
145
|
+
return normalized.slice(0, 240);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function boundedInteger(value: number | undefined, fallback: number, min: number, max: number): number {
|
|
149
|
+
if (value === undefined || !Number.isFinite(value)) return fallback;
|
|
150
|
+
return Math.max(min, Math.min(Math.trunc(value), max));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function relayLock<T>(
|
|
154
|
+
options: ControllerRoundRelayStoreOptions,
|
|
155
|
+
relayScopeId: string,
|
|
156
|
+
actor: string,
|
|
157
|
+
operation: () => T,
|
|
158
|
+
): T {
|
|
159
|
+
const lockId = createHash('sha256').update(relayScopeId).digest('hex').slice(0, 20);
|
|
160
|
+
return withControllerLock(
|
|
161
|
+
options.controllerHome,
|
|
162
|
+
{ scope: 'task', repoId: options.repoId, taskId: `controller-round-relay-${lockId}` },
|
|
163
|
+
actor,
|
|
164
|
+
operation,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function readRelayRecord(
|
|
169
|
+
options: ControllerRoundRelayStoreOptions,
|
|
170
|
+
workId: string,
|
|
171
|
+
): ControlPlaneRecord<ControllerRoundRelayRecord> | undefined {
|
|
172
|
+
return readControlPlaneRecord<ControllerRoundRelayRecord>(options.controllerHome, NAMESPACE, options.repoId, workId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function getControllerRoundRelay(
|
|
176
|
+
options: ControllerRoundRelayStoreOptions,
|
|
177
|
+
workId: string,
|
|
178
|
+
): ControllerRoundRelayRecord | undefined {
|
|
179
|
+
return readRelayRecord(options, workId)?.value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve an existing Requirement-scoped ControllerRound authority for another
|
|
184
|
+
* Work that is mechanically part of the same durable Requirement/Work graph.
|
|
185
|
+
* This never mints, copies, or rebinds relay state; callers may only project the
|
|
186
|
+
* already-proven opaque authority into the target Work's ControllerSession.
|
|
187
|
+
*/
|
|
188
|
+
export function resolveRequirementControllerRoundRelayForWork(
|
|
189
|
+
options: ControllerRoundRelayStoreOptions,
|
|
190
|
+
input: { workId: string; authorityId: string; relayScopeId: string },
|
|
191
|
+
): ControllerRoundRelayRecord | undefined {
|
|
192
|
+
const workId = input.workId.trim();
|
|
193
|
+
const authorityId = input.authorityId.trim();
|
|
194
|
+
const relayScopeId = input.relayScopeId.trim();
|
|
195
|
+
if (!workId || !authorityId || !relayScopeId.startsWith('requirement:')) return undefined;
|
|
196
|
+
const candidate = latestRelayRecordsByScope(options).find((entry) =>
|
|
197
|
+
entry.relayScopeId === relayScopeId
|
|
198
|
+
&& entry.authorityId?.trim() === authorityId
|
|
199
|
+
&& Boolean(entry.requirementId)
|
|
200
|
+
&& `requirement:${entry.requirementId}` === relayScopeId);
|
|
201
|
+
if (!candidate) return undefined;
|
|
202
|
+
return relevantWork(options, candidate).some((work) => work.workId === workId) ? candidate : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function relayHistory(options: ControllerRoundRelayStoreOptions, relayScopeId: string): ControllerRoundRelayRecord[] {
|
|
206
|
+
return listControlPlaneRecords<ControllerRoundRelayRecord>(options.controllerHome, {
|
|
207
|
+
namespace: NAMESPACE,
|
|
208
|
+
scope: options.repoId,
|
|
209
|
+
limit: 5_000,
|
|
210
|
+
})
|
|
211
|
+
.map((entry) => entry.value)
|
|
212
|
+
.filter((entry) => entry.relayScopeId === relayScopeId && entry.status !== 'handed_off')
|
|
213
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function latestRelayRecordsByScope(options: ControllerRoundRelayStoreOptions): ControllerRoundRelayRecord[] {
|
|
217
|
+
const latest = new Map<string, ControllerRoundRelayRecord>();
|
|
218
|
+
for (const entry of listControlPlaneRecords<ControllerRoundRelayRecord>(options.controllerHome, {
|
|
219
|
+
namespace: NAMESPACE,
|
|
220
|
+
scope: options.repoId,
|
|
221
|
+
limit: 5_000,
|
|
222
|
+
}).map((record) => record.value)) {
|
|
223
|
+
if (entry.status === 'handed_off') continue;
|
|
224
|
+
const current = latest.get(entry.relayScopeId);
|
|
225
|
+
if (!current || entry.updatedAt > current.updatedAt) latest.set(entry.relayScopeId, entry);
|
|
226
|
+
}
|
|
227
|
+
return [...latest.values()].sort((left, right) => left.updatedAt.localeCompare(right.updatedAt));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function requirementForRelay(options: ControllerRoundRelayStoreOptions, requirementId: string | undefined) {
|
|
231
|
+
return requirementId ? readRequirement({ controllerHome: options.controllerHome }, requirementId)?.value : undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function relevantWork(options: ControllerRoundRelayStoreOptions, record: Pick<ControllerRoundRelayRecord, 'relayScopeId' | 'originWorkId' | 'requirementId'>): WorkContract[] {
|
|
235
|
+
const all = readWorkContractStore({ controllerHome: options.controllerHome, repoId: options.repoId }).contracts;
|
|
236
|
+
const linkedWorkIds = new Set([
|
|
237
|
+
record.originWorkId,
|
|
238
|
+
...relayHistory(options, record.relayScopeId).map((entry) => entry.originWorkId),
|
|
239
|
+
]);
|
|
240
|
+
if (record.requirementId) {
|
|
241
|
+
for (const work of all) {
|
|
242
|
+
if (work.requirementId === record.requirementId) linkedWorkIds.add(work.workId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
let expanded = true;
|
|
246
|
+
while (expanded) {
|
|
247
|
+
expanded = false;
|
|
248
|
+
for (const work of all) {
|
|
249
|
+
if (linkedWorkIds.has(work.workId)) {
|
|
250
|
+
if (work.parentWorkId && !linkedWorkIds.has(work.parentWorkId)) {
|
|
251
|
+
linkedWorkIds.add(work.parentWorkId);
|
|
252
|
+
expanded = true;
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (work.parentWorkId && linkedWorkIds.has(work.parentWorkId)) {
|
|
257
|
+
linkedWorkIds.add(work.workId);
|
|
258
|
+
expanded = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return all
|
|
263
|
+
.filter((work) => linkedWorkIds.has(work.workId))
|
|
264
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function semanticVerificationFacts(work: WorkContract): Array<{
|
|
268
|
+
checkId: string; verificationInputFingerprint: string; checkDefinitionDigest: string;
|
|
269
|
+
checkEnvironmentFingerprint: string; checkCacheKey: string; outcome: string; status: string;
|
|
270
|
+
}> {
|
|
271
|
+
const unique = new Map<string, {
|
|
272
|
+
checkId: string; verificationInputFingerprint: string; checkDefinitionDigest: string;
|
|
273
|
+
checkEnvironmentFingerprint: string; checkCacheKey: string; outcome: string; status: string;
|
|
274
|
+
}>();
|
|
275
|
+
for (const record of work.checkRefs.slice(-32)) {
|
|
276
|
+
const receipt = record.receipt;
|
|
277
|
+
if (!receipt || !record.verificationInputFingerprint || !receipt.checkDefinitionDigest
|
|
278
|
+
|| !receipt.checkEnvironmentFingerprint || !receipt.checkCacheKey
|
|
279
|
+
|| !['valid_pass', 'valid_fail'].includes(record.outcome)
|
|
280
|
+
|| !['passed', 'failed'].includes(receipt.status)) continue;
|
|
281
|
+
const fact = {
|
|
282
|
+
checkId: record.checkId,
|
|
283
|
+
verificationInputFingerprint: record.verificationInputFingerprint,
|
|
284
|
+
checkDefinitionDigest: receipt.checkDefinitionDigest,
|
|
285
|
+
checkEnvironmentFingerprint: receipt.checkEnvironmentFingerprint,
|
|
286
|
+
checkCacheKey: receipt.checkCacheKey,
|
|
287
|
+
outcome: record.outcome,
|
|
288
|
+
status: receipt.status,
|
|
289
|
+
};
|
|
290
|
+
unique.set(JSON.stringify(fact), fact);
|
|
291
|
+
}
|
|
292
|
+
return [...unique.values()].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function mechanicalStateFingerprint(
|
|
296
|
+
options: ControllerRoundRelayStoreOptions,
|
|
297
|
+
work: WorkContract,
|
|
298
|
+
requirementId: string | undefined,
|
|
299
|
+
relayScopeId: string,
|
|
300
|
+
): string {
|
|
301
|
+
const requirement = requirementForRelay(options, requirementId);
|
|
302
|
+
const works = relevantWork(options, { relayScopeId, originWorkId: work.workId, requirementId })
|
|
303
|
+
.map((entry) => ({
|
|
304
|
+
workId: entry.workId,
|
|
305
|
+
parentWorkId: entry.parentWorkId,
|
|
306
|
+
status: entry.status,
|
|
307
|
+
phase: entry.phase,
|
|
308
|
+
dispatchState: entry.dispatchState,
|
|
309
|
+
evidenceState: entry.evidenceState,
|
|
310
|
+
completionOutcome: entry.completionOutcome,
|
|
311
|
+
verificationFacts: semanticVerificationFacts(entry),
|
|
312
|
+
executionConcurrency: entry.executionConcurrency ? {
|
|
313
|
+
status: entry.executionConcurrency.status,
|
|
314
|
+
source: entry.executionConcurrency.source,
|
|
315
|
+
blockerCode: entry.executionConcurrency.blockerCode,
|
|
316
|
+
blockingWorkId: entry.executionConcurrency.blockingWorkId,
|
|
317
|
+
semanticScopeKeys: entry.executionConcurrency.semanticScopeKeys,
|
|
318
|
+
resourceKeys: entry.executionConcurrency.resourceKeys,
|
|
319
|
+
leaseRepoId: entry.executionConcurrency.leaseRepoId,
|
|
320
|
+
resourceIntents: entry.executionConcurrency.resourceIntents,
|
|
321
|
+
wakeTrigger: entry.executionConcurrency.wakeTrigger,
|
|
322
|
+
} : undefined,
|
|
323
|
+
}))
|
|
324
|
+
.sort((left, right) => left.workId.localeCompare(right.workId));
|
|
325
|
+
const linkedWorkIds = new Set(works.map((entry) => entry.workId));
|
|
326
|
+
const handoffs = listHandoffItems({ controllerHome: options.controllerHome, repoId: options.repoId, status: 'active', limit: 100 })
|
|
327
|
+
.filter((handoff) => !handoff.workId || linkedWorkIds.has(handoff.workId))
|
|
328
|
+
.map((handoff) => ({
|
|
329
|
+
id: handoff.id,
|
|
330
|
+
workId: handoff.workId,
|
|
331
|
+
status: handoff.status,
|
|
332
|
+
severity: handoff.severity,
|
|
333
|
+
title: handoff.title,
|
|
334
|
+
reason: handoff.reason,
|
|
335
|
+
creationReason: handoff.creationReason,
|
|
336
|
+
blockingDecision: handoff.blockingDecision,
|
|
337
|
+
}))
|
|
338
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
339
|
+
return createHash('sha256').update(JSON.stringify({
|
|
340
|
+
requirement: requirement ? {
|
|
341
|
+
requirementId: requirement.requirementId,
|
|
342
|
+
state: requirement.state,
|
|
343
|
+
revision: requirement.revision,
|
|
344
|
+
} : undefined,
|
|
345
|
+
works,
|
|
346
|
+
handoffs,
|
|
347
|
+
})).digest('hex');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Current semantic state identity for one existing ControllerRound lineage. Volatile persistence timestamps are intentionally excluded. */
|
|
351
|
+
export function readControllerRoundSemanticStateFingerprint(
|
|
352
|
+
options: ControllerRoundRelayStoreOptions,
|
|
353
|
+
workId: string,
|
|
354
|
+
): string | undefined {
|
|
355
|
+
const work = getWorkContract(options, workId);
|
|
356
|
+
const current = readRelayRecord(options, workId)?.value;
|
|
357
|
+
if (!work || !current) return undefined;
|
|
358
|
+
return mechanicalStateFingerprint(options, work, current.requirementId, current.relayScopeId);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function resolveRequirementId(
|
|
362
|
+
options: ControllerRoundRelayStoreOptions,
|
|
363
|
+
work: WorkContract,
|
|
364
|
+
requestedRequirementId: string | undefined,
|
|
365
|
+
): string | undefined {
|
|
366
|
+
const requested = bounded(requestedRequirementId, 160);
|
|
367
|
+
if (work.requirementId && requested && work.requirementId !== requested) {
|
|
368
|
+
throw new Error(`CONTROLLER_RELAY_REQUIREMENT_MISMATCH: Work ${work.workId} belongs to ${work.requirementId}`);
|
|
369
|
+
}
|
|
370
|
+
const requirementId = work.requirementId ?? requested;
|
|
371
|
+
if (requirementId && !requirementForRelay(options, requirementId)) {
|
|
372
|
+
throw new Error(`REQUIREMENT_NOT_FOUND: ${requirementId}`);
|
|
373
|
+
}
|
|
374
|
+
return requirementId;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function resolveRelayScope(
|
|
378
|
+
work: WorkContract,
|
|
379
|
+
requirementId: string | undefined,
|
|
380
|
+
requestedScopeId: string | undefined,
|
|
381
|
+
): string {
|
|
382
|
+
const canonicalRequirementScope = requirementId ? `requirement:${requirementId}` : undefined;
|
|
383
|
+
if (canonicalRequirementScope) {
|
|
384
|
+
if (requestedScopeId && normalizeScopeId(requestedScopeId) !== canonicalRequirementScope) {
|
|
385
|
+
throw new Error(`CONTROLLER_RELAY_SCOPE_MISMATCH: expected ${canonicalRequirementScope}`);
|
|
386
|
+
}
|
|
387
|
+
return canonicalRequirementScope;
|
|
388
|
+
}
|
|
389
|
+
return requestedScopeId ? normalizeScopeId(requestedScopeId) : `goal:${work.workId}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function assertControllerOwner(
|
|
393
|
+
options: ControllerRoundRelayStoreOptions,
|
|
394
|
+
workId: string,
|
|
395
|
+
identity: ControllerRoundRelayIdentity,
|
|
396
|
+
): ControllerSession & { claimGeneration: number } {
|
|
397
|
+
const owner = getControllerSession(options, workId);
|
|
398
|
+
if (!owner) throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${workId}`);
|
|
399
|
+
if (owner.controllerType !== identity.controllerType) throw new Error(`CONTROLLER_RELAY_CONTROLLER_TYPE_MISMATCH: ${workId}`);
|
|
400
|
+
if (owner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
|
|
401
|
+
const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
|
|
402
|
+
if (ownerPrincipal !== identity.principalId.trim()) throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
|
|
403
|
+
if (owner.sessionId !== identity.sessionId.trim()) throw new Error(`WORK_CONTROLLER_SESSION_MISMATCH: ${workId}`);
|
|
404
|
+
const ownerInstanceId = owner.controllerInstanceId?.trim();
|
|
405
|
+
if (ownerInstanceId && ownerInstanceId !== identity.controllerInstanceId.trim()) {
|
|
406
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
407
|
+
}
|
|
408
|
+
if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) throw new Error(`CONTROLLER_RELAY_CLAIM_GENERATION_REQUIRED: ${workId}`);
|
|
409
|
+
return owner as ControllerSession & { claimGeneration: number };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function assertControllerRoundSuccessorLineage(
|
|
413
|
+
options: ControllerRoundRelayStoreOptions,
|
|
414
|
+
predecessor: WorkContract,
|
|
415
|
+
successorWorkId: string,
|
|
416
|
+
): WorkContract {
|
|
417
|
+
const successor = getWorkContract(options, successorWorkId);
|
|
418
|
+
if (!successor) throw new Error(`CONTROLLER_RELAY_SUCCESSOR_WORK_NOT_FOUND: ${successorWorkId}`);
|
|
419
|
+
if (isTerminalWorkContractStatus(successor.status)) {
|
|
420
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_WORK_TERMINAL: ${successor.workId}:${successor.status}`);
|
|
421
|
+
}
|
|
422
|
+
if (successor.predecessorWorkId !== predecessor.workId) {
|
|
423
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_LINEAGE_MISMATCH: ${successor.workId}:predecessor=${successor.predecessorWorkId ?? 'none'}:expected=${predecessor.workId}`);
|
|
424
|
+
}
|
|
425
|
+
if (predecessor.requirementId && successor.requirementId !== predecessor.requirementId) {
|
|
426
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_REQUIREMENT_MISMATCH: ${successor.workId}`);
|
|
427
|
+
}
|
|
428
|
+
if (predecessor.planId && successor.planId !== predecessor.planId) {
|
|
429
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_PLAN_MISMATCH: ${successor.workId}`);
|
|
430
|
+
}
|
|
431
|
+
if (successor.workId === predecessor.workId) throw new Error('CONTROLLER_RELAY_SUCCESSOR_IDENTITY_REUSE_FORBIDDEN');
|
|
432
|
+
return successor;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Bind an already-admitted GoalWorkloop successor to the currently claimed
|
|
437
|
+
* ControllerRound. This stores only a mechanical handoff target: Goal/Plan
|
|
438
|
+
* semantics were decided before this call and are revalidated through Work
|
|
439
|
+
* lineage here. No successor Work is created by ControllerRound.
|
|
440
|
+
*/
|
|
441
|
+
export function bindControllerRoundSuccessorWork(
|
|
442
|
+
options: ControllerRoundRelayStoreOptions,
|
|
443
|
+
input: { workId: string; successorWorkId: string; identity: ControllerRoundRelayIdentity; controllerAuthorityId?: string },
|
|
444
|
+
): ControllerRoundRelayRecord {
|
|
445
|
+
const predecessor = getWorkContract(options, input.workId);
|
|
446
|
+
if (!predecessor) throw new Error(`WORK_NOT_FOUND: ${input.workId}`);
|
|
447
|
+
if (predecessor.status !== 'completed') {
|
|
448
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_PREDECESSOR_NOT_COMPLETED: ${predecessor.workId}:${predecessor.status}`);
|
|
449
|
+
}
|
|
450
|
+
const initial = readRelayRecord(options, predecessor.workId);
|
|
451
|
+
if (!initial) throw new Error(`CONTROLLER_RELAY_ROUND_NOT_OPEN: ${predecessor.workId}`);
|
|
452
|
+
return relayLock(options, initial.value.relayScopeId, `controller-relay-bind-successor:${input.identity.controllerId}`, () => {
|
|
453
|
+
const current = readRelayRecord(options, predecessor.workId);
|
|
454
|
+
if (!current || current.value.status !== 'claimed') {
|
|
455
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_REQUIRES_CLAIMED_ROUND: ${predecessor.workId}:${current?.value.status ?? 'missing'}`);
|
|
456
|
+
}
|
|
457
|
+
const expectedAuthorityId = current.value.authorityId?.trim() || '';
|
|
458
|
+
const requestedAuthorityId = input.controllerAuthorityId?.trim() || '';
|
|
459
|
+
if (expectedAuthorityId) {
|
|
460
|
+
if (!requestedAuthorityId) throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_REQUIRED: ${predecessor.workId}`);
|
|
461
|
+
if (requestedAuthorityId !== expectedAuthorityId) throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_MISMATCH: ${predecessor.workId}`);
|
|
462
|
+
const principalId = input.identity.principalId.trim();
|
|
463
|
+
if (!principalId || current.value.controllerId !== input.identity.controllerId || current.value.principalId !== principalId) {
|
|
464
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_MISMATCH: ${predecessor.workId}`);
|
|
465
|
+
}
|
|
466
|
+
if (relayControllerType(current.value) !== input.identity.controllerType || current.value.claimGeneration < 1) {
|
|
467
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_MISMATCH: ${predecessor.workId}`);
|
|
468
|
+
}
|
|
469
|
+
const liveOwner = getControllerSession(options, predecessor.workId);
|
|
470
|
+
if (liveOwner) {
|
|
471
|
+
const livePrincipal = liveOwner.principalId?.trim() || liveOwner.controllerId;
|
|
472
|
+
if (liveOwner.controllerId !== input.identity.controllerId
|
|
473
|
+
|| liveOwner.controllerType !== input.identity.controllerType
|
|
474
|
+
|| livePrincipal !== principalId) {
|
|
475
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_MISMATCH: ${predecessor.workId}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
// Legacy claimed rounds have no opaque per-round capability. Preserve only
|
|
480
|
+
// their exact live owner epoch; rollover is available after the round has
|
|
481
|
+
// been upgraded by the canonical launcher/recovery path.
|
|
482
|
+
const owner = assertControllerOwner(options, predecessor.workId, input.identity);
|
|
483
|
+
if (current.value.controllerId !== owner.controllerId
|
|
484
|
+
|| current.value.principalId !== (owner.principalId?.trim() || owner.controllerId)
|
|
485
|
+
|| current.value.claimGeneration !== owner.claimGeneration) {
|
|
486
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_BIND_AUTHORITY_MISMATCH: ${predecessor.workId}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const successor = assertControllerRoundSuccessorLineage(options, predecessor, input.successorWorkId.trim());
|
|
490
|
+
return applyControllerRoundTransition(options, current, {
|
|
491
|
+
type: 'successor_bound', at: nowIso(options), successorWorkId: successor.workId,
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function sameAssistantContextSnapshot(
|
|
497
|
+
left: AssistantContextSnapshot | undefined,
|
|
498
|
+
right: AssistantContextSnapshot | null | undefined,
|
|
499
|
+
): boolean {
|
|
500
|
+
if (right === undefined) return true;
|
|
501
|
+
if (right === null) return left === undefined;
|
|
502
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function validatedAssistantContextUsage(
|
|
506
|
+
record: ControllerRoundRelayRecord,
|
|
507
|
+
input: SubmitControllerRoundDispositionInput,
|
|
508
|
+
): AssistantContextUsage[] {
|
|
509
|
+
const usage = input.assistantContextUsage ?? [];
|
|
510
|
+
if (!Array.isArray(usage) || usage.length > 32) throw new Error('CONTROLLER_ASSISTANT_CONTEXT_USAGE_LIMIT');
|
|
511
|
+
const snapshot = record.assistantContextSnapshot;
|
|
512
|
+
const digest = input.assistantContextDigest?.trim();
|
|
513
|
+
if (usage.length > 0 && (!snapshot || !digest)) throw new Error('CONTROLLER_ASSISTANT_CONTEXT_SNAPSHOT_REQUIRED');
|
|
514
|
+
if (digest && (!snapshot || digest !== snapshot.digest)) throw new Error('CONTROLLER_ASSISTANT_CONTEXT_DIGEST_MISMATCH');
|
|
515
|
+
if (!usage.length) return [];
|
|
516
|
+
const expected = new Map(snapshot!.items.map((item) => [`${item.kind}:${item.itemId}`, item]));
|
|
517
|
+
const seen = new Set<string>();
|
|
518
|
+
for (const item of usage) {
|
|
519
|
+
const key = `${item.kind}:${item.itemId}`;
|
|
520
|
+
if (!expected.has(key) || seen.has(key) || !['used', 'rejected'].includes(item.decision)
|
|
521
|
+
|| typeof item.reason !== 'string' || !item.reason.trim() || item.reason.length > 1_000) {
|
|
522
|
+
throw new Error('CONTROLLER_ASSISTANT_CONTEXT_USAGE_INVALID');
|
|
523
|
+
}
|
|
524
|
+
seen.add(key);
|
|
525
|
+
}
|
|
526
|
+
if (seen.size !== expected.size) throw new Error('CONTROLLER_ASSISTANT_CONTEXT_USAGE_INCOMPLETE');
|
|
527
|
+
return usage.map((item) => ({ ...item, reason: item.reason.trim() }));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function persistClaimedAssistantContextSnapshot(
|
|
531
|
+
options: ControllerRoundRelayStoreOptions,
|
|
532
|
+
record: ControllerRoundRelayRecord,
|
|
533
|
+
requested: AssistantContextSnapshot | null | undefined,
|
|
534
|
+
at: string,
|
|
535
|
+
): ControllerRoundRelayRecord {
|
|
536
|
+
if (requested === undefined || sameAssistantContextSnapshot(record.assistantContextSnapshot, requested)) return record;
|
|
537
|
+
if (record.assistantContextSnapshot) {
|
|
538
|
+
throw new Error(`CONTROLLER_ASSISTANT_CONTEXT_CLAIM_MISMATCH: ${record.originWorkId}`);
|
|
539
|
+
}
|
|
540
|
+
if (requested === null) return record;
|
|
541
|
+
const current = readRelayRecord(options, record.originWorkId);
|
|
542
|
+
if (!current || current.value.relayScopeId !== record.relayScopeId || current.value.status !== 'claimed'
|
|
543
|
+
|| current.value.claimGeneration !== record.claimGeneration) {
|
|
544
|
+
throw new Error(`CONTROLLER_ASSISTANT_CONTEXT_CLAIM_STALE: ${record.originWorkId}`);
|
|
545
|
+
}
|
|
546
|
+
if (current.value.assistantContextSnapshot) {
|
|
547
|
+
if (sameAssistantContextSnapshot(current.value.assistantContextSnapshot, requested)) return current.value;
|
|
548
|
+
throw new Error(`CONTROLLER_ASSISTANT_CONTEXT_CLAIM_MISMATCH: ${record.originWorkId}`);
|
|
549
|
+
}
|
|
550
|
+
const next: ControllerRoundRelayRecord = { ...current.value, assistantContextSnapshot: requested, updatedAt: at };
|
|
551
|
+
writeControlPlaneRecord(options.controllerHome, {
|
|
552
|
+
namespace: NAMESPACE, scope: options.repoId, key: record.originWorkId, schemaVersion: SCHEMA_VERSION, value: next,
|
|
553
|
+
action: 'controller_round_assistant_context_bound', expectedRevision: current.revision,
|
|
554
|
+
});
|
|
555
|
+
return next;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function pendingQualitySignals(record: ControllerRoundRelayRecord): ExecutionQualitySignal[] {
|
|
559
|
+
const handled = new Set(record.qualityDecisions?.map(decision => decision.fingerprint) ?? []);
|
|
560
|
+
return deriveClosedRoundQualitySignals(record.observationWindow ?? [], { repeatedStateCount: record.repeatedStateCount,
|
|
561
|
+
maxRepeatedState: record.maxRepeatedState, waiting: record.status === 'waiting' || record.status === 'waiting_for_user',
|
|
562
|
+
roundRef: `${record.relayScopeId}:${record.roundCount}` }).map(signal => ({ ...signal,
|
|
563
|
+
fingerprint: createHash('sha256').update(JSON.stringify([signal.code, [...signal.evidenceRefs].sort()])).digest('hex'),
|
|
564
|
+
})).filter(signal => !handled.has(signal.fingerprint));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function closedRoundObservation(work: WorkContract, roundRef: string, stateFingerprint: string, waiting: boolean, assistantContext: AssistantContextSnapshot | undefined, assistantContextUsage: AssistantContextUsage[]): ClosedRoundObservation {
|
|
568
|
+
const coverageGaps: string[] = [];
|
|
569
|
+
const verifications: ClosedRoundObservation['verifications'] = [];
|
|
570
|
+
for (const check of work.checkRefs.slice(-32)) {
|
|
571
|
+
const receipt = check.receipt;
|
|
572
|
+
// HEAD alone does not identify dirty source. Require the exact input fingerprint.
|
|
573
|
+
if (!receipt || !check.verificationInputFingerprint || !receipt.checkDefinitionDigest || !receipt.checkEnvironmentFingerprint) {
|
|
574
|
+
coverageGaps.push('check_identity_incomplete'); continue;
|
|
575
|
+
}
|
|
576
|
+
if (receipt.status !== 'passed' && receipt.status !== 'failed') continue;
|
|
577
|
+
verifications.push({ checkId: `${receipt.checkId}:${receipt.checkDefinitionDigest}`, sourceDigest: check.verificationInputFingerprint,
|
|
578
|
+
environment: receipt.checkEnvironmentFingerprint, outcome: receipt.status, evidenceRef: receipt.receiptId });
|
|
579
|
+
}
|
|
580
|
+
// Generic evidence references without content identity cannot prove absence of new knowledge.
|
|
581
|
+
if (work.evidenceRefs.length) coverageGaps.push('generic_evidence_content_identity_unavailable');
|
|
582
|
+
if (work.checkRefs.length > 32) coverageGaps.push('check_window_truncated');
|
|
583
|
+
if (!assistantContext) coverageGaps.push('assistant_context_snapshot_unavailable');
|
|
584
|
+
else if (assistantContext.items.length > 0 && assistantContextUsage.length === 0) coverageGaps.push('assistant_context_usage_unreported');
|
|
585
|
+
const accepted = work.semanticAcceptanceEvidence ?? [];
|
|
586
|
+
if (accepted.length > 32) coverageGaps.push('accepted_result_window_truncated');
|
|
587
|
+
return { roundRef, workId: work.workId, requirementId: work.requirementId, planId: work.planId,
|
|
588
|
+
designVersion: work.engineeringContext?.sourceIdentity.kind === 'revision' ? work.engineeringContext.sourceIdentity.revision : undefined,
|
|
589
|
+
rootCauses: (work.engineeringContext?.blockerDispositions ?? []).slice(-32).filter(disposition => disposition.classification === 'same_root_cause')
|
|
590
|
+
.map(disposition => ({ dispositionRef: disposition.receiptId, rootCauseId: disposition.blockerId,
|
|
591
|
+
designScope: JSON.stringify([...disposition.semanticScopeKeys].sort()), controllerConfirmed: true })),
|
|
592
|
+
stateFingerprint, waiting, coverageGaps: [...new Set(coverageGaps)], verifications, assistantContext, assistantContextUsage,
|
|
593
|
+
evidenceIdentities: work.checkRefs.slice(-32).flatMap(check => check.receipt ? [check.receipt.resultDigest] : []),
|
|
594
|
+
acceptedResultIdentities: accepted.slice(-32).map(result => createHash('sha256').update(JSON.stringify([result.criterion, [...result.evidenceIds].sort()])).digest('hex')) };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function submitControllerRoundDisposition(
|
|
598
|
+
options: ControllerRoundRelayStoreOptions,
|
|
599
|
+
input: SubmitControllerRoundDispositionInput,
|
|
600
|
+
): ControllerRoundRelayRecord {
|
|
601
|
+
if (!CONTROLLER_ROUND_DISPOSITIONS.includes(input.disposition)) throw new Error('CONTROLLER_RELAY_DISPOSITION_INVALID');
|
|
602
|
+
const work = getWorkContract(options, input.workId);
|
|
603
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${input.workId}`);
|
|
604
|
+
const requirementId = resolveRequirementId(options, work, input.requirementId);
|
|
605
|
+
const relayScopeId = resolveRelayScope(work, requirementId, input.relayScopeId);
|
|
606
|
+
const terminal = isTerminalWorkContractStatus(work.status);
|
|
607
|
+
|
|
608
|
+
return relayLock(options, relayScopeId, `controller-relay-submit:${input.identity.controllerId}`, () => {
|
|
609
|
+
const existing = readRelayRecord(options, work.workId);
|
|
610
|
+
if (!existing) throw new Error(`CONTROLLER_RELAY_ROUND_NOT_OPEN: ${work.workId}`);
|
|
611
|
+
if (existing.value.relayScopeId !== relayScopeId) {
|
|
612
|
+
throw new Error(`CONTROLLER_RELAY_SCOPE_MISMATCH: Work ${work.workId} is already bound to ${existing.value.relayScopeId}`);
|
|
613
|
+
}
|
|
614
|
+
if (existing.value.status !== 'claimed') {
|
|
615
|
+
throw new Error(`CONTROLLER_RELAY_ROUND_NOT_CLAIMED: ${existing.value.status}`);
|
|
616
|
+
}
|
|
617
|
+
const terminalSuccessor = terminal
|
|
618
|
+
&& work.status === 'completed'
|
|
619
|
+
&& input.disposition === 'continue_immediately'
|
|
620
|
+
&& existing.value.successorWorkId
|
|
621
|
+
? assertControllerRoundSuccessorLineage(options, work, existing.value.successorWorkId)
|
|
622
|
+
: undefined;
|
|
623
|
+
const terminalRoundClosureAllowed = terminal
|
|
624
|
+
&& work.status === 'completed'
|
|
625
|
+
&& (input.disposition === 'goal_complete' || Boolean(terminalSuccessor));
|
|
626
|
+
if (terminal && !terminalRoundClosureAllowed) {
|
|
627
|
+
throw new Error(`CONTROLLER_RELAY_WORK_TERMINAL: ${work.status}`);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const liveOwner = getControllerSession(options, work.workId);
|
|
631
|
+
const authority = terminal
|
|
632
|
+
? (() => {
|
|
633
|
+
// The physical Work may be completed while its old live lease still exists.
|
|
634
|
+
// goal_complete is semantic round closure, not a terminal Work reclaim: use
|
|
635
|
+
// the authenticated controller/principal plus the already-claimed relay as
|
|
636
|
+
// authority and allow MCP session / Runtime instance rotation. If a live
|
|
637
|
+
// owner remains, it must still agree on controller type and principal.
|
|
638
|
+
const principalId = input.identity.principalId.trim();
|
|
639
|
+
const controllerInstanceId = input.identity.controllerInstanceId.trim();
|
|
640
|
+
const sessionId = input.identity.sessionId.trim();
|
|
641
|
+
if (!principalId || !controllerInstanceId || !sessionId) throw new Error(`CONTROLLER_RELAY_TERMINAL_AUTHORITY_REQUIRED: ${work.workId}`);
|
|
642
|
+
const expectedControllerType = relayControllerType(existing.value);
|
|
643
|
+
if (input.identity.controllerType !== expectedControllerType) throw new Error(`CONTROLLER_RELAY_CONTROLLER_TYPE_MISMATCH: ${work.workId}`);
|
|
644
|
+
if (liveOwner) {
|
|
645
|
+
if (liveOwner.controllerType !== expectedControllerType) throw new Error(`CONTROLLER_RELAY_CONTROLLER_TYPE_MISMATCH: ${work.workId}`);
|
|
646
|
+
if (liveOwner.controllerId !== input.identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${work.workId}`);
|
|
647
|
+
if ((liveOwner.principalId?.trim() || liveOwner.controllerId) !== principalId) {
|
|
648
|
+
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${work.workId}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (existing.value.controllerId !== input.identity.controllerId) throw new Error(`CONTROLLER_RELAY_CLAIM_CONTROLLER_MISMATCH: ${work.workId}`);
|
|
652
|
+
if (existing.value.principalId !== principalId) throw new Error(`CONTROLLER_RELAY_CLAIM_PRINCIPAL_MISMATCH: ${work.workId}`);
|
|
653
|
+
if (existing.value.claimGeneration < 1) throw new Error(`CONTROLLER_RELAY_CLAIM_GENERATION_REQUIRED: ${work.workId}`);
|
|
654
|
+
return {
|
|
655
|
+
controllerId: existing.value.controllerId,
|
|
656
|
+
controllerType: expectedControllerType,
|
|
657
|
+
principalId: existing.value.principalId,
|
|
658
|
+
controllerInstanceId,
|
|
659
|
+
sessionId,
|
|
660
|
+
claimGeneration: existing.value.claimGeneration,
|
|
661
|
+
};
|
|
662
|
+
})()
|
|
663
|
+
: (() => {
|
|
664
|
+
const owner = assertControllerOwner(options, work.workId, input.identity);
|
|
665
|
+
return {
|
|
666
|
+
controllerId: owner.controllerId,
|
|
667
|
+
controllerType: owner.controllerType,
|
|
668
|
+
principalId: owner.principalId?.trim() || owner.controllerId,
|
|
669
|
+
controllerInstanceId: owner.controllerInstanceId?.trim() || '',
|
|
670
|
+
sessionId: owner.sessionId,
|
|
671
|
+
claimGeneration: owner.claimGeneration,
|
|
672
|
+
};
|
|
673
|
+
})();
|
|
674
|
+
if (existing.value.controllerId !== authority.controllerId) {
|
|
675
|
+
throw new Error(`CONTROLLER_RELAY_CLAIM_CONTROLLER_MISMATCH: ${work.workId}`);
|
|
676
|
+
}
|
|
677
|
+
if (existing.value.principalId !== authority.principalId) {
|
|
678
|
+
throw new Error(`CONTROLLER_RELAY_CLAIM_PRINCIPAL_MISMATCH: ${work.workId}`);
|
|
679
|
+
}
|
|
680
|
+
// A completed Work may have already released its live controller session before
|
|
681
|
+
// the prompt-required terminal disposition arrives. At that point the exact
|
|
682
|
+
// authenticated controller/principal + Work + relay scope remain the lineage
|
|
683
|
+
// fence; MCP session and canonical Runtime instance are allowed to rotate.
|
|
684
|
+
const terminalLineageMigration = terminal;
|
|
685
|
+
if (!terminalLineageMigration && existing.value.controllerInstanceId !== authority.controllerInstanceId) {
|
|
686
|
+
throw new Error(`CONTROLLER_RELAY_CLAIM_INSTANCE_MISMATCH: ${work.workId}`);
|
|
687
|
+
}
|
|
688
|
+
if (!terminalLineageMigration && existing.value.claimGeneration !== authority.claimGeneration) {
|
|
689
|
+
throw new Error(`CONTROLLER_RELAY_CLAIM_GENERATION_MISMATCH: ${work.workId}`);
|
|
690
|
+
}
|
|
691
|
+
const previous = relayHistory(options, relayScopeId)[0];
|
|
692
|
+
const assistantContextUsage = validatedAssistantContextUsage(existing.value, input);
|
|
693
|
+
const at = nowIso(options);
|
|
694
|
+
const qualityDecisions = input.executionQualityDecisions ?? [];
|
|
695
|
+
if (!Array.isArray(qualityDecisions) || qualityDecisions.length > 8) throw new Error('CONTROLLER_QUALITY_DECISION_LIMIT');
|
|
696
|
+
const pending = new Set(pendingQualitySignals(existing.value).map(signal => signal.fingerprint));
|
|
697
|
+
const normalizedQualityDecisions: ExecutionQualityDecision[] = [];
|
|
698
|
+
for (const decision of qualityDecisions) {
|
|
699
|
+
if (!pending.has(decision.fingerprint) || !['no_adjustment', 'adjustment'].includes(decision.action)
|
|
700
|
+
|| typeof decision.reason !== 'string' || !decision.reason.trim() || decision.reason.length > 1000
|
|
701
|
+
|| decision.action === 'adjustment' && (typeof decision.verificationCondition !== 'string' || !decision.verificationCondition.trim() || decision.verificationCondition.length > 1000)) {
|
|
702
|
+
throw new Error('CONTROLLER_QUALITY_DECISION_INVALID');
|
|
703
|
+
}
|
|
704
|
+
normalizedQualityDecisions.push({ ...decision, decidedAt: at });
|
|
705
|
+
pending.delete(decision.fingerprint);
|
|
706
|
+
}
|
|
707
|
+
const knownDecisions = [...(existing.value.qualityDecisions ?? []), ...normalizedQualityDecisions];
|
|
708
|
+
const submittedAdjustmentResults = input.executionQualityAdjustmentResults ?? [];
|
|
709
|
+
if (!Array.isArray(submittedAdjustmentResults) || submittedAdjustmentResults.length > 8) throw new Error('CONTROLLER_QUALITY_ADJUSTMENT_RESULT_LIMIT');
|
|
710
|
+
const knownResultFingerprints = new Set((existing.value.qualityAdjustmentResults ?? []).map(result => result.fingerprint));
|
|
711
|
+
const normalizedAdjustmentResults: ExecutionQualityAdjustmentResult[] = [];
|
|
712
|
+
for (const submitted of submittedAdjustmentResults) {
|
|
713
|
+
const decision = knownDecisions.find(candidate => candidate.fingerprint === submitted.fingerprint);
|
|
714
|
+
const refs = Array.isArray(submitted.evidenceRefs) ? submitted.evidenceRefs.map(ref => String(ref).trim()).filter(Boolean) : [];
|
|
715
|
+
if (!decision || decision.action !== 'adjustment' || !decision.decidedAt
|
|
716
|
+
|| !['improved', 'not_improved', 'inconclusive'].includes(submitted.outcome)
|
|
717
|
+
|| typeof submitted.reason !== 'string' || !submitted.reason.trim() || submitted.reason.length > 1000
|
|
718
|
+
|| refs.length < 1 || refs.length > 16 || new Set(refs).size !== refs.length
|
|
719
|
+
|| knownResultFingerprints.has(submitted.fingerprint)) {
|
|
720
|
+
throw new Error('CONTROLLER_QUALITY_ADJUSTMENT_RESULT_INVALID');
|
|
721
|
+
}
|
|
722
|
+
const decidedAt = Date.parse(decision.decidedAt);
|
|
723
|
+
if (!Number.isFinite(decidedAt) || refs.some(ref => {
|
|
724
|
+
const check = work.checkRefs.find(candidate => candidate.receipt?.receiptId === ref);
|
|
725
|
+
return !check || !check.receipt || !Number.isFinite(Date.parse(check.recordedAt)) || Date.parse(check.recordedAt) <= decidedAt;
|
|
726
|
+
})) throw new Error('CONTROLLER_QUALITY_ADJUSTMENT_VERIFICATION_EVIDENCE_INVALID');
|
|
727
|
+
normalizedAdjustmentResults.push({ fingerprint: submitted.fingerprint, outcome: submitted.outcome, evidenceRefs: refs, reason: submitted.reason.trim(), verifiedAt: at });
|
|
728
|
+
knownResultFingerprints.add(submitted.fingerprint);
|
|
729
|
+
}
|
|
730
|
+
const requestedMaxRounds = boundedInteger(input.maxRounds, DEFAULT_MAX_ROUNDS, 1, 32);
|
|
731
|
+
const requestedMaxRepeatedState = boundedInteger(input.maxRepeatedState, DEFAULT_MAX_REPEATED_STATE, 1, 8);
|
|
732
|
+
const requestedMaxFailures = boundedInteger(input.maxFailures, DEFAULT_MAX_FAILURES, 1, 8);
|
|
733
|
+
const stateFingerprint = bounded(input.stateFingerprint, 256) ?? mechanicalStateFingerprint(options, terminalSuccessor ?? work, requirementId, relayScopeId);
|
|
734
|
+
const bindingId = bounded(input.bindingId, 500) ?? existing.value.bindingId;
|
|
735
|
+
const handoffId = bounded(input.handoffId, 200);
|
|
736
|
+
if (input.disposition === 'wait_for_user') {
|
|
737
|
+
if (!handoffId) throw new Error('CONTROLLER_RELAY_WAIT_FOR_USER_HANDOFF_REQUIRED');
|
|
738
|
+
const handoff = getHandoffItem(options, handoffId);
|
|
739
|
+
if (!handoff) throw new Error(`HANDOFF_NOT_FOUND: ${handoffId}`);
|
|
740
|
+
if (isTerminalHandoffStatus(handoff.status)) throw new Error(`CONTROLLER_RELAY_HANDOFF_TERMINAL: ${handoff.status}`);
|
|
741
|
+
if (handoff.workId && handoff.workId !== work.workId) throw new Error(`CONTROLLER_RELAY_HANDOFF_WORK_MISMATCH: ${handoffId}`);
|
|
742
|
+
}
|
|
743
|
+
const observationWindow = [
|
|
744
|
+
...(existing.value.observationWindow ?? []),
|
|
745
|
+
closedRoundObservation(work, `${relayScopeId}:${existing.value.roundCount}`, stateFingerprint, input.disposition === 'wait' || input.disposition === 'wait_for_user', existing.value.assistantContextSnapshot, assistantContextUsage),
|
|
746
|
+
].slice(-8);
|
|
747
|
+
const accumulatedQualityDecisions = [...(existing.value.qualityDecisions ?? []), ...normalizedQualityDecisions].slice(-64);
|
|
748
|
+
const accumulatedQualityAdjustmentResults = [...(existing.value.qualityAdjustmentResults ?? []), ...normalizedAdjustmentResults].slice(-64);
|
|
749
|
+
return applyControllerRoundTransition(options, existing, {
|
|
750
|
+
type: 'semantic_disposition_submitted', at, disposition: input.disposition, stateFingerprint,
|
|
751
|
+
maxRounds: requestedMaxRounds, maxRepeatedState: requestedMaxRepeatedState, maxFailures: requestedMaxFailures,
|
|
752
|
+
controllerSession: authority,
|
|
753
|
+
qualityDecisions: accumulatedQualityDecisions, qualityAdjustmentResults: accumulatedQualityAdjustmentResults, observationWindow,
|
|
754
|
+
...(handoffId ? { handoffId } : {}),
|
|
755
|
+
...(bounded(input.reason, 1_000) ? { reason: bounded(input.reason, 1_000) } : {}),
|
|
756
|
+
...(bindingId ? { bindingId } : {}),
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export function beginInitialControllerRoundDispatch(
|
|
762
|
+
options: ControllerRoundRelayStoreOptions,
|
|
763
|
+
input: BeginInitialControllerRoundDispatchInput,
|
|
764
|
+
): ControllerRoundRelayRecord {
|
|
765
|
+
const work = getWorkContract(options, input.workId);
|
|
766
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${input.workId}`);
|
|
767
|
+
if (isTerminalWorkContractStatus(work.status)) throw new Error(`CONTROLLER_RELAY_WORK_TERMINAL: ${work.status}`);
|
|
768
|
+
const requirementId = resolveRequirementId(options, work, input.requirementId);
|
|
769
|
+
const requirement = requirementForRelay(options, requirementId);
|
|
770
|
+
if (requirement && !['planned', 'active'].includes(requirement.state)) {
|
|
771
|
+
throw new Error(`CONTROLLER_RELAY_REQUIREMENT_TERMINAL: ${requirement.state}`);
|
|
772
|
+
}
|
|
773
|
+
const relayScopeId = resolveRelayScope(work, requirementId, input.relayScopeId);
|
|
774
|
+
|
|
775
|
+
return relayLock(options, relayScopeId, `controller-relay-launch:${input.identity.controllerId}`, () => {
|
|
776
|
+
const existing = readRelayRecord(options, work.workId);
|
|
777
|
+
if (existing && existing.value.relayScopeId !== relayScopeId) {
|
|
778
|
+
throw new Error(`CONTROLLER_RELAY_SCOPE_MISMATCH: Work ${work.workId} is already bound to ${existing.value.relayScopeId}`);
|
|
779
|
+
}
|
|
780
|
+
const previous = relayHistory(options, relayScopeId)[0];
|
|
781
|
+
const abandonedReleasedRound = previous?.status === 'failed' && previous.failureClass === 'abandoned_release';
|
|
782
|
+
const stateFingerprint = mechanicalStateFingerprint(options, work, requirementId, relayScopeId);
|
|
783
|
+
const occurrenceId = bounded(input.occurrenceId, 500);
|
|
784
|
+
return applyControllerRoundTransition(options, existing, {
|
|
785
|
+
type: 'occurrence_requested', at: nowIso(options), repoId: options.repoId, relayScopeId, originWorkId: work.workId,
|
|
786
|
+
...(requirementId ? { requirementId } : {}), identity: input.identity, stateFingerprint, proposedAuthorityId: newControllerRoundAuthorityId(),
|
|
787
|
+
maxRounds: boundedInteger(input.maxRounds, DEFAULT_MAX_ROUNDS, 1, 32), maxRepeatedState: boundedInteger(input.maxRepeatedState, DEFAULT_MAX_REPEATED_STATE, 1, 8),
|
|
788
|
+
maxFailures: boundedInteger(input.maxFailures, DEFAULT_MAX_FAILURES, 1, 8),
|
|
789
|
+
...(bounded(input.bindingId, 500) ? { bindingId: bounded(input.bindingId, 500) } : {}), ...(occurrenceId ? { occurrenceId } : {}),
|
|
790
|
+
abandonedReleaseRecovery: Boolean(abandonedReleasedRound),
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export function beginControllerRoundRelayAfterRelease(
|
|
796
|
+
options: ControllerRoundRelayStoreOptions,
|
|
797
|
+
input: { workId: string; releasedSession: ControllerSession },
|
|
798
|
+
): ControllerRoundRelayRecord | undefined {
|
|
799
|
+
const initial = readRelayRecord(options, input.workId);
|
|
800
|
+
if (!initial || initial.value.status !== 'pending_release') return undefined;
|
|
801
|
+
return relayLock(options, initial.value.relayScopeId, `controller-relay-begin:${input.releasedSession.controllerId}`, () => {
|
|
802
|
+
const current = readRelayRecord(options, input.workId);
|
|
803
|
+
if (!current || current.value.status !== 'pending_release') return undefined;
|
|
804
|
+
const record = current.value;
|
|
805
|
+
const releasedPrincipal = input.releasedSession.principalId?.trim() || input.releasedSession.controllerId;
|
|
806
|
+
if (
|
|
807
|
+
record.claimGeneration !== input.releasedSession.claimGeneration
|
|
808
|
+
|| record.controllerId !== input.releasedSession.controllerId
|
|
809
|
+
|| record.principalId !== releasedPrincipal
|
|
810
|
+
) {
|
|
811
|
+
throw new Error(`CONTROLLER_RELAY_RELEASE_FENCE_MISMATCH: ${input.workId}`);
|
|
812
|
+
}
|
|
813
|
+
const at = nowIso(options);
|
|
814
|
+
if (record.successorWorkId) {
|
|
815
|
+
const predecessor = getWorkContract(options, input.workId);
|
|
816
|
+
if (!predecessor || predecessor.status !== 'completed') {
|
|
817
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_PREDECESSOR_NOT_COMPLETED: ${input.workId}:${predecessor?.status ?? 'missing'}`);
|
|
818
|
+
}
|
|
819
|
+
const successor = assertControllerRoundSuccessorLineage(options, predecessor, record.successorWorkId);
|
|
820
|
+
const successorStateFingerprint = mechanicalStateFingerprint(options, successor, record.requirementId, record.relayScopeId);
|
|
821
|
+
return withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
822
|
+
const predecessorRelay = readControlPlaneRecordWithinTransaction<ControllerRoundRelayRecord>(
|
|
823
|
+
database, NAMESPACE, options.repoId, input.workId,
|
|
824
|
+
);
|
|
825
|
+
if (!predecessorRelay || predecessorRelay.value.status !== 'pending_release' || predecessorRelay.value.successorWorkId !== successor.workId) {
|
|
826
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_HANDOFF_STALE: ${input.workId}`);
|
|
827
|
+
}
|
|
828
|
+
const existingSuccessorRelay = readControlPlaneRecordWithinTransaction<ControllerRoundRelayRecord>(
|
|
829
|
+
database, NAMESPACE, options.repoId, successor.workId,
|
|
830
|
+
);
|
|
831
|
+
if (existingSuccessorRelay) {
|
|
832
|
+
if (existingSuccessorRelay.value.relayScopeId === record.relayScopeId
|
|
833
|
+
&& ['dispatching', 'dispatched', 'claimed'].includes(existingSuccessorRelay.value.status)) {
|
|
834
|
+
return existingSuccessorRelay.value;
|
|
835
|
+
}
|
|
836
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_ALREADY_HAS_ROUND: ${successor.workId}`);
|
|
837
|
+
}
|
|
838
|
+
const decision = atomicTransitionDecisionOrThrow(decideControllerRoundTransition(predecessorRelay.value, {
|
|
839
|
+
type: 'successor_release_handoff', at, successorWorkId: successor.workId, successorStateFingerprint, proposedAuthorityId: newControllerRoundAuthorityId(),
|
|
840
|
+
}));
|
|
841
|
+
writeControlPlaneRecordWithinTransaction(database, {
|
|
842
|
+
namespace: NAMESPACE, scope: options.repoId, key: predecessor.workId, schemaVersion: SCHEMA_VERSION,
|
|
843
|
+
value: decision.next, action: decision.action, expectedRevision: predecessorRelay.revision,
|
|
844
|
+
});
|
|
845
|
+
writeControlPlaneRecordWithinTransaction(database, {
|
|
846
|
+
namespace: NAMESPACE, scope: options.repoId, key: decision.relatedWorkId, schemaVersion: SCHEMA_VERSION,
|
|
847
|
+
value: decision.relatedNext, action: decision.relatedAction, expectedRevision: null,
|
|
848
|
+
});
|
|
849
|
+
return decision.relatedNext;
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
return applyControllerRoundTransition(options, current, {
|
|
853
|
+
type: 'controller_release_observed', at, proposedAuthorityId: newControllerRoundAuthorityId(),
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Mechanically close a claimed controller round only after its exact controller
|
|
860
|
+
* epoch has been released and durable state proves no controller lease remains.
|
|
861
|
+
* This never invents a semantic disposition; launcher_start must open a new round.
|
|
862
|
+
*/
|
|
863
|
+
export function reconcileControllerRoundAfterAbandonedRelease(
|
|
864
|
+
options: ControllerRoundRelayStoreOptions,
|
|
865
|
+
input: { workId: string; releasedSession: ControllerSession },
|
|
866
|
+
): ControllerRoundRelayRecord | undefined {
|
|
867
|
+
const initial = readRelayRecord(options, input.workId);
|
|
868
|
+
if (!initial || initial.value.status !== 'claimed') return undefined;
|
|
869
|
+
return relayLock(options, initial.value.relayScopeId, `controller-relay-abandoned-release:${input.releasedSession.controllerId}`, () => {
|
|
870
|
+
const current = readRelayRecord(options, input.workId);
|
|
871
|
+
if (!current || current.value.status !== 'claimed') return undefined;
|
|
872
|
+
const record = current.value;
|
|
873
|
+
const releasedPrincipal = input.releasedSession.principalId?.trim() || input.releasedSession.controllerId;
|
|
874
|
+
const releasedInstanceId = input.releasedSession.controllerInstanceId?.trim() || '';
|
|
875
|
+
if (
|
|
876
|
+
record.claimGeneration !== input.releasedSession.claimGeneration
|
|
877
|
+
|| record.controllerId !== input.releasedSession.controllerId
|
|
878
|
+
|| record.principalId !== releasedPrincipal
|
|
879
|
+
|| record.controllerInstanceId !== releasedInstanceId
|
|
880
|
+
) {
|
|
881
|
+
throw new Error(`CONTROLLER_RELAY_RELEASE_FENCE_MISMATCH: ${input.workId}`);
|
|
882
|
+
}
|
|
883
|
+
if (getControllerSession(options, input.workId)) {
|
|
884
|
+
throw new Error(`CONTROLLER_RELAY_ABANDONED_RELEASE_ACTIVE_CLAIM: ${input.workId}`);
|
|
885
|
+
}
|
|
886
|
+
const work = getWorkContract(options, input.workId);
|
|
887
|
+
// Completed Work still requires semantic goal_complete or a valid terminal
|
|
888
|
+
// successor. Failed/cancelled Work has no legal semantic continuation, so
|
|
889
|
+
// its exact released controller epoch may mechanically abandon the claimed
|
|
890
|
+
// round and free the relay scope for later recovery/replanning.
|
|
891
|
+
if (!work || work.status === 'completed') return undefined;
|
|
892
|
+
|
|
893
|
+
return applyControllerRoundTransition(options, current, {
|
|
894
|
+
type: 'abandoned_release_observed', at: nowIso(options), error: CONTROLLER_RELAY_ABANDONED_RELEASE_ERROR,
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Mechanically retire any surviving ControllerRound once Work lifecycle authority
|
|
901
|
+
* is durably failed/cancelled and no Controller lease remains. This is cleanup,
|
|
902
|
+
* not a semantic disposition: completed Work is intentionally excluded.
|
|
903
|
+
*/
|
|
904
|
+
export function reconcileControllerRoundAfterTerminalWork(
|
|
905
|
+
options: ControllerRoundRelayStoreOptions,
|
|
906
|
+
input: { workId: string; actor?: string },
|
|
907
|
+
): ControllerRoundRelayRecord | undefined {
|
|
908
|
+
const initial = readRelayRecord(options, input.workId);
|
|
909
|
+
if (!initial) return undefined;
|
|
910
|
+
const work = getWorkContract(options, input.workId);
|
|
911
|
+
if (!work || !['failed', 'cancelled'].includes(work.status)) return undefined;
|
|
912
|
+
if (getControllerSession(options, input.workId)) {
|
|
913
|
+
throw new Error(`CONTROLLER_RELAY_TERMINAL_WORK_ACTIVE_CLAIM: ${input.workId}`);
|
|
914
|
+
}
|
|
915
|
+
if (initial.value.status === 'failed') return initial.value;
|
|
916
|
+
return relayLock(options, initial.value.relayScopeId, input.actor ?? `controller-relay-terminal-work:${input.workId}`, () => {
|
|
917
|
+
const current = readRelayRecord(options, input.workId);
|
|
918
|
+
if (!current) return undefined;
|
|
919
|
+
const currentWork = getWorkContract(options, input.workId);
|
|
920
|
+
if (!currentWork || !['failed', 'cancelled'].includes(currentWork.status)) return undefined;
|
|
921
|
+
if (getControllerSession(options, input.workId)) {
|
|
922
|
+
throw new Error(`CONTROLLER_RELAY_TERMINAL_WORK_ACTIVE_CLAIM: ${input.workId}`);
|
|
923
|
+
}
|
|
924
|
+
if (current.value.status === 'failed') return current.value;
|
|
925
|
+
return applyControllerRoundTransition(options, current, {
|
|
926
|
+
type: 'terminal_work_observed', at: nowIso(options), error: `CONTROLLER_RELAY_TERMINAL_WORK_RETIRED:${input.workId}`,
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export function finishControllerRoundRelayDispatch(
|
|
932
|
+
options: ControllerRoundRelayStoreOptions,
|
|
933
|
+
input: { workId: string; ok: boolean; bindingId?: string; providerDispatchEffectId?: string; providerDispatchReceiptId?: string; error?: string; recovery?: boolean; outcomeUnknown?: boolean; waitForUser?: boolean; handoffId?: string; nowMs?: number },
|
|
934
|
+
): ControllerRoundRelayRecord | undefined {
|
|
935
|
+
const initial = readRelayRecord(options, input.workId);
|
|
936
|
+
if (!initial || initial.value.status !== 'dispatching') return initial?.value;
|
|
937
|
+
return relayLock(options, initial.value.relayScopeId, `controller-relay-finish:${input.workId}`, () => {
|
|
938
|
+
const current = readRelayRecord(options, input.workId);
|
|
939
|
+
if (!current || current.value.status !== 'dispatching') return current?.value;
|
|
940
|
+
const at = typeof input.nowMs === 'number' ? new Date(input.nowMs).toISOString() : nowIso(options);
|
|
941
|
+
const handoffId = bounded(input.handoffId, 200);
|
|
942
|
+
if (input.waitForUser) {
|
|
943
|
+
if (!handoffId) throw new Error('CONTROLLER_RELAY_WAIT_FOR_USER_HANDOFF_REQUIRED');
|
|
944
|
+
const handoff = getHandoffItem(options, handoffId);
|
|
945
|
+
if (!handoff) throw new Error(`HANDOFF_NOT_FOUND: ${handoffId}`);
|
|
946
|
+
if (isTerminalHandoffStatus(handoff.status)) throw new Error(`CONTROLLER_RELAY_HANDOFF_TERMINAL: ${handoff.status}`);
|
|
947
|
+
if (handoff.workId && handoff.workId !== input.workId) throw new Error(`CONTROLLER_RELAY_HANDOFF_WORK_MISMATCH: ${handoffId}`);
|
|
948
|
+
}
|
|
949
|
+
const error = bounded(input.error, 2_000) ?? (input.outcomeUnknown
|
|
950
|
+
? 'CONTROLLER_RELAY_PROVIDER_DISPATCH_OUTCOME_UNKNOWN'
|
|
951
|
+
: input.waitForUser
|
|
952
|
+
? 'CONTROLLER_RELAY_WAIT_FOR_USER'
|
|
953
|
+
: input.recovery
|
|
954
|
+
? 'CONTROLLER_RELAY_RECOVERY_FAILED'
|
|
955
|
+
: 'CONTROLLER_RELAY_DISPATCH_FAILED');
|
|
956
|
+
const providerDispatchEffectId = bounded(input.providerDispatchEffectId, 500)
|
|
957
|
+
?? (current.value.authorityId ? controllerRoundProviderEffectId(current.value.relayScopeId, current.value.authorityId) : undefined);
|
|
958
|
+
let event: ControllerRoundTransitionEvent;
|
|
959
|
+
if (input.ok) {
|
|
960
|
+
if (!providerDispatchEffectId) throw new Error('CONTROLLER_RELAY_PROVIDER_EFFECT_ID_REQUIRED');
|
|
961
|
+
event = { type: 'provider_dispatch_succeeded', at, providerDispatchEffectId, ...(bounded(input.bindingId, 500) ? { bindingId: bounded(input.bindingId, 500) } : {}), ...(bounded(input.providerDispatchReceiptId, 500) ? { providerDispatchReceiptId: bounded(input.providerDispatchReceiptId, 500) } : {}) };
|
|
962
|
+
} else if (input.outcomeUnknown) {
|
|
963
|
+
const effectIdentity = providerDispatchEffectId;
|
|
964
|
+
if (!effectIdentity) throw new Error('CONTROLLER_RELAY_PROVIDER_EFFECT_ID_REQUIRED');
|
|
965
|
+
event = { type: 'provider_dispatch_outcome_unknown', at, error, providerDispatchEffectId: effectIdentity };
|
|
966
|
+
} else if (input.waitForUser) {
|
|
967
|
+
event = { type: 'provider_user_action_required', at, error, handoffId: handoffId! };
|
|
968
|
+
} else {
|
|
969
|
+
const nextFailureCount = current.value.consecutiveFailures + 1;
|
|
970
|
+
const recoveryDelayMs = Math.min(MAX_STALLED_RECOVERY_BACKOFF_MS, DEFAULT_STALLED_RECOVERY_BACKOFF_MS * 2 ** Math.max(0, nextFailureCount - 1));
|
|
971
|
+
event = { type: 'provider_dispatch_failed', at, error, recovery: input.recovery === true, ...(input.recovery ? { nextRecoveryAt: new Date(Date.parse(at) + recoveryDelayMs).toISOString() } : {}) };
|
|
972
|
+
}
|
|
973
|
+
return applyControllerRoundTransition(options, current, event);
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export function acknowledgeControllerRoundClaim(
|
|
978
|
+
options: ControllerRoundRelayStoreOptions,
|
|
979
|
+
input: { workId: string; session: ControllerSession; assistantContextSnapshot?: AssistantContextSnapshot | null },
|
|
980
|
+
): ControllerRoundRelayRecord | undefined {
|
|
981
|
+
const initial = readRelayRecord(options, input.workId);
|
|
982
|
+
if (!initial) return undefined;
|
|
983
|
+
const expectedControllerType = relayControllerType(initial.value);
|
|
984
|
+
if (input.session.controllerType !== expectedControllerType) throw new Error(`CONTROLLER_RELAY_CONTROLLER_TYPE_MISMATCH: ${input.workId}`);
|
|
985
|
+
|
|
986
|
+
return relayLock(options, initial.value.relayScopeId, `controller-relay-claim:${input.session.controllerId}`, () => {
|
|
987
|
+
const current = readRelayRecord(options, input.workId);
|
|
988
|
+
if (!current) return undefined;
|
|
989
|
+
if (current.value.status === 'claimed'
|
|
990
|
+
&& current.value.controllerId === input.session.controllerId
|
|
991
|
+
&& current.value.sessionId === input.session.sessionId
|
|
992
|
+
&& current.value.claimGeneration === input.session.claimGeneration
|
|
993
|
+
&& sameAssistantContextSnapshot(current.value.assistantContextSnapshot, input.assistantContextSnapshot)) return current.value;
|
|
994
|
+
|
|
995
|
+
const blocker = controllerRoundBlockerClass(current.value);
|
|
996
|
+
const policyClaimable = ['dispatching', 'dispatched', 'claimed'].includes(current.value.status)
|
|
997
|
+
|| blocker === 'provider_dispatch_outcome_unknown'
|
|
998
|
+
|| blocker === 'repeated_state';
|
|
999
|
+
if (!policyClaimable) return current.value;
|
|
1000
|
+
|
|
1001
|
+
const owner = getControllerSession(options, input.workId);
|
|
1002
|
+
const ownerPrincipal = owner?.principalId?.trim() || owner?.controllerId;
|
|
1003
|
+
const sessionPrincipal = input.session.principalId?.trim() || input.session.controllerId;
|
|
1004
|
+
if (
|
|
1005
|
+
!owner
|
|
1006
|
+
|| owner.controllerType !== relayControllerType(current.value)
|
|
1007
|
+
|| owner.controllerId !== input.session.controllerId
|
|
1008
|
+
|| owner.sessionId !== input.session.sessionId
|
|
1009
|
+
|| owner.claimGeneration !== input.session.claimGeneration
|
|
1010
|
+
|| ownerPrincipal !== sessionPrincipal
|
|
1011
|
+
|| (owner.controllerInstanceId?.trim() || '') !== (input.session.controllerInstanceId?.trim() || '')
|
|
1012
|
+
) throw new Error(`CONTROLLER_RELAY_CLAIM_IDENTITY_MISMATCH: ${input.workId}`);
|
|
1013
|
+
// The dispatching relay records the provider/launcher identity until the
|
|
1014
|
+
// authenticated controller actually claims the round. The domain transition
|
|
1015
|
+
// policy owns that one-way identity handoff; duplicating an equality guard
|
|
1016
|
+
// here would turn provider dispatch metadata into a second claim authority.
|
|
1017
|
+
if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
|
|
1018
|
+
throw new Error(`CONTROLLER_RELAY_CLAIM_GENERATION_REQUIRED: ${input.workId}`);
|
|
1019
|
+
}
|
|
1020
|
+
const controllerInstanceId = owner.controllerInstanceId?.trim();
|
|
1021
|
+
if (!controllerInstanceId) throw new Error(`CONTROLLER_RELAY_CLAIM_INSTANCE_REQUIRED: ${input.workId}`);
|
|
1022
|
+
const at = nowIso(options);
|
|
1023
|
+
const session = owner as ControllerSession & { claimGeneration: number };
|
|
1024
|
+
|
|
1025
|
+
if (blocker === 'repeated_state') {
|
|
1026
|
+
const work = getWorkContract(options, input.workId);
|
|
1027
|
+
if (!work || isTerminalWorkContractStatus(work.status)) return current.value;
|
|
1028
|
+
const stateFingerprint = mechanicalStateFingerprint(options, work, current.value.requirementId, current.value.relayScopeId);
|
|
1029
|
+
const transitioned = applyControllerRoundTransition(options, current, {
|
|
1030
|
+
type: 'semantic_state_changed', at, stateFingerprint, session, principalId: ownerPrincipal!, controllerInstanceId,
|
|
1031
|
+
});
|
|
1032
|
+
return persistClaimedAssistantContextSnapshot(options, transitioned, input.assistantContextSnapshot, at);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const transitioned = applyControllerRoundTransition(options, current, {
|
|
1036
|
+
type: 'controller_claim_observed', at, session, principalId: ownerPrincipal!, controllerInstanceId,
|
|
1037
|
+
});
|
|
1038
|
+
return persistClaimedAssistantContextSnapshot(options, transitioned, input.assistantContextSnapshot, at);
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Explicitly rekey one exact relay-bound Work after the caller lost the opaque
|
|
1044
|
+
* per-round capability with its MCP/ChatGPT transport. This is not a semantic
|
|
1045
|
+
* continuation and never dispatches a provider: it preserves the current
|
|
1046
|
+
* relay lineage and budgets, rotates only the capability epoch, and leaves the
|
|
1047
|
+
* standard controller_claim path responsible for mechanically acknowledging
|
|
1048
|
+
* the recovered round.
|
|
1049
|
+
*/
|
|
1050
|
+
export function recoverControllerRoundRelayAuthority(
|
|
1051
|
+
options: ControllerRoundRelayStoreOptions,
|
|
1052
|
+
input: RecoverControllerRoundRelayAuthorityInput,
|
|
1053
|
+
): ControllerRoundRelayRecord {
|
|
1054
|
+
const workId = input.workId.trim();
|
|
1055
|
+
if (!workId) throw new Error('WORK_CONTROLLER_AUTHORITY_RECOVERY_WORK_REQUIRED');
|
|
1056
|
+
if (input.requestedBy !== 'user') {
|
|
1057
|
+
throw new Error('WORK_CONTROLLER_AUTHORITY_RECOVERY_USER_REQUIRED: relay authority rekey is an explicit user-directed recovery only.');
|
|
1058
|
+
}
|
|
1059
|
+
const work = getWorkContract(options, workId);
|
|
1060
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
1061
|
+
if (isTerminalWorkContractStatus(work.status)) {
|
|
1062
|
+
throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_TERMINAL: ${workId}:${work.status}`);
|
|
1063
|
+
}
|
|
1064
|
+
const initial = readRelayRecord(options, workId);
|
|
1065
|
+
if (!initial) throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_RELAY_REQUIRED: ${workId}`);
|
|
1066
|
+
const expectedType = relayControllerType(initial.value);
|
|
1067
|
+
if (expectedType !== input.identity.controllerType) {
|
|
1068
|
+
throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_CONTROLLER_TYPE_MISMATCH: ${workId}`);
|
|
1069
|
+
}
|
|
1070
|
+
if (initial.value.controllerId !== input.identity.controllerId || initial.value.principalId !== input.identity.principalId) {
|
|
1071
|
+
throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_PRINCIPAL_MISMATCH: ${workId}`);
|
|
1072
|
+
}
|
|
1073
|
+
const recoverableStatuses: readonly ControllerRoundRelayStatus[] = ['pending_release', 'dispatching', 'dispatched', 'claimed', 'failed'];
|
|
1074
|
+
if (!recoverableStatuses.includes(initial.value.status)) {
|
|
1075
|
+
throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_RELAY_STATE_INVALID: ${workId}:${initial.value.status}`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return relayLock(options, initial.value.relayScopeId, `controller-relay-explicit-recover:${workId}`, () => {
|
|
1079
|
+
const current = readRelayRecord(options, workId);
|
|
1080
|
+
if (!current) throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_RELAY_REQUIRED: ${workId}`);
|
|
1081
|
+
const latest = relayHistory(options, current.value.relayScopeId)[0];
|
|
1082
|
+
if (!latest || latest.originWorkId !== workId || latest.updatedAt !== current.value.updatedAt) {
|
|
1083
|
+
throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_RELAY_NOT_CURRENT: ${workId}`);
|
|
1084
|
+
}
|
|
1085
|
+
if (!recoverableStatuses.includes(current.value.status)) {
|
|
1086
|
+
throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_RELAY_STATE_INVALID: ${workId}:${current.value.status}`);
|
|
1087
|
+
}
|
|
1088
|
+
if (relayControllerType(current.value) !== input.identity.controllerType
|
|
1089
|
+
|| current.value.controllerId !== input.identity.controllerId
|
|
1090
|
+
|| current.value.principalId !== input.identity.principalId) {
|
|
1091
|
+
throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_PRINCIPAL_MISMATCH: ${workId}`);
|
|
1092
|
+
}
|
|
1093
|
+
const currentWork = getWorkContract(options, workId);
|
|
1094
|
+
if (!currentWork) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
1095
|
+
if (isTerminalWorkContractStatus(currentWork.status)) {
|
|
1096
|
+
throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_TERMINAL: ${workId}:${currentWork.status}`);
|
|
1097
|
+
}
|
|
1098
|
+
const activeWorks = relevantWork(options, current.value).filter((entry) => !isTerminalWorkContractStatus(entry.status));
|
|
1099
|
+
if (activeWorks.some((entry) => workHasActiveExecution(options.controllerHome, options.repoId, entry.workId))) {
|
|
1100
|
+
throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_ACTIVE_EXECUTION: ${workId}`);
|
|
1101
|
+
}
|
|
1102
|
+
if (activeWorks.some((entry) => Boolean(getControllerSession(options, entry.workId)))) {
|
|
1103
|
+
throw new Error(`WORK_CONTROLLER_AUTHORITY_RECOVERY_ACTIVE_CLAIM: ${workId}`);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return applyControllerRoundTransition(options, current, {
|
|
1107
|
+
type: 'authority_recovery_requested', at: nowIso(options), proposedAuthorityId: newControllerRoundAuthorityId(),
|
|
1108
|
+
keepsConfirmedDispatch: current.value.status === 'dispatched',
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
export interface RearmControllerRoundAfterProviderRecoveryInput {
|
|
1114
|
+
workId: string;
|
|
1115
|
+
relayScopeId: string;
|
|
1116
|
+
authorityId: string;
|
|
1117
|
+
expectedUpdatedAt: string;
|
|
1118
|
+
evidenceId: string;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export interface BindLegacyControllerRoundOccurrenceInput {
|
|
1122
|
+
workId: string;
|
|
1123
|
+
relayScopeId: string;
|
|
1124
|
+
occurrenceId: string;
|
|
1125
|
+
authorityId: string;
|
|
1126
|
+
expectedUpdatedAt: string;
|
|
1127
|
+
identity: Pick<ControllerRoundRelayIdentity, 'controllerId' | 'controllerType' | 'principalId'>;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/** Exact evidence-gated provider/environment recovery for one exhausted same-round dispatch responsibility. */
|
|
1131
|
+
export function rearmControllerRoundAfterProviderRecovery(
|
|
1132
|
+
options: ControllerRoundRelayStoreOptions,
|
|
1133
|
+
input: RearmControllerRoundAfterProviderRecoveryInput,
|
|
1134
|
+
): ControllerRoundRelayRecord {
|
|
1135
|
+
const workId = input.workId.trim();
|
|
1136
|
+
const initial = readRelayRecord(options, workId);
|
|
1137
|
+
if (!initial) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_RELAY_REQUIRED: ${workId}`);
|
|
1138
|
+
if (initial.value.relayScopeId !== input.relayScopeId.trim()) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_SCOPE_MISMATCH: ${workId}`);
|
|
1139
|
+
return relayLock(options, initial.value.relayScopeId, `controller-relay-provider-recovered:${workId}`, () => {
|
|
1140
|
+
const current = readRelayRecord(options, workId);
|
|
1141
|
+
if (!current) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_RELAY_REQUIRED: ${workId}`);
|
|
1142
|
+
if (current.value.updatedAt !== input.expectedUpdatedAt.trim()) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_STALE: ${workId}`);
|
|
1143
|
+
if (current.value.relayScopeId !== input.relayScopeId.trim()) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_SCOPE_MISMATCH: ${workId}`);
|
|
1144
|
+
if ((current.value.authorityId?.trim() || '') !== input.authorityId.trim()) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_AUTHORITY_MISMATCH: ${workId}`);
|
|
1145
|
+
if (controllerRoundBlockerClass(current.value) !== 'consecutive_failures') throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_BLOCKER_MISMATCH: ${workId}`);
|
|
1146
|
+
const work = getWorkContract(options, workId);
|
|
1147
|
+
if (!work || isTerminalWorkContractStatus(work.status)) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_WORK_TERMINAL: ${workId}:${work?.status ?? 'missing'}`);
|
|
1148
|
+
const requirement = requirementForRelay(options, current.value.requirementId);
|
|
1149
|
+
if (requirement && !['planned', 'active'].includes(requirement.state)) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_REQUIREMENT_TERMINAL: ${requirement.state}`);
|
|
1150
|
+
const activeWorks = relevantWork(options, current.value).filter((entry) => !isTerminalWorkContractStatus(entry.status));
|
|
1151
|
+
if (activeWorks.some((entry) => workHasActiveExecution(options.controllerHome, options.repoId, entry.workId))) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_ACTIVE_EXECUTION: ${workId}`);
|
|
1152
|
+
if (activeWorks.some((entry) => Boolean(getControllerSession(options, entry.workId)))) throw new Error(`CONTROLLER_RELAY_PROVIDER_RECOVERY_ACTIVE_CLAIM: ${workId}`);
|
|
1153
|
+
const evidenceId = bounded(input.evidenceId, 500);
|
|
1154
|
+
if (!evidenceId) throw new Error('CONTROLLER_RELAY_PROVIDER_RECOVERY_EVIDENCE_REQUIRED');
|
|
1155
|
+
return applyControllerRoundTransition(options, current, { type: 'provider_environment_recovered', at: nowIso(options), evidenceId });
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* One-time migration of a pre-occurrence-era dispatch responsibility onto the
|
|
1161
|
+
* first explicit post-recovery Scheduler occurrence. This does not create a
|
|
1162
|
+
* round or provider effect: exact round authority, CAS and controller lineage
|
|
1163
|
+
* fence the single occurrenceId write before normal strict occurrence matching
|
|
1164
|
+
* resumes.
|
|
1165
|
+
*/
|
|
1166
|
+
export function bindLegacyControllerRoundOccurrence(
|
|
1167
|
+
options: ControllerRoundRelayStoreOptions,
|
|
1168
|
+
input: BindLegacyControllerRoundOccurrenceInput,
|
|
1169
|
+
): ControllerRoundRelayRecord {
|
|
1170
|
+
const workId = input.workId.trim();
|
|
1171
|
+
const relayScopeId = input.relayScopeId.trim();
|
|
1172
|
+
const occurrenceId = bounded(input.occurrenceId, 500);
|
|
1173
|
+
if (!occurrenceId) throw new Error('CONTROLLER_RELAY_OCCURRENCE_ID_REQUIRED');
|
|
1174
|
+
const initial = readRelayRecord(options, workId);
|
|
1175
|
+
if (!initial) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_RELAY_REQUIRED: ${workId}`);
|
|
1176
|
+
if (initial.value.relayScopeId !== relayScopeId) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_SCOPE_MISMATCH: ${workId}`);
|
|
1177
|
+
return relayLock(options, initial.value.relayScopeId, `controller-relay-bind-legacy-occurrence:${workId}`, () => {
|
|
1178
|
+
const current = readRelayRecord(options, workId);
|
|
1179
|
+
if (!current) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_RELAY_REQUIRED: ${workId}`);
|
|
1180
|
+
if (current.value.updatedAt !== input.expectedUpdatedAt.trim()) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_STALE: ${workId}`);
|
|
1181
|
+
if (current.value.relayScopeId !== relayScopeId) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_SCOPE_MISMATCH: ${workId}`);
|
|
1182
|
+
if (current.value.originWorkId !== workId) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_WORK_MISMATCH: ${workId}`);
|
|
1183
|
+
if ((current.value.authorityId?.trim() || '') !== input.authorityId.trim()) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_AUTHORITY_MISMATCH: ${workId}`);
|
|
1184
|
+
const expectedPrincipalId = input.identity.principalId.trim() || input.identity.controllerId.trim();
|
|
1185
|
+
const currentPrincipalId = current.value.principalId?.trim() || current.value.controllerId;
|
|
1186
|
+
if (current.value.controllerId !== input.identity.controllerId.trim()
|
|
1187
|
+
|| relayControllerType(current.value) !== input.identity.controllerType
|
|
1188
|
+
|| currentPrincipalId !== expectedPrincipalId) {
|
|
1189
|
+
throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_CONTROLLER_MISMATCH: ${workId}`);
|
|
1190
|
+
}
|
|
1191
|
+
const work = getWorkContract(options, workId);
|
|
1192
|
+
if (!work || isTerminalWorkContractStatus(work.status)) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_WORK_TERMINAL: ${workId}:${work?.status ?? 'missing'}`);
|
|
1193
|
+
const requirement = requirementForRelay(options, current.value.requirementId);
|
|
1194
|
+
if (requirement && !['planned', 'active'].includes(requirement.state)) throw new Error(`CONTROLLER_RELAY_LEGACY_OCCURRENCE_REQUIREMENT_TERMINAL: ${requirement.state}`);
|
|
1195
|
+
return applyControllerRoundTransition(options, current, { type: 'legacy_occurrence_bound', at: nowIso(options), occurrenceId });
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
export function claimStalledControllerRoundRelays(
|
|
1200
|
+
options: ControllerRoundRelayStoreOptions,
|
|
1201
|
+
input: { nowMs?: number; graceMs?: number; limit?: number; controllerTypes?: readonly ControllerType[] } = {},
|
|
1202
|
+
): ControllerRoundRelayRecord[] {
|
|
1203
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
1204
|
+
const graceMs = Math.max(60_000, Math.min(input.graceMs ?? DEFAULT_UNCLOSED_ROUND_GRACE_MS, MAX_UNCLOSED_ROUND_GRACE_MS));
|
|
1205
|
+
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 2), 16));
|
|
1206
|
+
const claimed: ControllerRoundRelayRecord[] = [];
|
|
1207
|
+
|
|
1208
|
+
for (const candidate of latestRelayRecordsByScope(options)) {
|
|
1209
|
+
if (claimed.length >= limit) break;
|
|
1210
|
+
const repeatedStateBlocked = candidate.status === 'blocked' && candidate.blockedReason?.startsWith('repeated_state:') === true;
|
|
1211
|
+
if (!['pending_release', 'dispatching', 'dispatched', 'claimed'].includes(candidate.status) && !repeatedStateBlocked) continue;
|
|
1212
|
+
if (input.controllerTypes && !input.controllerTypes.includes(relayControllerType(candidate))) continue;
|
|
1213
|
+
const scheduledRecoveryAtMs = candidate.nextRecoveryAt ? Date.parse(candidate.nextRecoveryAt) : Number.NaN;
|
|
1214
|
+
if (Number.isFinite(scheduledRecoveryAtMs)) {
|
|
1215
|
+
if (nowMs < scheduledRecoveryAtMs) continue;
|
|
1216
|
+
} else {
|
|
1217
|
+
const roundOpenedAtMs = Date.parse(candidate.claimedAt ?? candidate.dispatchedAt ?? candidate.updatedAt);
|
|
1218
|
+
if (!Number.isFinite(roundOpenedAtMs) || nowMs - roundOpenedAtMs < graceMs) continue;
|
|
1219
|
+
}
|
|
1220
|
+
const requirement = requirementForRelay(options, candidate.requirementId);
|
|
1221
|
+
if (requirement && !['planned', 'active'].includes(requirement.state)) continue;
|
|
1222
|
+
const candidateWorks = relevantWork(options, candidate);
|
|
1223
|
+
const activeCandidateWorks = candidateWorks.filter((work) => !isTerminalWorkContractStatus(work.status));
|
|
1224
|
+
if (activeCandidateWorks.length === 0) continue;
|
|
1225
|
+
if (activeCandidateWorks.some((work) => workHasActiveExecution(options.controllerHome, options.repoId, work.workId) || controllerSessionBlocksRecovery(options, work.workId, { nowMs, graceMs }))) continue;
|
|
1226
|
+
if (repeatedStateBlocked) {
|
|
1227
|
+
const fingerprintWork = activeCandidateWorks[0] ?? getWorkContract(options, candidate.originWorkId);
|
|
1228
|
+
const currentFingerprint = fingerprintWork
|
|
1229
|
+
? mechanicalStateFingerprint(options, fingerprintWork, candidate.requirementId, candidate.relayScopeId)
|
|
1230
|
+
: candidate.stateFingerprint;
|
|
1231
|
+
if (currentFingerprint === candidate.stateFingerprint) continue;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const next = relayLock(options, candidate.relayScopeId, `controller-relay-recover:${candidate.originWorkId}`, () => {
|
|
1235
|
+
const latest = relayHistory(options, candidate.relayScopeId)[0];
|
|
1236
|
+
if (!latest || latest.originWorkId !== candidate.originWorkId || latest.updatedAt !== candidate.updatedAt || latest.status !== candidate.status) return undefined;
|
|
1237
|
+
const latestRepeatedStateBlocked = latest.status === 'blocked' && latest.blockedReason?.startsWith('repeated_state:') === true;
|
|
1238
|
+
if (latest.status === 'blocked' && !latestRepeatedStateBlocked) return undefined;
|
|
1239
|
+
if (input.controllerTypes && !input.controllerTypes.includes(relayControllerType(latest))) return undefined;
|
|
1240
|
+
const latestRecoveryAtMs = latest.nextRecoveryAt ? Date.parse(latest.nextRecoveryAt) : Number.NaN;
|
|
1241
|
+
if (Number.isFinite(latestRecoveryAtMs)) {
|
|
1242
|
+
if (nowMs < latestRecoveryAtMs) return undefined;
|
|
1243
|
+
} else {
|
|
1244
|
+
const latestRoundOpenedAtMs = Date.parse(latest.claimedAt ?? latest.dispatchedAt ?? latest.updatedAt);
|
|
1245
|
+
if (!Number.isFinite(latestRoundOpenedAtMs) || nowMs - latestRoundOpenedAtMs < graceMs) return undefined;
|
|
1246
|
+
}
|
|
1247
|
+
const latestRequirement = requirementForRelay(options, latest.requirementId);
|
|
1248
|
+
if (latestRequirement && !['planned', 'active'].includes(latestRequirement.state)) return undefined;
|
|
1249
|
+
const works = relevantWork(options, latest);
|
|
1250
|
+
const activeWorks = works.filter((work) => !isTerminalWorkContractStatus(work.status));
|
|
1251
|
+
if (activeWorks.length === 0) return undefined;
|
|
1252
|
+
if (activeWorks.some((work) => workHasActiveExecution(options.controllerHome, options.repoId, work.workId) || controllerSessionBlocksRecovery(options, work.workId, { nowMs, graceMs }))) return undefined;
|
|
1253
|
+
|
|
1254
|
+
const currentRecord = readRelayRecord(options, latest.originWorkId);
|
|
1255
|
+
if (!currentRecord || currentRecord.value.updatedAt !== latest.updatedAt || currentRecord.value.status !== latest.status) return undefined;
|
|
1256
|
+
const fingerprintWork = activeWorks[0] ?? getWorkContract(options, latest.originWorkId);
|
|
1257
|
+
const stateFingerprint = fingerprintWork
|
|
1258
|
+
? mechanicalStateFingerprint(options, fingerprintWork, latest.requirementId, latest.relayScopeId)
|
|
1259
|
+
: latest.stateFingerprint;
|
|
1260
|
+
const at = new Date(nowMs).toISOString();
|
|
1261
|
+
const lastError = latestRepeatedStateBlocked
|
|
1262
|
+
? undefined
|
|
1263
|
+
: latest.nextRecoveryAt
|
|
1264
|
+
? latest.lastError
|
|
1265
|
+
: latest.status === 'pending_release'
|
|
1266
|
+
? 'CONTROLLER_RELAY_RELEASE_TRANSITION_INCOMPLETE'
|
|
1267
|
+
: latest.status === 'dispatching'
|
|
1268
|
+
? 'CONTROLLER_RELAY_DISPATCH_TRANSITION_INCOMPLETE'
|
|
1269
|
+
: latest.status === 'claimed' ? 'CONTROLLER_RELAY_CLAIMED_ROUND_UNCLOSED' : 'CONTROLLER_RELAY_ROUND_UNCLOSED';
|
|
1270
|
+
const recovered = applyControllerRoundTransition(options, currentRecord, {
|
|
1271
|
+
type: 'stalled_round_observed', at, stateFingerprint, proposedAuthorityId: newControllerRoundAuthorityId(),
|
|
1272
|
+
...(lastError ? { lastError } : {}),
|
|
1273
|
+
});
|
|
1274
|
+
return recovered.status === 'dispatching' ? recovered : undefined;
|
|
1275
|
+
});
|
|
1276
|
+
if (next) claimed.push(next);
|
|
1277
|
+
}
|
|
1278
|
+
return claimed;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const CONTROLLER_ROUND_ORIGIN_OBJECTIVE_MAX_CHARS = 4_000;
|
|
1282
|
+
const CONTROLLER_ROUND_RELATED_OBJECTIVE_MAX_CHARS = 500;
|
|
1283
|
+
const CONTROLLER_ROUND_ORIGIN_ACCEPTANCE_MAX_ITEMS = 16;
|
|
1284
|
+
const CONTROLLER_ROUND_ACCEPTANCE_CRITERION_MAX_CHARS = 1_000;
|
|
1285
|
+
|
|
1286
|
+
export interface ControllerRoundContextSnapshot {
|
|
1287
|
+
repoId: string;
|
|
1288
|
+
relayScopeId: string;
|
|
1289
|
+
originWorkId: string;
|
|
1290
|
+
requirement?: {
|
|
1291
|
+
requirementId: string;
|
|
1292
|
+
state: string;
|
|
1293
|
+
outcomeStatement: string;
|
|
1294
|
+
};
|
|
1295
|
+
works: Array<{
|
|
1296
|
+
workId: string;
|
|
1297
|
+
status: string;
|
|
1298
|
+
phase: string;
|
|
1299
|
+
updatedAt: string;
|
|
1300
|
+
objective: string;
|
|
1301
|
+
acceptanceCriteria: string[];
|
|
1302
|
+
}>;
|
|
1303
|
+
handoffs: Array<{
|
|
1304
|
+
id: string;
|
|
1305
|
+
status: string;
|
|
1306
|
+
workId?: string;
|
|
1307
|
+
title: string;
|
|
1308
|
+
reason: string;
|
|
1309
|
+
}>;
|
|
1310
|
+
round: {
|
|
1311
|
+
count: number;
|
|
1312
|
+
maxRounds: number;
|
|
1313
|
+
repeatedStateCount: number;
|
|
1314
|
+
maxRepeatedState: number;
|
|
1315
|
+
consecutiveFailures: number;
|
|
1316
|
+
maxFailures: number;
|
|
1317
|
+
};
|
|
1318
|
+
recoveryReason?: string;
|
|
1319
|
+
assistantContext?: string;
|
|
1320
|
+
executionQualitySignals?: ExecutionQualitySignal[];
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/** Provider-neutral launch context. ControllerHost adapters decide how to render it. */
|
|
1324
|
+
export function readControllerRoundContextSnapshot(
|
|
1325
|
+
options: ControllerRoundRelayStoreOptions,
|
|
1326
|
+
record: ControllerRoundRelayRecord,
|
|
1327
|
+
): ControllerRoundContextSnapshot {
|
|
1328
|
+
const requirement = requirementForRelay(options, record.requirementId);
|
|
1329
|
+
const relevantWorks = relevantWork(options, record);
|
|
1330
|
+
const works = relevantWorks.slice(0, 8);
|
|
1331
|
+
const workIds = new Set(relevantWorks.map((work) => work.workId));
|
|
1332
|
+
const handoffs = listHandoffItems({ controllerHome: options.controllerHome, repoId: options.repoId, status: 'active', limit: 100 })
|
|
1333
|
+
.filter((handoff) => !handoff.workId || workIds.has(handoff.workId))
|
|
1334
|
+
.slice(0, 8);
|
|
1335
|
+
return {
|
|
1336
|
+
repoId: record.repoId,
|
|
1337
|
+
relayScopeId: record.relayScopeId,
|
|
1338
|
+
originWorkId: record.originWorkId,
|
|
1339
|
+
assistantContext: options.prepareAssistantContext?.(record.originWorkId),
|
|
1340
|
+
executionQualitySignals: pendingQualitySignals(record),
|
|
1341
|
+
...(requirement ? {
|
|
1342
|
+
requirement: {
|
|
1343
|
+
requirementId: requirement.requirementId,
|
|
1344
|
+
state: requirement.state,
|
|
1345
|
+
outcomeStatement: requirement.outcomeStatement.slice(0, 800),
|
|
1346
|
+
},
|
|
1347
|
+
} : {}),
|
|
1348
|
+
works: works.map((work) => {
|
|
1349
|
+
const origin = work.workId === record.originWorkId;
|
|
1350
|
+
return {
|
|
1351
|
+
workId: work.workId,
|
|
1352
|
+
status: work.status,
|
|
1353
|
+
phase: work.phase,
|
|
1354
|
+
updatedAt: work.updatedAt,
|
|
1355
|
+
objective: work.objective.slice(0, origin
|
|
1356
|
+
? CONTROLLER_ROUND_ORIGIN_OBJECTIVE_MAX_CHARS
|
|
1357
|
+
: CONTROLLER_ROUND_RELATED_OBJECTIVE_MAX_CHARS),
|
|
1358
|
+
acceptanceCriteria: origin
|
|
1359
|
+
? work.acceptanceCriteria
|
|
1360
|
+
.slice(0, CONTROLLER_ROUND_ORIGIN_ACCEPTANCE_MAX_ITEMS)
|
|
1361
|
+
.map((criterion) => criterion.slice(0, CONTROLLER_ROUND_ACCEPTANCE_CRITERION_MAX_CHARS))
|
|
1362
|
+
: [],
|
|
1363
|
+
};
|
|
1364
|
+
}),
|
|
1365
|
+
handoffs: handoffs.map((handoff) => ({
|
|
1366
|
+
id: handoff.id,
|
|
1367
|
+
status: handoff.status,
|
|
1368
|
+
...(handoff.workId ? { workId: handoff.workId } : {}),
|
|
1369
|
+
title: handoff.title,
|
|
1370
|
+
reason: handoff.reason.slice(0, 300),
|
|
1371
|
+
})),
|
|
1372
|
+
round: {
|
|
1373
|
+
count: record.roundCount,
|
|
1374
|
+
maxRounds: record.maxRounds,
|
|
1375
|
+
repeatedStateCount: record.repeatedStateCount,
|
|
1376
|
+
maxRepeatedState: record.maxRepeatedState,
|
|
1377
|
+
consecutiveFailures: record.consecutiveFailures,
|
|
1378
|
+
maxFailures: record.maxFailures,
|
|
1379
|
+
},
|
|
1380
|
+
...(['CONTROLLER_RELAY_ROUND_UNCLOSED', 'CONTROLLER_RELAY_CLAIMED_ROUND_UNCLOSED'].includes(record.lastError ?? '')
|
|
1381
|
+
? { recoveryReason: record.lastError }
|
|
1382
|
+
: {}),
|
|
1383
|
+
};
|
|
1384
|
+
}
|