@moretea-labs/forge 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +73 -7
- package/CHANGELOG.md +34 -0
- package/CLAUDE.md +17 -2
- package/CONTRIBUTING.md +3 -2
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-host.ts +7 -1
- package/adapters/chatgpt/browser-delivery-runtime.ts +531 -56
- package/adapters/chatgpt/controller-binding-store.ts +101 -22
- package/adapters/chatgpt/controller-round-host.ts +15 -25
- package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
- package/adapters/chatgpt/provider-delivery.ts +229 -2
- package/adapters/chatgpt/work-conversation-binding-store.ts +99 -54
- package/adapters/computer/chatgpt-extension-target.ts +708 -0
- package/adapters/computer/chrome-extension/background.js +541 -0
- package/adapters/computer/chrome-extension/content.js +402 -0
- package/adapters/computer/chrome-extension/core.js +24 -0
- package/adapters/computer/chrome-extension/manifest.json +32 -0
- package/adapters/computer/desktop-operator-discovery.ts +11 -31
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +178 -24
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -7
- package/adapters/mcp/instructions.ts +6 -5
- package/adapters/mcp/multi-repository.ts +10 -47
- package/adapters/mcp/oauth.ts +73 -0
- package/adapters/mcp/paths.ts +4 -1
- package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
- package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
- package/adapters/mcp/runtime-gateway/context-adapter.ts +490 -113
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +83 -0
- package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
- package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
- package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
- package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
- package/adapters/mcp/runtime-gateway/execution-tools.ts +14 -54
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +132 -47
- package/adapters/mcp/runtime-gateway/process-tools.ts +173 -23
- package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
- package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
- package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
- package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
- package/adapters/mcp/runtime-gateway/router.ts +19 -507
- package/adapters/mcp/runtime-gateway/routing-policy.ts +506 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +15 -66
- package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +123 -178
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +11 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +37 -1
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +416 -270
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +140 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +262 -3123
- package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
- package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +229 -0
- package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
- package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
- package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
- package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
- package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +320 -0
- package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +180 -0
- package/adapters/mcp/server.ts +91 -26
- package/adapters/mcp/setup.ts +5 -13
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +703 -3092
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +5 -10
- package/adapters/mcp/tool-mapping/repository-tools.ts +170 -213
- package/adapters/mcp/toolset-names.ts +11 -37
- package/adapters/mcp/toolset.ts +19 -92
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +377 -0
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +200 -683
- package/adapters/mcp/transports/oauth-http.ts +322 -0
- package/adapters/mcp/transports/session-registry.ts +6 -0
- package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
- package/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/plugin-registry.v1.json +6 -4
- package/assets/reference-configs/agentic-development-flow.md +26 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/global-working-rules.md +19 -0
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +205 -51
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
- package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
- package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
- package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
- package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +234 -0
- package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +74 -0
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +342 -0
- package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +6 -5
- package/docs/architecture/domains/workflow-engine.md +6 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +3 -3
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +16 -4
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/controller-performance-and-502.md +17 -0
- package/docs/operations/controller-reliability-runbook.md +2 -2
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -52
- package/docs/operations/releasing.md +2 -0
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +15 -7
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +25 -13
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +25 -25
- package/packages/kernel/cognition/api/index.ts +6 -0
- package/packages/kernel/cognition/application/activation-service.ts +451 -0
- package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
- package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
- package/packages/kernel/cognition/application/memory-service.ts +64 -0
- package/packages/kernel/cognition/domain/learning.ts +106 -0
- package/packages/kernel/cognition/domain/memory.ts +254 -0
- package/packages/kernel/controller/api/index.ts +1 -0
- package/packages/kernel/controller/application/continuation-service.ts +281 -0
- package/packages/kernel/controller/application/controller-round-service.ts +12 -0
- package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
- package/packages/kernel/controller/domain/controller-round.ts +4 -1
- package/packages/kernel/controller/domain/execution-quality.ts +3 -1
- package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +348 -97
- package/packages/kernel/controller/ports/controller-host.ts +2 -0
- package/packages/kernel/identity/api/index.ts +10 -0
- package/packages/kernel/identity/domain/activity-projection.ts +43 -0
- package/packages/kernel/identity/domain/capability-broker.ts +60 -0
- package/packages/kernel/identity/domain/grant.ts +72 -0
- package/packages/kernel/identity/domain/owned-resource.ts +116 -0
- package/packages/kernel/identity/domain/user-request.ts +93 -0
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +87 -0
- package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
- package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
- package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
- package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
- package/packages/kernel/scheduler/api/index.ts +0 -2
- package/packages/kernel/scheduler/application/eligibility.ts +4 -1
- package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
- package/packages/kernel/scheduler/application/settlement.ts +0 -15
- package/packages/kernel/scheduler/domain/schedule.ts +87 -0
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +166 -143
- package/packages/kernel/work/api/index.ts +3 -2
- package/packages/kernel/work/application/work-service.ts +18 -42
- package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
- package/packages/kernel/work/domain/engineering-design.ts +10 -5
- package/packages/kernel/work/domain/engineering-profile.ts +8 -12
- package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
- package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
- package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
- package/packages/kernel/work/domain/implementation-review.ts +14 -331
- package/packages/kernel/work/domain/objective-graph.ts +119 -0
- package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +34 -65
- package/packages/kernel/work/domain/types.ts +57 -51
- package/packages/kernel/work/infrastructure/work-contract-store.ts +847 -729
- package/packages/kernel/work/ports/work-contract-store.ts +5 -2
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- package/packages/plugin-runtime/browser/session-authority.ts +2 -1
- package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
- package/packages/plugin-runtime/computer/conversation-target.ts +153 -0
- package/packages/plugin-runtime/computer/index.ts +2 -0
- package/packages/plugin-runtime/computer/target-authority.ts +26 -1
- package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
- package/packages/protocols/computer/contract.ts +27 -1
- package/scripts/activate-source-baseline.ts +7 -9
- package/scripts/assemble-template.ts +11 -72
- package/scripts/canary-controller-semantic-wait.ts +12 -24
- package/scripts/check-mcp-compatibility.ts +94 -201
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +921 -239
- package/scripts/forge-local-recovery-helper.mjs +93 -15
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +6 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +1 -3
- package/scripts/run-bounded-command.mjs +5 -0
- package/scripts/run-governed-gate.ts +2 -1
- package/scripts/run-mobile-monitor-bridge.ts +0 -1
- package/scripts/smoke-control-plane-failure-domain.ts +94 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +12 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- package/scripts/smoke-runtime-recovery.ts +12 -43
- package/scripts/smoke-schedule-engine.ts +23 -15
- package/scripts/smoke-unattended-transport-recovery.ts +223 -0
- package/scripts/smoke-user-request-continuation-restart.ts +232 -0
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +95 -0
- package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
- package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
- package/scripts/smoke-workflow-supervisor.ts +103 -0
- package/scripts/sync-generated-authority.mjs +0 -1
- package/scripts/verify-forge-runtime.sh +2 -1
- package/src/cli/agent-jobs/executor-health.ts +12 -35
- package/src/cli/agent-jobs/integration.ts +20 -51
- package/src/cli/agent-jobs/job-manager.ts +7 -3
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- package/src/cli/commands/brain-assistant.ts +27 -13
- package/src/cli/commands/brain.ts +13 -15
- package/src/cli/commands/chatgpt.ts +1 -163
- package/src/cli/commands/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +137 -135
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/runtime.ts +3 -3
- package/src/cli/commands/setup-execution.ts +41 -0
- package/src/cli/commands/setup-profile.ts +8 -0
- package/src/cli/controller/check-runner.ts +13 -2
- package/src/cli/controller/completion-backlog.ts +3 -3
- package/src/cli/controller/completion-orchestrator.ts +5 -44
- package/src/cli/controller/completion-target.ts +3 -72
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +83 -102
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +107 -84
- package/src/cli/controller/execution-completion.ts +4 -7
- package/src/cli/controller/execution-policy.ts +36 -153
- package/src/cli/controller/governance.ts +2 -3
- package/src/cli/controller/issue-store.ts +4 -99
- package/src/cli/controller/launch-agents.ts +59 -18
- package/src/cli/controller/operational-plan.ts +25 -52
- package/src/cli/controller/runtime-config.ts +5 -1
- package/src/cli/controller/stuck-state-migration.ts +2 -3
- package/src/cli/controller/systemd-user.ts +42 -10
- package/src/cli/editing/edit-session.ts +18 -12
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +19 -14
- package/src/cli/local-bridge/console-view-models.ts +4 -13
- package/src/cli/local-bridge/facade-api.ts +64 -193
- package/src/cli/local-bridge/server.ts +24 -147
- package/src/cli/local-bridge/types.ts +0 -1
- package/src/cli/mcp/oauth-client-retirement.ts +74 -0
- package/src/cli/repositories/command-classifier.ts +0 -9
- package/src/cli/repositories/command-executor.ts +29 -23
- package/src/cli/repositories/command-scope.ts +37 -18
- package/src/cli/repositories/controller-home.ts +48 -0
- package/src/cli/repositories/ephemeral-workspace.ts +20 -30
- package/src/cli/repositories/git-command-executor.ts +2 -2
- package/src/cli/repositories/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +79 -12
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +101 -5
- package/src/cli/repository/inspector.ts +185 -343
- package/src/cli/repository/session-cache.ts +92 -6
- package/src/effects/process-runner.ts +17 -1
- package/src/runtime/context/assistant-context.ts +20 -7
- package/src/runtime/context/assistant-work-context.ts +89 -27
- package/src/runtime/context/automatic-learning.ts +565 -0
- package/src/runtime/context/cognitive-attention.ts +173 -0
- package/src/runtime/context/cognitive-skill-canary.ts +158 -0
- package/src/runtime/context/context-closure.ts +46 -9
- package/src/runtime/context/context-pack-process.ts +103 -0
- package/src/runtime/context/context-plane.ts +1 -9
- package/src/runtime/context/direct-controller-learning.ts +298 -0
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +16 -0
- package/src/runtime/context/semantic-navigation.ts +7 -35
- package/src/runtime/context/typescript-navigation-process.ts +145 -0
- package/src/runtime/context/typescript-navigation.ts +89 -3
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +215 -12
- package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
- package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
- package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
- package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
- package/src/runtime/control-plane/execution/session-store.ts +31 -1
- package/src/runtime/control-plane/execution/validation.ts +6 -8
- package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
- package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
- package/src/runtime/control-plane/execution/work-delivery-service.ts +616 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
- package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
- package/src/runtime/control-plane/execution/work-handle-authority.ts +197 -110
- package/src/runtime/control-plane/execution/work-handle-store.ts +72 -44
- package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
- package/src/runtime/control-plane/execution/work-operation-service.ts +60 -34
- package/src/runtime/control-plane/execution/work-preparation-service.ts +231 -267
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
- package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +201 -199
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +33 -85
- package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +6 -2
- package/src/runtime/control-plane/facade/capability-registry.ts +364 -37
- package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
- package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
- package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
- package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
- package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
- package/src/runtime/control-plane/facade/index.ts +0 -1
- package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
- package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
- package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
- package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
- package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
- package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
- package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
- package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
- package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
- package/src/runtime/control-plane/facade/types.ts +49 -108
- package/src/runtime/control-plane/facade/work-continuation.ts +9 -9
- package/src/runtime/control-plane/generated-cache-retention.ts +206 -59
- package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +256 -117
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -12
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
- package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
- package/src/runtime/control-plane/governance/access-policy.ts +19 -2
- package/src/runtime/control-plane/governance/authorization.ts +20 -3
- package/src/runtime/control-plane/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +29 -300
- package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
- package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
- package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
- package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
- package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
- package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
- package/src/runtime/control-plane/release-retention.ts +135 -47
- package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
- package/src/runtime/control-plane/runtime-cleanup.ts +295 -107
- package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
- package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
- package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
- package/src/runtime/diagnostics/incident-repair.ts +313 -146
- package/src/runtime/diagnostics/mcp-timing.ts +114 -0
- package/src/runtime/evidence/artifact-store.ts +158 -40
- package/src/runtime/evidence/evidence-store.ts +112 -8
- package/src/runtime/evidence/output-handle-index.ts +225 -0
- package/src/runtime/evidence/result-store.ts +158 -44
- package/src/runtime/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- package/src/runtime/execution/jobs/store.ts +3 -4
- package/src/runtime/execution/managed-workspace.ts +57 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
- package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
- package/src/runtime/execution/process-runtime/check-result.ts +105 -1
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
- package/src/runtime/execution/process-runtime/command-facade.ts +82 -30
- package/src/runtime/execution/process-runtime/gc.ts +15 -2
- package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
- package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
- package/src/runtime/execution/process-runtime/index.ts +3 -0
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
- package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
- package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
- package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
- package/src/runtime/execution/process-runtime/runtime.ts +55 -18
- package/src/runtime/execution/process-runtime/store.ts +30 -2
- package/src/runtime/execution/process-runtime/types.ts +6 -1
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +3 -8
- package/src/runtime/maintenance/cleanup.ts +41 -5
- package/src/runtime/platform/service-inventory.ts +130 -0
- package/src/runtime/platform/service-manager.ts +8 -0
- package/src/runtime/plugins/action-application.ts +97 -29
- package/src/runtime/plugins/app-store-connect-adapter.ts +290 -18
- package/src/runtime/plugins/browser-adapter.ts +1124 -333
- package/src/runtime/plugins/browser-automation-service.ts +23 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +431 -137
- package/src/runtime/plugins/browser-manifest-surface.ts +47 -12
- package/src/runtime/plugins/browser-native-session-service.ts +63 -13
- package/src/runtime/plugins/browser-registration.ts +18 -0
- package/src/runtime/plugins/browser-runtime.ts +40 -36
- package/src/runtime/plugins/browser-session-authority.ts +2 -2
- package/src/runtime/plugins/browser-session-store.ts +46 -76
- package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +228 -0
- package/src/runtime/plugins/computer-registration.ts +298 -20
- package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
- package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
- package/src/runtime/plugins/external-adapter.ts +42 -6
- package/src/runtime/plugins/external-registration.ts +21 -1
- package/src/runtime/plugins/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +189 -3
- package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
- package/src/runtime/plugins/google-shared.ts +24 -12
- package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +46 -41
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/lightweight-action.ts +208 -2
- package/src/runtime/plugins/local-system-adapter.ts +12 -100
- package/src/runtime/plugins/managed-process-adapter.ts +12 -5
- package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
- package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +682 -435
- package/src/runtime/plugins/types.ts +24 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/projections/invalidation.ts +7 -3
- package/src/runtime/projections/materialized-view.ts +80 -36
- package/src/runtime/recovery/actions.ts +4 -4
- package/src/runtime/recovery/capability-action-application.ts +46 -45
- package/src/runtime/recovery/lifecycle-inventory.ts +30 -40
- package/src/runtime/recovery/maintenance-executor.ts +374 -123
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/recovery/store.ts +29 -11
- package/src/runtime/release/release-coordinator.ts +312 -0
- package/src/runtime/release/release-session.ts +716 -0
- package/src/runtime/release/release-state-migration.ts +25 -0
- package/src/runtime/resources/leases/store.ts +56 -25
- package/src/runtime/root/computer-composition.ts +191 -6
- package/src/runtime/root/controller-progression-composition.ts +254 -0
- package/src/runtime/root/controller-round-composition.ts +20 -1
- package/src/runtime/root/deployment-topology.ts +106 -0
- package/src/runtime/root/entry.ts +4 -3
- package/src/runtime/root/known-good-recovery.ts +184 -0
- package/src/runtime/root/local-bridge.ts +0 -1
- package/src/runtime/root/mcp-transport.ts +111 -18
- package/src/runtime/root/ownership.ts +261 -9
- package/src/runtime/root/package-connector-service.ts +227 -29
- package/src/runtime/root/package-runtime-release.ts +117 -30
- package/src/runtime/root/package-runtime-service.ts +24 -24
- package/src/runtime/root/release-execution-canary.ts +57 -12
- package/src/runtime/root/release-loader.ts +90 -8
- package/src/runtime/root/release-manifest.ts +80 -40
- package/src/runtime/root/release-materialize.ts +372 -106
- package/src/runtime/root/release-store.ts +395 -36
- package/src/runtime/root/runtime-lane.ts +191 -0
- package/src/runtime/root/runtime.ts +160 -29
- package/src/runtime/root/scheduled-controller-composition.ts +29 -3
- package/src/runtime/root/service-runner.ts +2 -2
- package/src/runtime/root/service.ts +126 -44
- package/src/runtime/root/status.ts +48 -0
- package/src/runtime/root/types.ts +32 -9
- package/src/runtime/root/workflow-supervisor-composition.ts +681 -0
- package/src/runtime/root/workflow-supervisor-runtime.ts +564 -0
- package/src/runtime/root/write-fence.ts +50 -2
- package/src/runtime/safe-tooling/google-credential-store.ts +67 -22
- package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +5 -2
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
- package/src/runtime/shared/json-files.ts +8 -3
- package/src/runtime/shared/process-environment.ts +20 -9
- package/src/runtime/shared/response-budget.ts +1 -3
- package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
- package/src/runtime/standalone-recovery/core.ts +3792 -417
- package/src/runtime/standalone-recovery/entry.ts +507 -45
- package/src/runtime/standalone-recovery/installer.ts +262 -148
- package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
- package/src/runtime/standalone-recovery/operation-lock.ts +12 -2
- package/src/runtime/standalone-recovery/performance.ts +54 -16
- package/src/runtime/standalone-recovery/release.ts +53 -5
- package/src/runtime/standalone-recovery/service-labels.ts +5 -2
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +11 -3
- package/src/runtime/workflow/schedules/engine.ts +166 -178
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +95 -11
- package/src/runtime/workflows/runtime.ts +15 -19
- package/src/testing/test-governance.ts +5 -1
- package/supervisor/chatgpt-conversation.ts +23 -0
- package/supervisor/client.ts +218 -0
- package/supervisor/control-plane.ts +963 -0
- package/supervisor/entry.ts +15 -0
- package/supervisor/forge-validators.ts +147 -0
- package/supervisor/index.ts +5 -0
- package/supervisor/native-browser-adapter.ts +1151 -0
- package/supervisor/native-messaging/host.ts +58 -0
- package/supervisor/native-messaging/manifest.ts +26 -0
- package/supervisor/native-messaging/protocol.ts +27 -0
- package/supervisor/paths.ts +18 -0
- package/supervisor/protocol.ts +250 -0
- package/supervisor/server.ts +412 -0
- package/supervisor/store.ts +1870 -0
- package/supervisor/tsconfig.json +8 -0
- package/supervisor/types.ts +261 -0
- package/SKILL.md +0 -427
- package/adapters/chatgpt/controller-host.ts +0 -119
- package/adapters/mcp/controller-round-compatibility.ts +0 -184
- package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
- package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- package/packages/kernel/progression/api/index.ts +0 -2
- package/packages/kernel/progression/application/projector.ts +0 -211
- package/packages/kernel/progression/domain/types.ts +0 -116
- package/packages/kernel/progression/index.ts +0 -1
- package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
- package/packages/kernel/scheduler/domain/continuation.ts +0 -38
- package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
- package/packages/kernel/work/domain/admission-policy.ts +0 -34
- package/scripts/benchmark-background-check-overlap.ts +0 -228
- package/scripts/benchmark-computer-hotpath.ts +0 -255
- package/scripts/benchmark-controller-hotpath.ts +0 -212
- package/scripts/benchmark-controller-round2.ts +0 -191
- package/scripts/benchmark-ios-agent-device.ts +0 -511
- package/scripts/benchmark-mcp-host.sh +0 -117
- package/scripts/benchmark-mcp-transport.ts +0 -169
- package/scripts/benchmark-operational-memory-activation.ts +0 -235
- package/scripts/benchmark-operational-shadow.ts +0 -28
- package/scripts/benchmark-quality-harness.ts +0 -146
- package/scripts/benchmark-route-session-concurrency.ts +0 -915
- package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
- package/scripts/benchmark-thin-harness.ts +0 -251
- package/scripts/benchmark-typescript-navigation.ts +0 -186
- package/scripts/benchmark-work-efficiency.ts +0 -334
- package/scripts/build-controller-ui.ts +0 -28
- package/scripts/check-npm-release.sh +0 -32
- package/scripts/check-public-release-surface.sh +0 -19
- package/scripts/check-release-published.sh +0 -57
- package/scripts/check-release-readiness.sh +0 -37
- package/scripts/check-release-version.mjs +0 -74
- package/scripts/check-skill-version.ts +0 -5
- package/scripts/check-tarball-install-smoke.sh +0 -150
- package/scripts/ensure-codegraph.sh +0 -16
- package/scripts/package-source-archive.sh +0 -147
- package/scripts/public-release-files.txt +0 -39
- package/scripts/public-release.ts +0 -174
- package/scripts/publish-release-tarball.sh +0 -27
- package/scripts/route-nl-vs-ts-eval.ts +0 -679
- package/scripts/run-skill-hook.ts +0 -244
- package/scripts/stage-runtime-release.ts +0 -44
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- package/src/cli/controller/progress.ts +0 -656
- package/src/cli/controller/work-mode.ts +0 -181
- package/src/cli/local-bridge/console-assets.ts +0 -17
- package/src/cli/local-bridge/dashboard.ts +0 -17
- package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
- package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
- package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
- package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
- package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
- package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
- package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
- package/src/cli/local-bridge/frontend/src/styles.css +0 -28
- package/src/cli/local-bridge/frontend/src/types.ts +0 -15
- package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
- package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
- package/src/cli/local-bridge/ui-dist/app.css +0 -1
- package/src/cli/local-bridge/ui-dist/app.js +0 -11228
- package/src/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
- package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
- package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
- package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
- package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
- package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
- package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
- package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
- package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
- package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
- package/src/runtime/control-plane/routing/index.ts +0 -1
- package/src/runtime/control-plane/routing/route-policy.ts +0 -401
- package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { parseChatgptConversationIdentity } from './chatgpt-conversation';
|
|
3
|
+
import { parseSupervisorCompletion, renderEffectMarker, renderSupervisorPrompt, sha256, validateEffectId, validateSupervisorObjective } from './protocol';
|
|
4
|
+
import { WorkflowSupervisorStore } from './store';
|
|
5
|
+
import { getWorkContract } from '../packages/kernel/work/api/index';
|
|
6
|
+
import { readRequirement } from '../src/runtime/control-plane/persistence/requirement-store';
|
|
7
|
+
import { resolveWorkflowSupervisorForgeHome } from './paths';
|
|
8
|
+
import type { WorkflowAssistantObservation, WorkflowAssistantObservationResult, WorkflowContractValidation, WorkflowEffectKind, WorkflowSupervisorBrowserPollResult, WorkflowSupervisorBrowserTask, WorkflowSupervisorCompletion, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorLifecycleHooks, WorkflowSupervisorProjectScope, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTerminalState, WorkflowSupervisorValidators, WorkflowSupervisorPreflightComposerObservation } from './types';
|
|
9
|
+
|
|
10
|
+
function compactProjectIdentity(value: string): string {
|
|
11
|
+
return value.normalize('NFKC').toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function projectIdentityTokens(value: string): string[] {
|
|
15
|
+
return value.normalize('NFKC').toLocaleLowerCase().split(/[^\p{L}\p{N}]+/gu).filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function boundedProjectIdentityMatch(expected: string, observed: string): boolean {
|
|
19
|
+
const compactExpected = compactProjectIdentity(expected);
|
|
20
|
+
const compactObserved = compactProjectIdentity(observed);
|
|
21
|
+
if (!compactExpected || !compactObserved) return false;
|
|
22
|
+
if (compactExpected === compactObserved) return true;
|
|
23
|
+
if (compactExpected.length < 4) return false;
|
|
24
|
+
const expectedTokens = projectIdentityTokens(expected);
|
|
25
|
+
const observedTokens = projectIdentityTokens(observed);
|
|
26
|
+
if (expectedTokens.length === 0 || expectedTokens.length >= observedTokens.length) return false;
|
|
27
|
+
return expectedTokens.every((token, index) => observedTokens[index] === token);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function projectSlugFromUrl(value: string): string | undefined {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(value);
|
|
33
|
+
const match = /^\/g\/(g-p-[a-z0-9]+)(?:-([^/]+))?\/project\/?$/i.exec(parsed.pathname);
|
|
34
|
+
return match?.[2]?.toLocaleLowerCase();
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function projectMatchesScope(conversation: WorkflowSupervisorDiscoveredConversation, scope: WorkflowSupervisorProjectScope): boolean {
|
|
41
|
+
const names = [scope.title, ...(scope.aliases ?? [])]
|
|
42
|
+
.map((value) => value.trim().toLocaleLowerCase())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
const projectTitle = conversation.projectTitle?.trim().toLocaleLowerCase();
|
|
45
|
+
if (projectTitle && names.some((name) => boundedProjectIdentityMatch(name, projectTitle))) return true;
|
|
46
|
+
const projectSlug = conversation.projectUrl ? projectSlugFromUrl(conversation.projectUrl) : undefined;
|
|
47
|
+
if (!projectSlug) return false;
|
|
48
|
+
return names.some((name) => boundedProjectIdentityMatch(name, projectSlug));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function effectId(): string { return `fx_${randomUUID().replaceAll('-', '')}`; }
|
|
52
|
+
function stableEffectId(originKey: string): string {
|
|
53
|
+
return validateEffectId(`fx_${sha256(originKey).slice(0, 32)}`);
|
|
54
|
+
}
|
|
55
|
+
const rejectUnconfigured = async (): Promise<WorkflowContractValidation> => ({ valid: false, reason: 'validator_unconfigured' });
|
|
56
|
+
|
|
57
|
+
export class WorkflowSupervisorControlPlane {
|
|
58
|
+
readonly validators: WorkflowSupervisorValidators;
|
|
59
|
+
readonly hooks: WorkflowSupervisorLifecycleHooks;
|
|
60
|
+
constructor(readonly store: WorkflowSupervisorStore, validators: Partial<WorkflowSupervisorValidators> = {}, hooks: WorkflowSupervisorLifecycleHooks = {}) {
|
|
61
|
+
this.validators = { completionContract: validators.completionContract ?? rejectUnconfigured, userBlockerPolicy: validators.userBlockerPolicy ?? rejectUnconfigured };
|
|
62
|
+
this.hooks = hooks;
|
|
63
|
+
}
|
|
64
|
+
registerTask(input: WorkflowSupervisorTaskInput): WorkflowSupervisorTask { return this.store.registerTask(input); }
|
|
65
|
+
reviseTaskObjective(input: {
|
|
66
|
+
taskId: string;
|
|
67
|
+
objective: string;
|
|
68
|
+
expectedObjectiveSha256: string;
|
|
69
|
+
requestId: string;
|
|
70
|
+
authorizedBy?: string;
|
|
71
|
+
workId?: string;
|
|
72
|
+
requirementId?: string;
|
|
73
|
+
expectedCompletionContractSha256?: string;
|
|
74
|
+
}): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; completionContractSha256: string; semanticBindingChanged: boolean; refreshedEffect?: WorkflowSupervisorEffect } {
|
|
75
|
+
const task = this.requireTask(input.taskId);
|
|
76
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
77
|
+
const objective = validateSupervisorObjective(input.objective);
|
|
78
|
+
if (!/^[0-9a-f]{64}$/i.test(input.expectedObjectiveSha256)) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_EXPECTED_SHA_INVALID');
|
|
79
|
+
if (!input.requestId.trim()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_REQUIRED');
|
|
80
|
+
const workId = input.workId?.trim() || undefined;
|
|
81
|
+
const requirementId = input.requirementId?.trim() || undefined;
|
|
82
|
+
if (workId && requirementId) throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_TARGET_INVALID');
|
|
83
|
+
if ((workId || requirementId) && !input.expectedCompletionContractSha256) {
|
|
84
|
+
throw new Error('WORKFLOW_SUPERVISOR_COMPLETION_CONTRACT_REVISION_EXPECTED_SHA_REQUIRED');
|
|
85
|
+
}
|
|
86
|
+
if (!workId && !requirementId && input.expectedCompletionContractSha256) {
|
|
87
|
+
throw new Error('WORKFLOW_SUPERVISOR_COMPLETION_CONTRACT_REVISION_TARGET_REQUIRED');
|
|
88
|
+
}
|
|
89
|
+
let semanticBinding: { workId?: string; requirementId?: string; expectedCompletionContractSha256: string } | undefined;
|
|
90
|
+
if (workId || requirementId) {
|
|
91
|
+
if (task.completionContract.kind !== 'model_semantic_completion') {
|
|
92
|
+
throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_REVISION_KIND_UNSUPPORTED');
|
|
93
|
+
}
|
|
94
|
+
const controllerHome = typeof task.completionContract.controller_home === 'string' ? task.completionContract.controller_home.trim() : '';
|
|
95
|
+
const repoId = typeof task.completionContract.repo_id === 'string' ? task.completionContract.repo_id.trim() : '';
|
|
96
|
+
if (!controllerHome || !repoId
|
|
97
|
+
|| resolveWorkflowSupervisorForgeHome(controllerHome) !== resolveWorkflowSupervisorForgeHome(this.store.forgeHome)) {
|
|
98
|
+
throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_CONTEXT_REQUIRED');
|
|
99
|
+
}
|
|
100
|
+
if (workId && !getWorkContract({ controllerHome, scopeKey: repoId }, workId)) {
|
|
101
|
+
throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_WORK_NOT_FOUND');
|
|
102
|
+
}
|
|
103
|
+
if (requirementId && !readRequirement({ controllerHome }, requirementId)?.value) {
|
|
104
|
+
throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_REQUIREMENT_NOT_FOUND');
|
|
105
|
+
}
|
|
106
|
+
semanticBinding = {
|
|
107
|
+
...(workId ? { workId } : {}),
|
|
108
|
+
...(requirementId ? { requirementId } : {}),
|
|
109
|
+
expectedCompletionContractSha256: input.expectedCompletionContractSha256!,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const pending = this.store.currentUnappliedEffect(task.taskId);
|
|
113
|
+
let refreshEffect: { effectId: string; expectedPrompt: string; prompt: string } | undefined;
|
|
114
|
+
if (pending && !this.store.latestEffectDispatch(pending.effectId)) {
|
|
115
|
+
const source = pending.sourceCompletionFingerprint ? this.store.getCompletion(pending.sourceCompletionFingerprint) : undefined;
|
|
116
|
+
const checkpoint = source?.proposal.reason === 'compact_receipt' ? undefined : source?.proposal.checkpoint;
|
|
117
|
+
refreshEffect = {
|
|
118
|
+
effectId: pending.effectId,
|
|
119
|
+
expectedPrompt: pending.prompt,
|
|
120
|
+
prompt: this.renderPrompt({ ...task, objective }, pending.effectId, pending.kind, checkpoint),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return this.store.reviseTaskObjective({
|
|
124
|
+
taskId: task.taskId,
|
|
125
|
+
objective,
|
|
126
|
+
expectedObjectiveSha256: input.expectedObjectiveSha256.toLowerCase(),
|
|
127
|
+
requestId: input.requestId.trim(),
|
|
128
|
+
authorizedBy: input.authorizedBy?.trim() || 'operator',
|
|
129
|
+
...(semanticBinding ? { semanticBinding } : {}),
|
|
130
|
+
...(refreshEffect ? { refreshEffect } : {}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Explicit operator recovery. It is deliberately evidence-classified rather
|
|
135
|
+
* than a generic "try again": each class names the durable mechanical fact
|
|
136
|
+
* that makes exactly one bounded next step safe, and none of them can replay
|
|
137
|
+
* a submission whose outcome is unknown.
|
|
138
|
+
*
|
|
139
|
+
* - proven-un-applied effect at its retry ceiling -> refund the mechanical
|
|
140
|
+
* budget once and re-dispatch that same effect;
|
|
141
|
+
* - unknown-outcome effect -> read-only reclassification only, unless the
|
|
142
|
+
* operator explicitly authorizes retiring it so a *fresh* turn can run;
|
|
143
|
+
* - applied effect whose single provider resume is exhausted -> reserve one
|
|
144
|
+
* new, separately identified recovery turn on the same conversation.
|
|
145
|
+
*/
|
|
146
|
+
recoverTask(input: {
|
|
147
|
+
taskId: string;
|
|
148
|
+
sourceEffectId?: string;
|
|
149
|
+
requestId: string;
|
|
150
|
+
reason: string;
|
|
151
|
+
/** Explicit operator authority required to retire an outcome-unknown effect. */
|
|
152
|
+
supersedeUnknown?: boolean;
|
|
153
|
+
authorizedBy?: string;
|
|
154
|
+
}): { recoveryEffect: WorkflowSupervisorEffect; action: 'retry_authorized' | 'recovery_reserved' | 'unknown_superseded' } {
|
|
155
|
+
const task = this.requireTask(input.taskId);
|
|
156
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
157
|
+
if (!input.requestId.trim() || !input.reason.trim()) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_REASON_REQUIRED');
|
|
158
|
+
const authorizedBy = input.authorizedBy?.trim() || 'operator';
|
|
159
|
+
// Identity-stable replay first: an operator recovery already reserved for
|
|
160
|
+
// this exact source effect is the durable answer, whether or not it has
|
|
161
|
+
// since been dispatched.
|
|
162
|
+
const legacyOrigin = input.sourceEffectId ? `provider-recovery:${input.sourceEffectId}` : undefined;
|
|
163
|
+
const existingLegacy = legacyOrigin ? this.store.getEffectByOriginKey(legacyOrigin) : undefined;
|
|
164
|
+
if (existingLegacy?.taskId === task.taskId) return { recoveryEffect: existingLegacy, action: 'recovery_reserved' };
|
|
165
|
+
// The same explicit supersession decision is one durable fact; replaying it
|
|
166
|
+
// returns its replacement instead of failing on the now-retired source.
|
|
167
|
+
const supersedeOrigin = `operator-recovery:${input.sourceEffectId ?? ''}:${input.requestId}`;
|
|
168
|
+
const existingSupersede = this.store.getEffectByOriginKey(supersedeOrigin);
|
|
169
|
+
if (existingSupersede?.taskId === task.taskId) return { recoveryEffect: existingSupersede, action: 'unknown_superseded' };
|
|
170
|
+
const pending = this.store.currentUnappliedEffect(task.taskId);
|
|
171
|
+
if (input.sourceEffectId && pending && pending.effectId !== input.sourceEffectId) {
|
|
172
|
+
throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
|
|
173
|
+
}
|
|
174
|
+
if (pending) {
|
|
175
|
+
if (this.store.effectDispatchBudget(pending.effectId).generations === 0) {
|
|
176
|
+
// Browser provider was repaired independently of Forge's source release.
|
|
177
|
+
// The old enrollment text and effect identity remain authoritative;
|
|
178
|
+
// rearm a single preparation without manufacturing a provider send.
|
|
179
|
+
this.store.authorizeOperatorPreflightRetry({ effectId: pending.effectId, requestId: input.requestId,
|
|
180
|
+
reason: input.reason, authorizedBy });
|
|
181
|
+
return { recoveryEffect: pending, action: 'retry_authorized' };
|
|
182
|
+
}
|
|
183
|
+
// Read-only reclassification first: a dispatch-owner pre-send return is a
|
|
184
|
+
// stronger fact than any operator decision and makes the effect retryable
|
|
185
|
+
// through the normal mechanical path.
|
|
186
|
+
if (this.store.reconcileNativePreSendFailure(pending.effectId, input.requestId)) {
|
|
187
|
+
return { recoveryEffect: pending, action: 'retry_authorized' };
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
this.store.authorizeOperatorRetry({ effectId: pending.effectId, requestId: input.requestId, reason: input.reason, authorizedBy });
|
|
191
|
+
return { recoveryEffect: pending, action: 'retry_authorized' };
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
194
|
+
if (message !== 'WORKFLOW_SUPERVISOR_RECOVERY_OUTCOME_UNKNOWN') throw error;
|
|
195
|
+
if (input.supersedeUnknown !== true) throw error;
|
|
196
|
+
}
|
|
197
|
+
// The operator explicitly decided to stop waiting on an unknown
|
|
198
|
+
// submission. Record that decision durably and reserve one *new* turn in
|
|
199
|
+
// the same transaction; the unknown effect is never resent and never
|
|
200
|
+
// silently discarded.
|
|
201
|
+
const origin = `operator-recovery:${pending.effectId}:${input.requestId}`;
|
|
202
|
+
const supersededId = stableEffectId(origin);
|
|
203
|
+
const prompt = this.renderPrompt(task, supersededId, 'recovery', undefined,
|
|
204
|
+
`The operator explicitly superseded outcome-unknown effect ${pending.effectId} (${input.requestId}): ${input.reason}. That effect is retired and must never be resent; read durable Forge state and continue this same task with one fresh turn.`);
|
|
205
|
+
const superseded = this.store.supersedeUnknownEffect({
|
|
206
|
+
effectId: pending.effectId, requestId: input.requestId, reason: input.reason, authorizedBy,
|
|
207
|
+
replacement: { effectId: supersededId, prompt },
|
|
208
|
+
});
|
|
209
|
+
return { recoveryEffect: superseded.recoveryEffect, action: 'unknown_superseded' };
|
|
210
|
+
}
|
|
211
|
+
const source = this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
|
|
212
|
+
if (!source || (input.sourceEffectId && source.effectId !== input.sourceEffectId)) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
|
|
213
|
+
const origin = `provider-recovery:${source.effectId}`;
|
|
214
|
+
const id = stableEffectId(origin);
|
|
215
|
+
if (this.store.providerResumeExhausted(source.effectId)) {
|
|
216
|
+
// The automatic resume ceiling is the exact mechanical fact that permits
|
|
217
|
+
// a further operator turn; without it this path must not exist.
|
|
218
|
+
const prompt = this.renderPrompt(task, id, 'recovery', undefined,
|
|
219
|
+
`The operator explicitly requested recovery (${input.requestId}): ${input.reason}. The single bounded provider resume for applied effect ${source.effectId} is exhausted; the source effect remains applied and must not be replayed. Read durable Forge state and continue this same task.`);
|
|
220
|
+
return { recoveryEffect: this.store.reserveOperatorProviderRecovery({
|
|
221
|
+
taskId: task.taskId, sourceEffectId: source.effectId, requestId: input.requestId, reason: input.reason,
|
|
222
|
+
authorizedBy, originKey: origin, effectId: id, prompt,
|
|
223
|
+
}), action: 'recovery_reserved' };
|
|
224
|
+
}
|
|
225
|
+
const prompt = this.renderPrompt(task, id, 'recovery', undefined,
|
|
226
|
+
`The user explicitly requested recovery (${input.requestId}): ${input.reason}. Preserve the prior applied effect and all completed source work; read durable state and continue this same task. This is one operator-authorized recovery, not a reset of automatic recovery or dispatch budgets.`);
|
|
227
|
+
return { recoveryEffect: this.store.reserveOperatorRecovery({
|
|
228
|
+
taskId: task.taskId, sourceEffectId: source.effectId, requestId: input.requestId, reason: input.reason, effectId: id, prompt,
|
|
229
|
+
}), action: 'recovery_reserved' };
|
|
230
|
+
}
|
|
231
|
+
reserveEnrollment(taskId: string, canonicalEffectId?: string): WorkflowSupervisorEffect {
|
|
232
|
+
const task = this.requireTask(taskId);
|
|
233
|
+
// A task that already reached a terminal supervisor action is not deliverable.
|
|
234
|
+
// Reserving another enrollment effect for it produced an "enrolled" result that
|
|
235
|
+
// could never be delivered, so the ControllerRound waited forever instead of
|
|
236
|
+
// surfacing the operator/provider decision that terminal state represents.
|
|
237
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
238
|
+
const originKey = `enrollment:${taskId}`;
|
|
239
|
+
// Supervisor owns the outer-turn effect identity. A lower ControllerRound may
|
|
240
|
+
// rotate when the semantic Work carrier changes, but the Requirement + exact
|
|
241
|
+
// conversation task must keep one enrollment effect. Reuse by origin before
|
|
242
|
+
// considering a newer lower-layer canonical effect id.
|
|
243
|
+
const existingForOrigin = this.store.getEffectByOriginKey(originKey);
|
|
244
|
+
let effect: WorkflowSupervisorEffect;
|
|
245
|
+
if (existingForOrigin) {
|
|
246
|
+
if (existingForOrigin.taskId !== taskId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT:${originKey}`);
|
|
247
|
+
if (existingForOrigin.kind !== 'enrollment') throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_KIND_CONFLICT:${originKey}`);
|
|
248
|
+
effect = existingForOrigin;
|
|
249
|
+
} else {
|
|
250
|
+
const id = canonicalEffectId ? validateEffectId(canonicalEffectId) : stableEffectId(originKey);
|
|
251
|
+
const existing = this.store.getEffect(id);
|
|
252
|
+
if (existing) {
|
|
253
|
+
if (existing.taskId !== taskId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT:${originKey}`);
|
|
254
|
+
if (existing.kind !== 'enrollment') throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_KIND_CONFLICT:${originKey}`);
|
|
255
|
+
effect = existing;
|
|
256
|
+
} else {
|
|
257
|
+
effect = this.store.reserveEffect({ taskId, effectId: id, kind: 'enrollment', originKey, prompt: this.renderPrompt(task, id, 'enrollment') });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const inheritedDispatch = this.hooks.inheritedEffectDispatch?.(task, effect);
|
|
261
|
+
if (inheritedDispatch && !this.store.effectApplied(effect.effectId)) {
|
|
262
|
+
this.store.recordEffectDispatchStarted(
|
|
263
|
+
effect.effectId,
|
|
264
|
+
inheritedDispatch.generation,
|
|
265
|
+
inheritedDispatch.dispatchId,
|
|
266
|
+
inheritedDispatch.evidence,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return effect;
|
|
270
|
+
}
|
|
271
|
+
observeEffect(input: { effectId: string; observationId: string; outcome: 'applied' | 'not_applied' | 'unknown'; evidence?: Record<string, unknown> }): void {
|
|
272
|
+
this.store.recordEffectObservation(validateEffectId(input.effectId), input.observationId, input.outcome, input.evidence);
|
|
273
|
+
}
|
|
274
|
+
getTask(taskId: string): WorkflowSupervisorTask | undefined { return this.store.getTask(taskId); }
|
|
275
|
+
getTaskByConversationId(conversationId: string): { task: WorkflowSupervisorTask; terminal?: WorkflowSupervisorTerminalState } | undefined {
|
|
276
|
+
const task = this.store.getTaskByConversationId(conversationId);
|
|
277
|
+
if (!task) return undefined;
|
|
278
|
+
const terminal = this.store.terminalAction(task.taskId);
|
|
279
|
+
return { task, ...(terminal ? { terminal } : {}) };
|
|
280
|
+
}
|
|
281
|
+
listTasks(activeOnly = false): WorkflowSupervisorTask[] {
|
|
282
|
+
const tasks = this.store.listTasks();
|
|
283
|
+
return activeOnly ? tasks.filter((task) => !this.store.terminalAction(task.taskId)) : tasks;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Completed/blocked/stopped history is a read-only projection of canonical
|
|
287
|
+
* terminal events. It never archives the ChatGPT conversation or rewrites Work.
|
|
288
|
+
*/
|
|
289
|
+
listTerminalTasks(): Array<{ task: WorkflowSupervisorTask; terminal: WorkflowSupervisorTerminalState }> {
|
|
290
|
+
return this.store.listTasks().flatMap((task) => {
|
|
291
|
+
const terminal = this.store.terminalAction(task.taskId);
|
|
292
|
+
return terminal ? [{ task, terminal }] : [];
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/** Reconcile Work-owned task closure after a crash or a direct Work writer.
|
|
296
|
+
* Independent Requirement and standalone Goals are intentionally excluded.
|
|
297
|
+
*/
|
|
298
|
+
reconcileTerminalWorkBoundTasks(): string[] {
|
|
299
|
+
if (!this.hooks.terminalWorkStopReasonForTask) return [];
|
|
300
|
+
const stopped: string[] = [];
|
|
301
|
+
for (const task of this.store.listTasks()) {
|
|
302
|
+
if (this.store.terminalAction(task.taskId)) continue;
|
|
303
|
+
const reason = this.hooks.terminalWorkStopReasonForTask(task);
|
|
304
|
+
if (!reason) continue;
|
|
305
|
+
const result = this.store.stopTask(task.taskId, reason);
|
|
306
|
+
if (!result.deduplicated) stopped.push(task.taskId);
|
|
307
|
+
}
|
|
308
|
+
return stopped;
|
|
309
|
+
}
|
|
310
|
+
stopTask(taskId: string, reason = 'Stopped by operator request.'): { taskId: string; terminal: 'STOPPED'; deduplicated: boolean } {
|
|
311
|
+
return this.store.stopTask(taskId, reason);
|
|
312
|
+
}
|
|
313
|
+
bindBootstrapConversation(input: { taskId: string; conversationId: string; conversationUrl: string }): WorkflowSupervisorTask {
|
|
314
|
+
const identity = parseChatgptConversationIdentity(input.conversationUrl);
|
|
315
|
+
if (identity.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_CONVERSATION_MISMATCH');
|
|
316
|
+
const pendingMigration = this.store.freshConversationMigrationContext(input.taskId);
|
|
317
|
+
const task = this.store.bindBootstrapConversation(input.taskId, identity.conversationId, identity.canonicalUrl);
|
|
318
|
+
this.hooks.bootstrapConversationBound?.(task, pendingMigration ? {
|
|
319
|
+
migratedFromConversationId: pendingMigration.fromConversationId,
|
|
320
|
+
migrationRequestId: pendingMigration.requestId,
|
|
321
|
+
} : undefined);
|
|
322
|
+
return task;
|
|
323
|
+
}
|
|
324
|
+
getEffect(id: string): WorkflowSupervisorEffect | undefined { return this.store.getEffect(validateEffectId(id)); }
|
|
325
|
+
preSendComposerObservation(effectId: string): WorkflowSupervisorPreflightComposerObservation | undefined {
|
|
326
|
+
return this.store.preSendComposerObservation(validateEffectId(effectId));
|
|
327
|
+
}
|
|
328
|
+
/** Bind the one canonical Scheduler occurrence lane to an exact Work-owned Supervisor task. */
|
|
329
|
+
bindScheduledWake(input: { taskId: string; repoId: string; workId: string; scheduleId: string }) {
|
|
330
|
+
const task = this.requireTask(input.taskId);
|
|
331
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
332
|
+
const bound = this.store.bindScheduledWake(input);
|
|
333
|
+
const pending = this.store.currentUnappliedEffect(task.taskId);
|
|
334
|
+
if (pending && !this.store.latestEffectDispatch(pending.effectId) && !this.store.effectApplied(pending.effectId)) {
|
|
335
|
+
const source = pending.sourceCompletionFingerprint ? this.store.getCompletion(pending.sourceCompletionFingerprint) : undefined;
|
|
336
|
+
const checkpoint = source?.proposal.reason === 'compact_receipt' ? undefined : source?.proposal.checkpoint;
|
|
337
|
+
this.store.refreshUndispatchedEffectPrompt({
|
|
338
|
+
taskId: task.taskId,
|
|
339
|
+
effectId: pending.effectId,
|
|
340
|
+
expectedPrompt: pending.prompt,
|
|
341
|
+
prompt: this.renderPrompt(bound.task, pending.effectId, pending.kind, checkpoint),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return bound;
|
|
345
|
+
}
|
|
346
|
+
/** Consume a completed WAIT receipt exactly once for one exact Scheduler occurrence. */
|
|
347
|
+
wakeScheduledTask(input: { taskId: string; scheduleId: string; occurrenceId: string }): { effect: WorkflowSupervisorEffect; deduplicated: boolean } {
|
|
348
|
+
const task = this.requireTask(input.taskId);
|
|
349
|
+
if (task.continuationPolicy.scheduled_wake_schedule_id !== input.scheduleId) {
|
|
350
|
+
throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_BINDING_MISMATCH');
|
|
351
|
+
}
|
|
352
|
+
const originKey = `scheduled-wake:${input.scheduleId}:${input.occurrenceId}`;
|
|
353
|
+
const id = stableEffectId(originKey);
|
|
354
|
+
const prior = this.store.getEffectByOriginKey(originKey);
|
|
355
|
+
if (prior) {
|
|
356
|
+
if (prior.taskId !== task.taskId || prior.effectId !== id || prior.kind !== 'continuation' || !prior.sourceCompletionFingerprint) {
|
|
357
|
+
throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_IDENTITY_CONFLICT');
|
|
358
|
+
}
|
|
359
|
+
return { effect: prior, deduplicated: true };
|
|
360
|
+
}
|
|
361
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
362
|
+
return this.store.reserveScheduledWake({
|
|
363
|
+
taskId: task.taskId,
|
|
364
|
+
scheduleId: input.scheduleId,
|
|
365
|
+
occurrenceId: input.occurrenceId,
|
|
366
|
+
effectId: id,
|
|
367
|
+
prompt: this.renderPrompt(task, id, 'continuation', `scheduled wake ${input.scheduleId} occurrence ${input.occurrenceId}`),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
/** Read-only stall classification for one task; no lifecycle authority. */
|
|
371
|
+
taskStall(taskId: string): ReturnType<WorkflowSupervisorStore['taskStall']> { return this.store.taskStall(this.requireTask(taskId).taskId); }
|
|
372
|
+
/**
|
|
373
|
+
* Explicit operator move of one task's exact conversation. Chatting in a new
|
|
374
|
+
* conversation is never an automatic answer to provider backpressure: a 429 is
|
|
375
|
+
* answered by fewer requests, and only a conversation that is durably unusable
|
|
376
|
+
* is replaced. Requires the caller's expected current conversation so a stale
|
|
377
|
+
* decision cannot silently move a task that already advanced.
|
|
378
|
+
*/
|
|
379
|
+
migrateConversation(input: { taskId: string; expectedConversationId: string; conversationId?: string; conversationUrl?: string; fresh?: boolean; requestId: string; reason: string; authorizedBy?: string }): { taskId: string; conversationId: string; conversationUrl: string; migrated: boolean; freshConversation?: boolean } {
|
|
380
|
+
const task = this.requireTask(input.taskId);
|
|
381
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
382
|
+
if (!input.requestId.trim() || !input.reason.trim()) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_REASON_REQUIRED');
|
|
383
|
+
if (input.fresh === true) {
|
|
384
|
+
if (input.conversationId?.trim() || input.conversationUrl?.trim()) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_FRESH_CONVERSATION_CONFLICT');
|
|
385
|
+
const origin = `fresh-conversation:${task.taskId}:${input.requestId}`;
|
|
386
|
+
const effectId = stableEffectId(origin);
|
|
387
|
+
const migrated = this.store.migrateToFreshConversation({
|
|
388
|
+
taskId: task.taskId,
|
|
389
|
+
expectedConversationId: input.expectedConversationId,
|
|
390
|
+
requestId: input.requestId,
|
|
391
|
+
reason: input.reason,
|
|
392
|
+
authorizedBy: input.authorizedBy?.trim() || 'operator',
|
|
393
|
+
// A fresh conversation has no prior turns, so the replacement turn must
|
|
394
|
+
// carry the objective again instead of the bare "继续。" continuation.
|
|
395
|
+
replacement: { effectId, prompt: this.renderPrompt(task, effectId, 'enrollment') },
|
|
396
|
+
});
|
|
397
|
+
return { taskId: migrated.task.taskId, conversationId: migrated.task.conversationId, conversationUrl: migrated.task.conversationUrl, migrated: migrated.migrated, freshConversation: migrated.migrated };
|
|
398
|
+
}
|
|
399
|
+
if (!input.conversationId?.trim() || !input.conversationUrl?.trim()) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_CONVERSATION_REQUIRED');
|
|
400
|
+
const identity = parseChatgptConversationIdentity(input.conversationUrl);
|
|
401
|
+
if (identity.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_CONVERSATION_MISMATCH');
|
|
402
|
+
const migrated = this.store.migrateConversation({
|
|
403
|
+
taskId: task.taskId,
|
|
404
|
+
expectedConversationId: input.expectedConversationId,
|
|
405
|
+
conversationId: identity.conversationId,
|
|
406
|
+
conversationUrl: identity.canonicalUrl,
|
|
407
|
+
requestId: input.requestId,
|
|
408
|
+
reason: input.reason,
|
|
409
|
+
authorizedBy: input.authorizedBy?.trim() || 'operator',
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
taskId: migrated.task.taskId, conversationId: migrated.task.conversationId,
|
|
413
|
+
conversationUrl: migrated.task.conversationUrl, migrated: migrated.migrated,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/** Mechanical provider re-dispatch budget for one effect, used by recovery to surface exhaustion. */
|
|
417
|
+
effectDispatchBudget(effectId: string): ReturnType<WorkflowSupervisorStore['effectDispatchBudget']> {
|
|
418
|
+
return this.store.effectDispatchBudget(validateEffectId(effectId));
|
|
419
|
+
}
|
|
420
|
+
continuationProof(input: { repoId?: string; activeReleaseId: string; notBefore: string }) { return this.store.continuationProof(input); }
|
|
421
|
+
browserDiscoverySnapshot() { return this.store.discoverySnapshot(); }
|
|
422
|
+
bootstrapProjectUrl(taskId: string): string {
|
|
423
|
+
const task = this.requireTask(taskId);
|
|
424
|
+
const scope = this.hooks.projectScopeForTask?.(task);
|
|
425
|
+
const title = scope?.title.trim();
|
|
426
|
+
if (!scope || !title) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_PROJECT_SCOPE_REQUIRED');
|
|
427
|
+
const projects = new Map<string, string>();
|
|
428
|
+
for (const conversation of this.store.discoverySnapshot().conversations) {
|
|
429
|
+
if (!projectMatchesScope(conversation, scope)) continue;
|
|
430
|
+
const value = conversation.projectUrl?.trim();
|
|
431
|
+
if (!value) continue;
|
|
432
|
+
try {
|
|
433
|
+
const parsed = new URL(value);
|
|
434
|
+
if (parsed.protocol !== 'https:' || parsed.hostname !== 'chatgpt.com') continue;
|
|
435
|
+
const match = /^\/g\/(g-p-[a-z0-9]+)(?:-[^/]+)?\/project\/?$/i.exec(parsed.pathname);
|
|
436
|
+
if (!match) continue;
|
|
437
|
+
const projectId = match[1]!.toLocaleLowerCase();
|
|
438
|
+
projects.set(projectId, `https://chatgpt.com/g/${match[1]!}/project`);
|
|
439
|
+
} catch {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (projects.size === 0) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_PROJECT_NOT_DISCOVERED');
|
|
444
|
+
if (projects.size !== 1) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_PROJECT_AMBIGUOUS');
|
|
445
|
+
return [...projects.values()][0]!;
|
|
446
|
+
}
|
|
447
|
+
recordBrowserDiscovery(source: string, conversations: readonly WorkflowSupervisorDiscoveredConversation[]) {
|
|
448
|
+
// Discovery is durable observation only. Creating a Supervisor task/effect
|
|
449
|
+
// requires the explicit Work/current-conversation enrollment path.
|
|
450
|
+
return this.store.recordDiscovery(source, conversations);
|
|
451
|
+
}
|
|
452
|
+
browserProjectScopes(): WorkflowSupervisorProjectScope[] {
|
|
453
|
+
const scopes = new Map<string, WorkflowSupervisorProjectScope>();
|
|
454
|
+
for (const task of this.store.listTasks()) {
|
|
455
|
+
const scope = this.hooks.projectScopeForTask?.(task);
|
|
456
|
+
if (!scope?.title.trim()) continue;
|
|
457
|
+
const normalized: WorkflowSupervisorProjectScope = {
|
|
458
|
+
title: scope.title.trim().slice(0, 512),
|
|
459
|
+
...(scope.aliases?.length ? { aliases: [...new Set(scope.aliases.map((value) => value.trim().slice(0, 256)).filter(Boolean))].slice(0, 16) } : {}),
|
|
460
|
+
...(scope.repoId?.trim() ? { repoId: scope.repoId.trim().slice(0, 256) } : {}),
|
|
461
|
+
...(scope.controllerHome?.trim() ? { controllerHome: scope.controllerHome.trim().slice(0, 2048) } : {}),
|
|
462
|
+
};
|
|
463
|
+
const key = `${normalized.title.toLocaleLowerCase()}\n${normalized.repoId ?? ''}\n${normalized.controllerHome ?? ''}`;
|
|
464
|
+
scopes.set(key, normalized);
|
|
465
|
+
}
|
|
466
|
+
return [...scopes.values()];
|
|
467
|
+
}
|
|
468
|
+
browserTasks(): WorkflowSupervisorBrowserTask[] {
|
|
469
|
+
const active = this.store.listTasks().flatMap((task) => {
|
|
470
|
+
if (this.store.terminalAction(task.taskId)) return [];
|
|
471
|
+
// A Computer page is an ephemeral delivery surface. An applied effect
|
|
472
|
+
// awaiting its receipt remains eligible for read-only provider-health
|
|
473
|
+
// observation, so a terminal stream error can reserve its one bounded
|
|
474
|
+
// recovery effect. Rendered assistant text never decides the workflow;
|
|
475
|
+
// only a persisted receipt does that.
|
|
476
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
477
|
+
// Any applied effect awaiting its receipt stays observable, including one
|
|
478
|
+
// whose bounded provider resume is already exhausted. Observation is
|
|
479
|
+
// read-only and mints nothing, so keeping it attached is what lets late
|
|
480
|
+
// provider/completion evidence resolve the turn instead of stranding it.
|
|
481
|
+
const providerTurnAwaitingReceipt = this.store.hasAppliedEffectAwaitingCompletion(task.taskId);
|
|
482
|
+
if (!pending && !providerTurnAwaitingReceipt) return [];
|
|
483
|
+
// Bootstrap has no exact conversation yet, so it cannot satisfy the
|
|
484
|
+
// normal Work-boundary predicate. Its already-persisted enrollment effect
|
|
485
|
+
// is the narrow authority to acquire one through Computer exactly once.
|
|
486
|
+
if (!task.conversationId.startsWith('bootstrap:')) {
|
|
487
|
+
// Optional lower-layer Work/Requirement compatibility must be task-local.
|
|
488
|
+
// A stale legacy task is allowed to fail closed, but it must never poison
|
|
489
|
+
// the global Supervisor delivery queue and block standalone tasks.
|
|
490
|
+
try { if (pending?.mode === 'send' && !this.browserTaskActive(task)) return []; }
|
|
491
|
+
catch { return []; }
|
|
492
|
+
}
|
|
493
|
+
return [{ task, mode: pending?.mode ?? 'reconcile' }];
|
|
494
|
+
});
|
|
495
|
+
// Fresh effects are bounded work with no prior external mutation. Outcome-
|
|
496
|
+
// unknown reconciliation remains durable and exactly-once, but cannot be
|
|
497
|
+
// allowed to monopolize the single native browser lane indefinitely.
|
|
498
|
+
active.sort((left, right) => {
|
|
499
|
+
const leftPriority = left.mode === 'send' ? 0 : 1;
|
|
500
|
+
const rightPriority = right.mode === 'send' ? 0 : 1;
|
|
501
|
+
return leftPriority - rightPriority;
|
|
502
|
+
});
|
|
503
|
+
return active.map(({ task }) => browserTask(task));
|
|
504
|
+
}
|
|
505
|
+
browserPoll(input: { conversationId: string; conversationUrl: string }): WorkflowSupervisorBrowserPollResult {
|
|
506
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
507
|
+
const projection = browserTask(task);
|
|
508
|
+
const terminal = this.store.terminalAction(task.taskId);
|
|
509
|
+
// STOPPED is an operator terminal, not a semantic completion. Return it
|
|
510
|
+
// before the normal external-effect activity gate so an already-running
|
|
511
|
+
// browser poll observes the stop and cannot turn it into another command.
|
|
512
|
+
if (terminal === 'STOPPED') return { authorized: true, task: projection, terminal };
|
|
513
|
+
if (terminal) return { authorized: true, task: projection, terminal };
|
|
514
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
515
|
+
// Read-only reconciliation belongs to the already-started effect even when
|
|
516
|
+
// its Work/conversation binding was retired. It never authorizes a resend.
|
|
517
|
+
if (pending?.mode === 'send' && !this.browserTaskActive(task)) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_TASK_INACTIVE');
|
|
518
|
+
if (!pending) return { authorized: true, task: projection };
|
|
519
|
+
return { authorized: true, task: projection, command: { mode: pending.mode, effectId: pending.effect.effectId, kind: pending.effect.kind, prompt: pending.effect.prompt, dispatchGeneration: pending.generation, conversationId: task.conversationId, conversationUrl: task.conversationUrl } };
|
|
520
|
+
}
|
|
521
|
+
bootstrapPoll(taskId: string): WorkflowSupervisorBrowserPollResult {
|
|
522
|
+
const task = this.requireTask(taskId);
|
|
523
|
+
if (!task.conversationId.startsWith('bootstrap:')) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_PENDING');
|
|
524
|
+
const terminal = this.store.terminalAction(task.taskId);
|
|
525
|
+
if (terminal === 'STOPPED') return { authorized: true, task: browserTask(task), terminal };
|
|
526
|
+
// While identity is still bootstrap, reconcile the latest applied leaf
|
|
527
|
+
// before dispatching any successor. An unstarted recovery child must not
|
|
528
|
+
// send from a Project/new-composer surface while the source conversation
|
|
529
|
+
// is still unbound. The existing effect ledger owns observation cooldown.
|
|
530
|
+
const applied = this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
|
|
531
|
+
if (applied) {
|
|
532
|
+
if (!this.store.bootstrapAppliedReconcileDue(applied.effectId)) {
|
|
533
|
+
return { authorized: true, task: browserTask(task) };
|
|
534
|
+
}
|
|
535
|
+
const dispatch = this.store.latestEffectDispatch(applied.effectId);
|
|
536
|
+
return {
|
|
537
|
+
authorized: true,
|
|
538
|
+
task: browserTask(task),
|
|
539
|
+
command: {
|
|
540
|
+
mode: 'reconcile',
|
|
541
|
+
effectId: applied.effectId,
|
|
542
|
+
kind: applied.kind,
|
|
543
|
+
prompt: applied.prompt,
|
|
544
|
+
dispatchGeneration: dispatch?.generation ?? 1,
|
|
545
|
+
conversationId: task.conversationId,
|
|
546
|
+
conversationUrl: task.conversationUrl,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
551
|
+
if (!pending) return { authorized: true, task: browserTask(task) };
|
|
552
|
+
return {
|
|
553
|
+
authorized: true,
|
|
554
|
+
task: browserTask(task),
|
|
555
|
+
command: {
|
|
556
|
+
mode: pending.mode,
|
|
557
|
+
effectId: pending.effect.effectId,
|
|
558
|
+
kind: pending.effect.kind,
|
|
559
|
+
prompt: pending.effect.prompt,
|
|
560
|
+
dispatchGeneration: pending.generation,
|
|
561
|
+
conversationId: task.conversationId,
|
|
562
|
+
conversationUrl: task.conversationUrl,
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
bootstrapBeginEffect(input: { taskId: string; effectId: string; dispatchId: string; dispatchGeneration: number }): boolean {
|
|
567
|
+
const task = this.requireTask(input.taskId);
|
|
568
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
569
|
+
if (!task.conversationId.startsWith('bootstrap:')) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_PENDING');
|
|
570
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
571
|
+
if (!pending || pending.effect.effectId !== validateEffectId(input.effectId) || pending.mode !== 'send') return false;
|
|
572
|
+
return this.store.recordEffectDispatchStarted(input.effectId, input.dispatchGeneration, input.dispatchId, {
|
|
573
|
+
surface: 'computer-bootstrap',
|
|
574
|
+
...(this.hooks.effectDispatchEvidence?.({ task, effectId: input.effectId }) ?? {}),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
bootstrapObserveEffect(input: { taskId: string; effectId: string; observationId: string; outcome: 'applied' | 'not_applied' | 'unknown'; evidence?: Record<string, unknown> }): void {
|
|
578
|
+
const task = this.requireTask(input.taskId);
|
|
579
|
+
const effect = this.store.getEffect(validateEffectId(input.effectId));
|
|
580
|
+
if (!effect || effect.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_EFFECT_TASK_MISMATCH');
|
|
581
|
+
if (input.outcome === 'not_applied') {
|
|
582
|
+
// This path is private to the provider dispatch adapter after its exact
|
|
583
|
+
// dispatch function returned without clicking Send. It is therefore a
|
|
584
|
+
// mechanical negative proof, unlike a missing post-send conversation
|
|
585
|
+
// marker, which remains outcome-unknown and must reconcile.
|
|
586
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
587
|
+
const dispatch = this.store.latestEffectDispatch(effect.effectId);
|
|
588
|
+
if (!pending || pending.effect.effectId !== effect.effectId || pending.mode !== 'reconcile'
|
|
589
|
+
|| !dispatch || pending.generation !== dispatch.generation
|
|
590
|
+
|| dispatch.evidence.surface !== 'computer-bootstrap'
|
|
591
|
+
|| input.evidence?.dispatch_mutation !== 'not_attempted'
|
|
592
|
+
|| input.evidence?.send_clicked !== false) {
|
|
593
|
+
throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_APPLIED_PROOF_INVALID');
|
|
594
|
+
}
|
|
595
|
+
this.store.recordEffectNotAppliedProof(effect.effectId, input.observationId, {
|
|
596
|
+
surface: 'computer-bootstrap', send_clicked: false,
|
|
597
|
+
dispatch_generation: dispatch.generation,
|
|
598
|
+
pre_send_rejection: true,
|
|
599
|
+
attestation: 'local_computer_dispatch_returned_without_send',
|
|
600
|
+
});
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const evidence = { surface: 'computer-bootstrap', ...(input.evidence ?? {}) };
|
|
604
|
+
this.observeEffect({ effectId: effect.effectId, observationId: input.observationId, outcome: input.outcome, evidence });
|
|
605
|
+
if (input.outcome === 'applied') this.hooks.effectApplied?.(task, effect, { observationId: input.observationId, evidence });
|
|
606
|
+
}
|
|
607
|
+
/** Persist a bounded preflight rejection; started generations require an exact no-click proof first. */
|
|
608
|
+
recordProviderPreparationFailure(input: { taskId: string; effectId: string; reasonCode: string; composerObservation?: WorkflowSupervisorPreflightComposerObservation }): void {
|
|
609
|
+
const task = this.requireTask(input.taskId);
|
|
610
|
+
const effect = this.store.getEffect(input.effectId);
|
|
611
|
+
if (effect?.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_EFFECT_TASK_MISMATCH');
|
|
612
|
+
this.store.recordEffectPreparationFailure(input.effectId, input.reasonCode, input.composerObservation);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
browserBeginEffect(input: { conversationId: string; conversationUrl: string; effectId: string; dispatchId: string; dispatchGeneration: number; evidence?: Record<string, unknown> }): { started: boolean; mode: 'send' | 'reconcile'; generation: number } {
|
|
616
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
617
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
618
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
619
|
+
const effectId = validateEffectId(input.effectId);
|
|
620
|
+
if (!pending || pending.effect.effectId !== effectId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_EFFECT_NOT_CURRENT');
|
|
621
|
+
if (pending.mode !== 'send' || pending.generation !== input.dispatchGeneration) return { started: false, mode: 'reconcile', generation: pending.generation };
|
|
622
|
+
// Re-check immediately before committing dispatch; a queued browser command
|
|
623
|
+
// must not outlive an explicit conversation rebind.
|
|
624
|
+
if (!this.browserTaskActive(task)) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_TASK_INACTIVE');
|
|
625
|
+
const snapshot = browserSnapshot(input.evidence);
|
|
626
|
+
if (!snapshot || browserTextHasEffect(snapshot.latestUserText, effectId) || !this.browserSnapshotMatchesSource(task, pending.effect, snapshot)) {
|
|
627
|
+
return { started: false, mode: 'reconcile', generation: pending.generation };
|
|
628
|
+
}
|
|
629
|
+
const dispatchEvidence = {
|
|
630
|
+
surface: typeof input.evidence?.surface === 'string' ? input.evidence.surface.slice(0, 128) : 'chrome-extension',
|
|
631
|
+
baseline_user_sha256: browserTextSha256(snapshot.latestUserText),
|
|
632
|
+
baseline_assistant_sha256: sha256(snapshot.latestAssistantResponse),
|
|
633
|
+
baseline_has_source_completion: Boolean(pending.effect.sourceCompletionFingerprint),
|
|
634
|
+
...(this.hooks.effectDispatchEvidence?.({ task, effectId }) ?? {}),
|
|
635
|
+
};
|
|
636
|
+
const started = this.store.recordEffectDispatchStarted(effectId, input.dispatchGeneration, input.dispatchId, dispatchEvidence);
|
|
637
|
+
return { started, mode: started ? 'send' : 'reconcile', generation: input.dispatchGeneration };
|
|
638
|
+
}
|
|
639
|
+
browserObserveEffect(input: { conversationId: string; conversationUrl: string; effectId: string; observationId: string; outcome: 'applied' | 'not_applied' | 'unknown'; evidence?: Record<string, unknown> }): { recorded: true } {
|
|
640
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
641
|
+
const effect = this.store.getEffect(validateEffectId(input.effectId));
|
|
642
|
+
if (!effect || effect.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_EFFECT_TASK_MISMATCH');
|
|
643
|
+
if (input.outcome !== 'not_applied') {
|
|
644
|
+
const evidence = sanitizeBrowserEvidence(input.evidence);
|
|
645
|
+
this.observeEffect({ effectId: effect.effectId, observationId: input.observationId, outcome: input.outcome, evidence });
|
|
646
|
+
if (input.outcome === 'applied') this.hooks.effectApplied?.(task, effect, { observationId: input.observationId, evidence });
|
|
647
|
+
return { recorded: true };
|
|
648
|
+
}
|
|
649
|
+
// A rendered page can still be stale, including an empty composer and the
|
|
650
|
+
// exact pre-send history. Absence in that page is never proof that the
|
|
651
|
+
// provider rejected a submission. Only the dispatch owner can attest a
|
|
652
|
+
// mechanical pre-send failure (bootstrapObserveEffect); browser observers
|
|
653
|
+
// cannot authorize re-dispatch of an outcome-unknown mutation.
|
|
654
|
+
this.store.recordEffectObservation(effect.effectId, input.observationId, 'unknown', {
|
|
655
|
+
...sanitizeBrowserEvidence(input.evidence), reconciliation: true, reason: 'not_applied_proof_incomplete',
|
|
656
|
+
});
|
|
657
|
+
return { recorded: true };
|
|
658
|
+
}
|
|
659
|
+
/** Dispatch-owner attestation. Only the adapter that reserved this generation may call it. */
|
|
660
|
+
browserObserveDispatchFailure(input: { conversationId: string; conversationUrl: string; effectId: string; observationId: string; dispatchGeneration: number; reason: string; surface?: string }): void {
|
|
661
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
662
|
+
const effect = this.store.getEffect(input.effectId);
|
|
663
|
+
if (effect?.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_EFFECT_TASK_MISMATCH');
|
|
664
|
+
const dispatch = this.store.latestEffectDispatch(input.effectId);
|
|
665
|
+
if (dispatch?.generation !== input.dispatchGeneration) throw new Error('WORKFLOW_SUPERVISOR_DISPATCH_GENERATION_CHANGED');
|
|
666
|
+
this.store.recordEffectNotAppliedProof(input.effectId, input.observationId, {
|
|
667
|
+
surface: input.surface?.trim().slice(0, 128) || 'macos-native', reason: input.reason, send_clicked: false, dispatch_generation: input.dispatchGeneration,
|
|
668
|
+
// This call is the dispatch owner's exact mechanical return before any
|
|
669
|
+
// Send click. The store refunds this generation once; the existing preflight
|
|
670
|
+
// gate bounds ordinary repeats without spending actual send budget.
|
|
671
|
+
pre_send_rejection: true,
|
|
672
|
+
});
|
|
673
|
+
const recoveryStopPending = effect.kind === 'recovery' && input.reason.trim() === 'provider_recovery_stop_pending';
|
|
674
|
+
if (!recoveryStopPending) {
|
|
675
|
+
this.recordProviderPreparationFailure({ taskId: task.taskId, effectId: effect.effectId, reasonCode: input.reason });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
browserObserveProviderTurn(input: { conversationId: string; conversationUrl: string; generating: boolean; latestAssistantResponse: string; providerActivityText?: string; providerFailureCode?: string; receiptMissing?: boolean; observedAtMs: number; graceMs: number }): { state: 'inactive' | 'none' | 'generating' | 'idle_pending' | 'recovery_reserved' | 'exhausted'; recoveryEffect?: WorkflowSupervisorEffect } {
|
|
679
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
680
|
+
if (!this.browserTaskActiveForExternalEffect(task)) return { state: 'inactive' };
|
|
681
|
+
// Exhaustion stops new recovery mutations, not read-only observation of the exhausted applied leaf.
|
|
682
|
+
let sourceEffect = this.store.latestAppliedEffectWithoutCompletion(task.taskId);
|
|
683
|
+
if (!sourceEffect) {
|
|
684
|
+
const exhaustedLeaf = this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
|
|
685
|
+
if (exhaustedLeaf && this.store.providerResumeExhausted(exhaustedLeaf.effectId)) sourceEffect = exhaustedLeaf;
|
|
686
|
+
}
|
|
687
|
+
if (!sourceEffect) return { state: 'none' };
|
|
688
|
+
const recoveryId = stableEffectId(`provider-recovery:${sourceEffect.effectId}`);
|
|
689
|
+
const providerFailureCode = input.providerFailureCode?.trim();
|
|
690
|
+
const recoveryReason = providerFailureCode
|
|
691
|
+
? `Applied Supervisor effect ${sourceEffect.effectId} ended with provider failure ${providerFailureCode} before a committed Supervisor completion. Resume from durable Forge state; the source effect remains applied and must not be replayed.`
|
|
692
|
+
: `Applied Supervisor effect ${sourceEffect.effectId} stopped making observable provider progress without a committed Supervisor completion. Resume from durable Forge state; the source effect remains applied and must not be replayed.`;
|
|
693
|
+
return this.store.observeProviderTurn({
|
|
694
|
+
taskId: task.taskId,
|
|
695
|
+
effectId: sourceEffect.effectId,
|
|
696
|
+
generating: input.generating,
|
|
697
|
+
receiptMissing: input.receiptMissing === true,
|
|
698
|
+
assistantDigest: sha256(`${input.latestAssistantResponse}\n${input.providerActivityText ?? ''}`),
|
|
699
|
+
providerFailureCode,
|
|
700
|
+
observedAtMs: input.observedAtMs,
|
|
701
|
+
graceMs: input.graceMs,
|
|
702
|
+
recovery: { effectId: recoveryId, prompt: this.renderPrompt(task, recoveryId, 'recovery', undefined, recoveryReason) },
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
async browserObserveAssistant(input: { conversationId: string; conversationUrl: string; responseText: string }): Promise<WorkflowAssistantObservationResult> {
|
|
706
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
707
|
+
return await this.observeAssistantTurn({ taskId: task.taskId, conversationId: task.conversationId, responseText: input.responseText });
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Repair the crash boundary where a CONTINUE completion was durably committed
|
|
712
|
+
* before successor reservation finished in a previous Runtime. This never
|
|
713
|
+
* replays the provider response; it only reads the committed causal receipt and
|
|
714
|
+
* reserves the exactly-once successor effect.
|
|
715
|
+
*/
|
|
716
|
+
async reconcileCommittedContinuations(limit = 16): Promise<{ scanned: number; reconciled: number }> {
|
|
717
|
+
const completions = this.store.listContinueCompletionsAwaitingSuccessor(limit);
|
|
718
|
+
let reconciled = 0;
|
|
719
|
+
for (const completion of completions) {
|
|
720
|
+
const task = this.store.getTask(completion.taskId);
|
|
721
|
+
if (!task || this.store.terminalAction(task.taskId)) continue;
|
|
722
|
+
const committed = this.reserveContinuation(task, completion);
|
|
723
|
+
if (committed.successorEffect) reconciled += 1;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// A non-terminal Supervisor task must always have one derivable causal
|
|
727
|
+
// obligation: an unapplied effect, an applied effect awaiting completion,
|
|
728
|
+
// or a committed completion whose successor/terminal resolution can be
|
|
729
|
+
// reconstructed from durable facts. Registration is intentionally a
|
|
730
|
+
// separate transaction from effect reservation, so a Runtime crash between
|
|
731
|
+
// them must converge here instead of leaving an "active" inert task.
|
|
732
|
+
const inertTasks = this.store.listTasksWithoutCausalObligation(limit);
|
|
733
|
+
for (const task of inertTasks) {
|
|
734
|
+
if (this.store.terminalAction(task.taskId)) continue;
|
|
735
|
+
const latest = this.store.getLatestCompletion(task.taskId);
|
|
736
|
+
if (!latest) {
|
|
737
|
+
this.reserveEnrollment(task.taskId);
|
|
738
|
+
reconciled += 1;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (latest.action === 'CONTINUE') {
|
|
742
|
+
const committed = this.reserveContinuation(task, latest);
|
|
743
|
+
if (committed.successorEffect) reconciled += 1;
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (latest.action === 'WAIT') continue;
|
|
747
|
+
const validation = await this.validateTerminalProposal(task, latest.proposal);
|
|
748
|
+
const correctionId = validation.valid ? undefined : stableEffectId(`completion:${latest.completionFingerprint}`);
|
|
749
|
+
this.store.resolveTerminal({
|
|
750
|
+
completionFingerprint: latest.completionFingerprint,
|
|
751
|
+
taskId: task.taskId,
|
|
752
|
+
action: latest.action,
|
|
753
|
+
accepted: validation.valid,
|
|
754
|
+
reason: validation.reason,
|
|
755
|
+
...(correctionId ? {
|
|
756
|
+
correction: {
|
|
757
|
+
effectId: correctionId,
|
|
758
|
+
prompt: this.renderPrompt(task, correctionId, 'correction', latest.proposal.checkpoint, validation.reason),
|
|
759
|
+
},
|
|
760
|
+
} : {}),
|
|
761
|
+
});
|
|
762
|
+
reconciled += 1;
|
|
763
|
+
}
|
|
764
|
+
return { scanned: completions.length + inertTasks.length, reconciled };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async observeAssistantTurn(input: WorkflowAssistantObservation): Promise<WorkflowAssistantObservationResult> {
|
|
768
|
+
const task = this.requireTask(input.taskId);
|
|
769
|
+
if (task.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_CONVERSATION_MISMATCH');
|
|
770
|
+
const responseSha256 = sha256(input.responseText);
|
|
771
|
+
const expectedEffect = this.store.latestAppliedEffectWithoutCompletion(task.taskId)
|
|
772
|
+
?? this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
|
|
773
|
+
// A live applied effect always wins: an old compact receipt must never be
|
|
774
|
+
// allowed to satisfy a newer causal obligation. Only when no effect is
|
|
775
|
+
// awaiting completion may an exact persisted response hash recover the
|
|
776
|
+
// original effect context for idempotent duplicate observation.
|
|
777
|
+
const priorCompletion = expectedEffect ? undefined : this.store.getCompletionByResponseSha256(task.taskId, responseSha256);
|
|
778
|
+
const expectedEffectId = expectedEffect?.effectId ?? priorCompletion?.sourceEffectId;
|
|
779
|
+
const receiptEffect = expectedEffect ?? (expectedEffectId ? this.store.getEffect(expectedEffectId) : undefined);
|
|
780
|
+
const parsed = parseSupervisorCompletion(
|
|
781
|
+
input.responseText,
|
|
782
|
+
expectedEffectId ? { task, effectId: expectedEffectId, prompt: receiptEffect?.prompt } : undefined,
|
|
783
|
+
);
|
|
784
|
+
const sourceEffect = this.store.getEffect(parsed.proposal.sourceEffectId);
|
|
785
|
+
if (!sourceEffect || sourceEffect.taskId !== task.taskId || !this.store.effectApplied(sourceEffect.effectId)) throw new Error('WORKFLOW_SUPERVISOR_CAUSAL_EFFECT_NOT_APPLIED');
|
|
786
|
+
const explicitIdentityProtocol = sourceEffect.prompt.includes('conversation_id=') && sourceEffect.prompt.includes('task_id=') && sourceEffect.prompt.includes('supervisor_state');
|
|
787
|
+
if (parsed.proposal.conversationId && parsed.proposal.conversationId !== task.conversationId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_CONVERSATION_MISMATCH');
|
|
788
|
+
if (parsed.proposal.taskId && parsed.proposal.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_TASK_MISMATCH');
|
|
789
|
+
if (explicitIdentityProtocol && !parsed.proposal.conversationId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_CONVERSATION_ID_REQUIRED');
|
|
790
|
+
if (explicitIdentityProtocol && !parsed.proposal.taskId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_TASK_ID_REQUIRED');
|
|
791
|
+
if (explicitIdentityProtocol && !parsed.proposal.supervisorState) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_STATE_REQUIRED');
|
|
792
|
+
if (explicitIdentityProtocol && !parsed.proposal.activeScope) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_ACTIVE_SCOPE_REQUIRED');
|
|
793
|
+
const expectedScope = typeof task.completionContract.requirement_id === 'string' && task.completionContract.requirement_id.trim()
|
|
794
|
+
? `requirement:${task.completionContract.requirement_id.trim()}`
|
|
795
|
+
: typeof task.continuationPolicy.active_scope === 'string' && task.continuationPolicy.active_scope.trim()
|
|
796
|
+
? task.continuationPolicy.active_scope.trim()
|
|
797
|
+
: undefined;
|
|
798
|
+
if (expectedScope && parsed.proposal.activeScope && parsed.proposal.activeScope !== expectedScope) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_ACTIVE_SCOPE_MISMATCH');
|
|
799
|
+
const controlBlockSha256 = sha256(parsed.controlBlock);
|
|
800
|
+
const completionFingerprint = sha256(jsonIdentity(task.taskId, task.conversationId, parsed.proposal.sourceEffectId, responseSha256, controlBlockSha256));
|
|
801
|
+
const completion: WorkflowSupervisorCompletion = { completionFingerprint, taskId: task.taskId, sourceEffectId: parsed.proposal.sourceEffectId, action: parsed.proposal.action, responseSha256, controlBlockSha256, proposal: parsed.proposal, committedAt: new Date().toISOString() };
|
|
802
|
+
const terminal = this.store.terminalAction(task.taskId);
|
|
803
|
+
if (terminal) throw new Error(`WORKFLOW_SUPERVISOR_TASK_TERMINAL:${terminal}`);
|
|
804
|
+
|
|
805
|
+
if (parsed.proposal.action === 'CONTINUE') {
|
|
806
|
+
const committed = this.reserveContinuation(task, completion);
|
|
807
|
+
return { action: 'CONTINUE', completionFingerprint, terminal: false, successorEffect: committed.successorEffect!, deduplicated: committed.deduplicated };
|
|
808
|
+
}
|
|
809
|
+
if (parsed.proposal.action === 'WAIT') {
|
|
810
|
+
const committed = this.store.commitCompletion(completion);
|
|
811
|
+
return { action: 'WAIT', completionFingerprint, terminal: false, deduplicated: committed.deduplicated };
|
|
812
|
+
}
|
|
813
|
+
const committed = this.store.commitCompletion(completion);
|
|
814
|
+
|
|
815
|
+
const validation = await this.validateTerminalProposal(task, parsed.proposal);
|
|
816
|
+
const correctionId = validation.valid ? undefined : stableEffectId(`completion:${completionFingerprint}`);
|
|
817
|
+
const resolved = this.store.resolveTerminal({ completionFingerprint, taskId: task.taskId, action: parsed.proposal.action, accepted: validation.valid, reason: validation.reason,
|
|
818
|
+
...(correctionId ? { correction: { effectId: correctionId, prompt: this.renderPrompt(task, correctionId, 'correction', parsed.proposal.reason === 'compact_receipt' ? undefined : parsed.proposal.checkpoint, validation.reason) } } : {}) });
|
|
819
|
+
return { action: parsed.proposal.action, completionFingerprint, terminal: validation.valid, ...(resolved.successorEffect ? { successorEffect: resolved.successorEffect } : {}), validation, deduplicated: committed.deduplicated || resolved.deduplicated };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** Model CONTINUE and its causal successor commit in the same authority transaction. */
|
|
823
|
+
private reserveContinuation(task: WorkflowSupervisorTask, completion: WorkflowSupervisorCompletion) {
|
|
824
|
+
const nextId = stableEffectId(`completion:${completion.completionFingerprint}`);
|
|
825
|
+
const checkpoint = completion.proposal.reason === 'compact_receipt' ? undefined : completion.proposal.checkpoint;
|
|
826
|
+
return this.store.commitCompletion(completion, {
|
|
827
|
+
effectId: nextId,
|
|
828
|
+
kind: 'continuation',
|
|
829
|
+
prompt: this.renderPrompt(task, nextId, 'continuation', checkpoint),
|
|
830
|
+
// Rebuild from the same durable goal in a new logical conversation after
|
|
831
|
+
// a complete 15-receipt cycle; never migrate an uncompleted mutation.
|
|
832
|
+
...(task.continuationPolicy.kind === 'standalone_supervisor'
|
|
833
|
+
? { freshEnrollmentPrompt: this.renderPrompt(task, nextId, 'enrollment') } : {}),
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
private async validateTerminalProposal(
|
|
838
|
+
task: WorkflowSupervisorTask,
|
|
839
|
+
proposal: WorkflowSupervisorCompletion['proposal'],
|
|
840
|
+
): Promise<WorkflowContractValidation> {
|
|
841
|
+
const validator = proposal.action === 'DONE' ? this.validators.completionContract : this.validators.userBlockerPolicy;
|
|
842
|
+
const validation = await validator(task, proposal);
|
|
843
|
+
if (typeof task.continuationPolicy.scheduled_wake_schedule_id !== 'string') return validation;
|
|
844
|
+
if (proposal.action === 'DONE') {
|
|
845
|
+
return {
|
|
846
|
+
...validation,
|
|
847
|
+
valid: false,
|
|
848
|
+
reason: validation.valid ? 'scheduled_work_must_wait' : validation.reason,
|
|
849
|
+
evidence: [...(validation.evidence ?? []), 'A Work-bound recurring Schedule owns the next run; use WAIT instead of terminal DONE.'],
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
if (proposal.action === 'NEEDS_USER') {
|
|
853
|
+
return {
|
|
854
|
+
...validation,
|
|
855
|
+
valid: false,
|
|
856
|
+
reason: validation.valid ? 'scheduled_work_must_remain_open' : validation.reason,
|
|
857
|
+
evidence: [...(validation.evidence ?? []), 'Persist the exact user blocker and use WAIT; a recurring scheduled Work must remain open.'],
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
return validation;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private renderPrompt(
|
|
864
|
+
task: WorkflowSupervisorTask,
|
|
865
|
+
effectId: string,
|
|
866
|
+
kind: WorkflowEffectKind,
|
|
867
|
+
checkpoint?: string,
|
|
868
|
+
correctionReason?: string,
|
|
869
|
+
lowerLayerContext?: string,
|
|
870
|
+
): string {
|
|
871
|
+
const canonicalObjective = this.hooks.canonicalObjectiveForTask?.(task)?.trim();
|
|
872
|
+
const promptTask = canonicalObjective ? { ...task, objective: canonicalObjective } : task;
|
|
873
|
+
const cognition = this.hooks.advisoryCognitionForTask?.(promptTask);
|
|
874
|
+
return renderSupervisorPrompt(promptTask, effectId, kind, checkpoint, correctionReason, lowerLayerContext, cognition);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private browserTaskActive(task: WorkflowSupervisorTask): boolean { return this.hooks.browserTaskActive?.(task) ?? true; }
|
|
878
|
+
private browserTaskActiveForExternalEffect(task: WorkflowSupervisorTask): boolean {
|
|
879
|
+
return this.store.hasAppliedEffectAwaitingCompletion(task.taskId) || this.browserTaskActive(task);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Progress is a mechanical change in the canonical Work this turn is meant to
|
|
884
|
+
* advance, never the provider's willingness to answer again. Both endpoints of
|
|
885
|
+
* the comparison are durable facts: the Work projection captured in this
|
|
886
|
+
* effect's dispatch evidence and the current projection. A missing signal on
|
|
887
|
+
* either side is unknown, so it proves neither progress nor its absence.
|
|
888
|
+
*/
|
|
889
|
+
private automationTurnChangedNothing(task: WorkflowSupervisorTask, effectId: string): boolean {
|
|
890
|
+
const dispatched = boundedBrowserText(this.store.latestEffectDispatch(effectId)?.evidence.work_progress_fingerprint, 256);
|
|
891
|
+
const current = boundedBrowserText(this.hooks.workProgressFingerprint?.(task), 256);
|
|
892
|
+
return Boolean(dispatched && current && dispatched === current);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
private browserSnapshotMatchesSource(task: WorkflowSupervisorTask, effect: WorkflowSupervisorEffect, snapshot: { latestUserText: string; latestAssistantResponse: string }): boolean {
|
|
896
|
+
if (!effect.sourceCompletionFingerprint) return true;
|
|
897
|
+
const completion = this.store.getCompletion(effect.sourceCompletionFingerprint);
|
|
898
|
+
if (!completion || completion.taskId !== task.taskId) return false;
|
|
899
|
+
// A compact completion is committed only after observing the provider's
|
|
900
|
+
// finished assistant reply and checking its causal challenge. ChatGPT may
|
|
901
|
+
// subsequently collapse that historical reply or its original user marker;
|
|
902
|
+
// requiring the bytes to remain in the DOM would strand a valid successor.
|
|
903
|
+
// Persisted legacy tool receipts are read-only migration evidence; the MCP
|
|
904
|
+
// receipt writer has been retired. In both cases the committed completion,
|
|
905
|
+
// not a new heuristic, is the one authority for the successor.
|
|
906
|
+
const durableReceipt = completion.proposal.reason === 'compact_receipt'
|
|
907
|
+
|| completion.proposal.reason === 'automation_tool_receipt';
|
|
908
|
+
const sourceEffect = this.store.getEffect(completion.sourceEffectId);
|
|
909
|
+
if (!sourceEffect || sourceEffect.taskId !== task.taskId) return false;
|
|
910
|
+
if (durableReceipt) return true;
|
|
911
|
+
if (sha256(snapshot.latestAssistantResponse) !== completion.responseSha256) return false;
|
|
912
|
+
return normalizeBrowserText(snapshot.latestUserText) === normalizeBrowserText(sourceEffect.prompt)
|
|
913
|
+
|| browserTextHasEffect(snapshot.latestUserText, sourceEffect.effectId);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
private requireTask(taskId: string): WorkflowSupervisorTask { const task = this.store.getTask(taskId); if (!task) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN'); return task; }
|
|
917
|
+
private requireBrowserTask(conversationId: string, conversationUrl: string): WorkflowSupervisorTask {
|
|
918
|
+
const observed = parseChatgptConversationIdentity(conversationUrl);
|
|
919
|
+
if (observed.conversationId !== conversationId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_CONVERSATION_MISMATCH');
|
|
920
|
+
const task = this.store.getTaskByConversationId(conversationId);
|
|
921
|
+
if (!task) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_CONVERSATION_NOT_ENROLLED');
|
|
922
|
+
const registered = parseChatgptConversationIdentity(task.conversationUrl);
|
|
923
|
+
// Project routing is presentation metadata. A ChatGPT conversation can move
|
|
924
|
+
// between `/c/<id>` and a Project-prefixed route without becoming a different
|
|
925
|
+
// provider conversation, so the durable id is the only identity fence here.
|
|
926
|
+
if (task.conversationId !== conversationId || registered.conversationId !== conversationId || observed.conversationId !== conversationId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_CONVERSATION_MISMATCH');
|
|
927
|
+
return task;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function boundedBrowserText(value: unknown, max: number): string | undefined { return typeof value === 'string' && Buffer.byteLength(value, 'utf8') <= max ? value : undefined; }
|
|
932
|
+
/**
|
|
933
|
+
* A task that already reached DONE/NEEDS_USER can never deliver another effect.
|
|
934
|
+
* Reserving into it reported `enrolled` and left the caller waiting for a turn
|
|
935
|
+
* that the terminal action forbids.
|
|
936
|
+
*/
|
|
937
|
+
function requireNonTerminalTask(store: WorkflowSupervisorStore, taskId: string): void {
|
|
938
|
+
const terminal = store.terminalAction(taskId);
|
|
939
|
+
if (terminal) throw new Error(`WORKFLOW_SUPERVISOR_TASK_TERMINAL:${terminal}`);
|
|
940
|
+
}
|
|
941
|
+
function browserSnapshot(evidence: Record<string, unknown> | undefined): { latestUserText: string; latestAssistantResponse: string } | undefined {
|
|
942
|
+
const latestUserText = boundedBrowserText(evidence?.latest_user_text, 128 * 1024);
|
|
943
|
+
const latestAssistantResponse = boundedBrowserText(evidence?.latest_assistant_response, 512 * 1024);
|
|
944
|
+
if (latestUserText === undefined || latestAssistantResponse === undefined) return undefined;
|
|
945
|
+
return { latestUserText, latestAssistantResponse };
|
|
946
|
+
}
|
|
947
|
+
function normalizeBrowserText(value: string): string { return value.replace(/\s+/g, ' ').trim(); }
|
|
948
|
+
function browserTextSha256(value: string): string { return sha256(normalizeBrowserText(value)); }
|
|
949
|
+
function browserTextHasEffect(value: string, effectId: string): boolean { return value.includes(renderEffectMarker(effectId)); }
|
|
950
|
+
const PERSISTED_BROWSER_EVIDENCE_KEYS = new Set(['exact_user_message', 'observation_fingerprint', 'reconciliation', 'reason', 'surface', 'target_marker_present']);
|
|
951
|
+
function sanitizeBrowserEvidence(evidence: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
952
|
+
if (!evidence) return {};
|
|
953
|
+
const sanitized: Record<string, unknown> = {};
|
|
954
|
+
for (const [key, value] of Object.entries(evidence)) {
|
|
955
|
+
if (!PERSISTED_BROWSER_EVIDENCE_KEYS.has(key)) continue;
|
|
956
|
+
if (typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) sanitized[key] = value;
|
|
957
|
+
else if (typeof value === 'string' && value.length <= 512) sanitized[key] = value;
|
|
958
|
+
}
|
|
959
|
+
return sanitized;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function browserTask(task: WorkflowSupervisorTask): WorkflowSupervisorBrowserTask { return { taskId: task.taskId, conversationId: task.conversationId, conversationUrl: task.conversationUrl }; }
|
|
963
|
+
function jsonIdentity(...values: string[]): string { return JSON.stringify(values); }
|