@moretea-labs/forge 1.8.0 → 1.8.1
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 +70 -7
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +17 -2
- package/CONTRIBUTING.md +3 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/adapters/chatgpt/browser-delivery-host.ts +7 -1
- package/adapters/chatgpt/browser-delivery-runtime.ts +529 -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 +228 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
- package/adapters/computer/chatgpt-extension-target.ts +463 -0
- package/adapters/computer/chatgpt-target-router.ts +103 -0
- package/adapters/computer/chrome-extension/background.js +385 -0
- package/adapters/computer/chrome-extension/content.js +378 -0
- package/adapters/computer/chrome-extension/core.js +24 -0
- package/adapters/computer/chrome-extension/manifest.json +30 -0
- package/adapters/computer/desktop-operator-discovery.ts +11 -31
- package/adapters/computer/interaction-target-authority.ts +142 -18
- package/adapters/mcp/auth.ts +0 -1
- package/adapters/mcp/instructions.ts +5 -4
- package/adapters/mcp/multi-repository.ts +3 -19
- package/adapters/mcp/oauth.ts +73 -0
- package/adapters/mcp/paths.ts +4 -1
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
- 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 +411 -73
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -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 +8 -42
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
- package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
- 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 +18 -504
- package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
- package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
- 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 +246 -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 +263 -0
- package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
- package/adapters/mcp/server.ts +67 -23
- package/adapters/mcp/setup.ts +1 -3
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
- package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
- package/adapters/mcp/toolset-names.ts +5 -0
- package/adapters/mcp/toolset.ts +6 -5
- package/adapters/mcp/transports/http-observation.ts +365 -0
- package/adapters/mcp/transports/http.ts +154 -677
- 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/assets/plugin-registry.v1.json +6 -4
- package/assets/reference-configs/agentic-development-flow.md +25 -0
- package/assets/reference-configs/global-working-rules.md +19 -0
- package/assets/skill-version.json +2 -2
- package/docs/architecture/CURRENT.md +111 -41
- 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/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 +221 -0
- package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
- package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
- package/docs/architecture/domains/verification.md +1 -0
- package/docs/architecture/domains/workflow-engine.md +6 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/forge-plugin-management.md +2 -2
- package/docs/operations/controller-performance-and-502.md +17 -0
- package/docs/operations/controller-reliability-runbook.md +2 -2
- package/docs/operations/features.md +1 -1
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +2 -0
- package/docs/operations/releasing.md +2 -0
- package/docs/operations/standalone-disaster-recovery.md +14 -6
- package/docs/wiki/Implementation.md +1 -1
- package/package.json +25 -14
- 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 +297 -71
- 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 +77 -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/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 +153 -133
- package/packages/kernel/work/api/index.ts +3 -2
- package/packages/kernel/work/application/work-service.ts +17 -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 +25 -63
- package/packages/kernel/work/domain/types.ts +57 -51
- package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
- package/packages/kernel/work/ports/work-contract-store.ts +5 -2
- 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 +143 -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/canary-controller-semantic-wait.ts +12 -24
- package/scripts/check-mcp-compatibility.ts +48 -136
- package/scripts/check-runtime-architecture.mjs +622 -217
- package/scripts/forge-local-recovery-helper.mjs +93 -15
- package/scripts/lib/project-init-lib.sh +3 -0
- package/scripts/open-source-audit-allowlist.txt +1 -2
- package/scripts/run-bounded-command.mjs +5 -0
- package/scripts/run-governed-gate.ts +0 -1
- package/scripts/run-mobile-monitor-bridge.ts +0 -1
- package/scripts/smoke-control-plane-failure-domain.ts +94 -0
- package/scripts/smoke-mcp-http-runtime.ts +5 -0
- 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 +92 -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/verify-forge-runtime.sh +2 -1
- package/src/cli/agent-jobs/executor-health.ts +11 -34
- package/src/cli/agent-jobs/integration.ts +18 -49
- package/src/cli/agent-jobs/job-manager.ts +7 -3
- 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/recovery.ts +135 -121
- package/src/cli/commands/runtime.ts +3 -3
- 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 +4 -43
- package/src/cli/controller/completion-target.ts +3 -72
- package/src/cli/controller/context/source-materializer.ts +81 -100
- package/src/cli/controller/context-pack.ts +10 -8
- 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 +24 -51
- 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 +39 -1
- package/src/cli/editing/edit-session.ts +1 -7
- package/src/cli/local-bridge/console-automations.ts +18 -13
- package/src/cli/local-bridge/console-view-models.ts +3 -12
- package/src/cli/local-bridge/facade-api.ts +64 -191
- 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-executor.ts +29 -23
- package/src/cli/repositories/command-scope.ts +35 -16
- 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/registry.ts +77 -11
- package/src/cli/repositories/structured-git.ts +62 -2
- package/src/cli/repository/session-cache.ts +92 -6
- 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/semantic-navigation-contract.ts +9 -0
- package/src/runtime/context/semantic-navigation.ts +6 -34
- 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 +212 -9
- 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 +497 -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 +188 -109
- package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
- 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 +43 -27
- package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
- 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 +173 -186
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
- package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +362 -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 +8 -8
- package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
- 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 +239 -112
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
- 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/launcher/chatgpt-round-continuation.ts +63 -300
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -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 +290 -21
- 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/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 +79 -27
- 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 +35 -2
- package/src/runtime/execution/process-runtime/store.ts +30 -2
- package/src/runtime/execution/process-runtime/types.ts +6 -1
- package/src/runtime/execution/workers/executor.ts +2 -6
- 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 +85 -28
- package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
- package/src/runtime/plugins/browser-adapter.ts +757 -34
- package/src/runtime/plugins/browser-automation-service.ts +23 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
- package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
- package/src/runtime/plugins/browser-native-session-service.ts +63 -13
- package/src/runtime/plugins/browser-registration.ts +16 -0
- package/src/runtime/plugins/browser-runtime.ts +14 -1
- 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 +219 -0
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
- package/src/runtime/plugins/computer-registration.ts +222 -11
- 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/gmail-adapter.ts +23 -2
- package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
- package/src/runtime/plugins/google-shared.ts +14 -7
- package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
- package/src/runtime/plugins/ios-adapter.ts +1 -1
- package/src/runtime/plugins/lightweight-action.ts +208 -2
- package/src/runtime/plugins/local-system-adapter.ts +12 -2
- 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 +602 -428
- package/src/runtime/plugins/types.ts +20 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
- package/src/runtime/projections/invalidation.ts +7 -3
- package/src/runtime/projections/materialized-view.ts +80 -36
- package/src/runtime/recovery/actions.ts +3 -3
- package/src/runtime/recovery/capability-action-application.ts +46 -45
- package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
- package/src/runtime/recovery/maintenance-executor.ts +374 -123
- package/src/runtime/recovery/store.ts +29 -11
- package/src/runtime/release/release-coordinator.ts +296 -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 +192 -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 +105 -19
- package/src/runtime/root/package-runtime-release.ts +117 -18
- package/src/runtime/root/package-runtime-service.ts +17 -11
- package/src/runtime/root/release-execution-canary.ts +44 -12
- package/src/runtime/root/release-loader.ts +90 -8
- package/src/runtime/root/release-manifest.ts +80 -6
- package/src/runtime/root/release-materialize.ts +372 -28
- package/src/runtime/root/release-store.ts +395 -36
- package/src/runtime/root/runtime-lane.ts +191 -0
- package/src/runtime/root/runtime.ts +159 -27
- package/src/runtime/root/scheduled-controller-composition.ts +28 -3
- package/src/runtime/root/service-runner.ts +2 -2
- package/src/runtime/root/service.ts +36 -33
- package/src/runtime/root/status.ts +48 -0
- package/src/runtime/root/types.ts +32 -3
- package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
- package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
- package/src/runtime/root/write-fence.ts +50 -2
- package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
- package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
- package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
- package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
- 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 +3702 -342
- package/src/runtime/standalone-recovery/entry.ts +469 -39
- 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 +1 -1
- 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 +10 -3
- package/src/runtime/workflow/schedules/engine.ts +138 -172
- package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
- 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 +201 -0
- package/supervisor/control-plane.ts +868 -0
- package/supervisor/entry.ts +15 -0
- package/supervisor/forge-validators.ts +80 -0
- package/supervisor/index.ts +5 -0
- package/supervisor/native-browser-adapter.ts +786 -0
- package/supervisor/native-messaging/host.ts +39 -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 +202 -0
- package/supervisor/server.ts +404 -0
- package/supervisor/store.ts +1451 -0
- package/supervisor/tsconfig.json +8 -0
- package/supervisor/types.ts +255 -0
- 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/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-tarball-install-smoke.sh +0 -150
- 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/stage-runtime-release.ts +0 -44
- 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/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
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { parseChatgptConversationIdentity } from './chatgpt-conversation';
|
|
3
|
+
import { parseSupervisorCompletion, renderEffectMarker, renderSupervisorPrompt, sha256, validateEffectId } from './protocol';
|
|
4
|
+
import { WorkflowSupervisorStore } from './store';
|
|
5
|
+
import type { WorkflowAssistantObservation, WorkflowAssistantObservationResult, WorkflowContractValidation, WorkflowEffectKind, WorkflowSupervisorAutomationStatus, WorkflowSupervisorBrowserPollResult, WorkflowSupervisorBrowserTask, WorkflowSupervisorCompletion, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorLifecycleHooks, WorkflowSupervisorProjectScope, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTerminalState, WorkflowSupervisorValidators } from './types';
|
|
6
|
+
|
|
7
|
+
function compactProjectIdentity(value: string): string {
|
|
8
|
+
return value.normalize('NFKC').toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function projectIdentityTokens(value: string): string[] {
|
|
12
|
+
return value.normalize('NFKC').toLocaleLowerCase().split(/[^\p{L}\p{N}]+/gu).filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function boundedProjectIdentityMatch(expected: string, observed: string): boolean {
|
|
16
|
+
const compactExpected = compactProjectIdentity(expected);
|
|
17
|
+
const compactObserved = compactProjectIdentity(observed);
|
|
18
|
+
if (!compactExpected || !compactObserved) return false;
|
|
19
|
+
if (compactExpected === compactObserved) return true;
|
|
20
|
+
if (compactExpected.length < 4) return false;
|
|
21
|
+
const expectedTokens = projectIdentityTokens(expected);
|
|
22
|
+
const observedTokens = projectIdentityTokens(observed);
|
|
23
|
+
if (expectedTokens.length === 0 || expectedTokens.length >= observedTokens.length) return false;
|
|
24
|
+
return expectedTokens.every((token, index) => observedTokens[index] === token);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function projectSlugFromUrl(value: string): string | undefined {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(value);
|
|
30
|
+
const match = /^\/g\/(g-p-[a-z0-9]+)(?:-([^/]+))?\/project\/?$/i.exec(parsed.pathname);
|
|
31
|
+
return match?.[2]?.toLocaleLowerCase();
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function projectMatchesScope(conversation: WorkflowSupervisorDiscoveredConversation, scope: WorkflowSupervisorProjectScope): boolean {
|
|
38
|
+
const names = [scope.title, ...(scope.aliases ?? [])]
|
|
39
|
+
.map((value) => value.trim().toLocaleLowerCase())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
const projectTitle = conversation.projectTitle?.trim().toLocaleLowerCase();
|
|
42
|
+
if (projectTitle && names.some((name) => boundedProjectIdentityMatch(name, projectTitle))) return true;
|
|
43
|
+
const projectSlug = conversation.projectUrl ? projectSlugFromUrl(conversation.projectUrl) : undefined;
|
|
44
|
+
if (!projectSlug) return false;
|
|
45
|
+
return names.some((name) => boundedProjectIdentityMatch(name, projectSlug));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function effectId(): string { return `fx_${randomUUID().replaceAll('-', '')}`; }
|
|
49
|
+
function stableEffectId(originKey: string): string {
|
|
50
|
+
return validateEffectId(`fx_${sha256(originKey).slice(0, 32)}`);
|
|
51
|
+
}
|
|
52
|
+
const rejectUnconfigured = async (): Promise<WorkflowContractValidation> => ({ valid: false, reason: 'validator_unconfigured' });
|
|
53
|
+
|
|
54
|
+
export class WorkflowSupervisorControlPlane {
|
|
55
|
+
readonly validators: WorkflowSupervisorValidators;
|
|
56
|
+
readonly hooks: WorkflowSupervisorLifecycleHooks;
|
|
57
|
+
constructor(readonly store: WorkflowSupervisorStore, validators: Partial<WorkflowSupervisorValidators> = {}, hooks: WorkflowSupervisorLifecycleHooks = {}) {
|
|
58
|
+
this.validators = { completionContract: validators.completionContract ?? rejectUnconfigured, userBlockerPolicy: validators.userBlockerPolicy ?? rejectUnconfigured };
|
|
59
|
+
this.hooks = hooks;
|
|
60
|
+
}
|
|
61
|
+
registerTask(input: WorkflowSupervisorTaskInput): WorkflowSupervisorTask { return this.store.registerTask(input); }
|
|
62
|
+
reviseTaskObjective(input: {
|
|
63
|
+
taskId: string;
|
|
64
|
+
objective: string;
|
|
65
|
+
expectedObjectiveSha256: string;
|
|
66
|
+
requestId: string;
|
|
67
|
+
authorizedBy?: string;
|
|
68
|
+
}): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; refreshedEffect?: WorkflowSupervisorEffect } {
|
|
69
|
+
const task = this.requireTask(input.taskId);
|
|
70
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
71
|
+
const objective = input.objective.trim();
|
|
72
|
+
if (!objective || objective.length > 8_000) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_INVALID');
|
|
73
|
+
if (!/^[0-9a-f]{64}$/i.test(input.expectedObjectiveSha256)) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_EXPECTED_SHA_INVALID');
|
|
74
|
+
if (!input.requestId.trim()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_REQUIRED');
|
|
75
|
+
const pending = this.store.currentUnappliedEffect(task.taskId);
|
|
76
|
+
let refreshEffect: { effectId: string; expectedPrompt: string; prompt: string } | undefined;
|
|
77
|
+
if (pending && !this.store.latestEffectDispatch(pending.effectId)) {
|
|
78
|
+
const source = pending.sourceCompletionFingerprint ? this.store.getCompletion(pending.sourceCompletionFingerprint) : undefined;
|
|
79
|
+
const checkpoint = source?.proposal.reason === 'compact_receipt' ? undefined : source?.proposal.checkpoint;
|
|
80
|
+
refreshEffect = {
|
|
81
|
+
effectId: pending.effectId,
|
|
82
|
+
expectedPrompt: pending.prompt,
|
|
83
|
+
prompt: this.renderPrompt({ ...task, objective }, pending.effectId, pending.kind, checkpoint),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return this.store.reviseTaskObjective({
|
|
87
|
+
taskId: task.taskId,
|
|
88
|
+
objective,
|
|
89
|
+
expectedObjectiveSha256: input.expectedObjectiveSha256.toLowerCase(),
|
|
90
|
+
requestId: input.requestId.trim(),
|
|
91
|
+
authorizedBy: input.authorizedBy?.trim() || 'operator',
|
|
92
|
+
...(refreshEffect ? { refreshEffect } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Explicit operator recovery. It is deliberately evidence-classified rather
|
|
97
|
+
* than a generic "try again": each class names the durable mechanical fact
|
|
98
|
+
* that makes exactly one bounded next step safe, and none of them can replay
|
|
99
|
+
* a submission whose outcome is unknown.
|
|
100
|
+
*
|
|
101
|
+
* - proven-un-applied effect at its retry ceiling -> refund the mechanical
|
|
102
|
+
* budget once and re-dispatch that same effect;
|
|
103
|
+
* - unknown-outcome effect -> read-only reclassification only, unless the
|
|
104
|
+
* operator explicitly authorizes retiring it so a *fresh* turn can run;
|
|
105
|
+
* - applied effect whose single provider resume is exhausted -> reserve one
|
|
106
|
+
* new, separately identified recovery turn on the same conversation.
|
|
107
|
+
*/
|
|
108
|
+
recoverTask(input: {
|
|
109
|
+
taskId: string;
|
|
110
|
+
sourceEffectId?: string;
|
|
111
|
+
requestId: string;
|
|
112
|
+
reason: string;
|
|
113
|
+
/** Explicit operator authority required to retire an outcome-unknown effect. */
|
|
114
|
+
supersedeUnknown?: boolean;
|
|
115
|
+
authorizedBy?: string;
|
|
116
|
+
}): { recoveryEffect: WorkflowSupervisorEffect; action: 'retry_authorized' | 'recovery_reserved' | 'unknown_superseded' } {
|
|
117
|
+
const task = this.requireTask(input.taskId);
|
|
118
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
119
|
+
if (!input.requestId.trim() || !input.reason.trim()) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_REASON_REQUIRED');
|
|
120
|
+
const authorizedBy = input.authorizedBy?.trim() || 'operator';
|
|
121
|
+
// Identity-stable replay first: an operator recovery already reserved for
|
|
122
|
+
// this exact source effect is the durable answer, whether or not it has
|
|
123
|
+
// since been dispatched.
|
|
124
|
+
const legacyOrigin = input.sourceEffectId ? `provider-recovery:${input.sourceEffectId}` : undefined;
|
|
125
|
+
const existingLegacy = legacyOrigin ? this.store.getEffectByOriginKey(legacyOrigin) : undefined;
|
|
126
|
+
if (existingLegacy?.taskId === task.taskId) return { recoveryEffect: existingLegacy, action: 'recovery_reserved' };
|
|
127
|
+
// The same explicit supersession decision is one durable fact; replaying it
|
|
128
|
+
// returns its replacement instead of failing on the now-retired source.
|
|
129
|
+
const supersedeOrigin = `operator-recovery:${input.sourceEffectId ?? ''}:${input.requestId}`;
|
|
130
|
+
const existingSupersede = this.store.getEffectByOriginKey(supersedeOrigin);
|
|
131
|
+
if (existingSupersede?.taskId === task.taskId) return { recoveryEffect: existingSupersede, action: 'unknown_superseded' };
|
|
132
|
+
const pending = this.store.currentUnappliedEffect(task.taskId);
|
|
133
|
+
if (input.sourceEffectId && pending && pending.effectId !== input.sourceEffectId) {
|
|
134
|
+
throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
|
|
135
|
+
}
|
|
136
|
+
if (pending) {
|
|
137
|
+
// Read-only reclassification first: a dispatch-owner pre-send return is a
|
|
138
|
+
// stronger fact than any operator decision and makes the effect retryable
|
|
139
|
+
// through the normal mechanical path.
|
|
140
|
+
if (this.store.reconcileNativePreSendFailure(pending.effectId, input.requestId)) {
|
|
141
|
+
return { recoveryEffect: pending, action: 'retry_authorized' };
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
this.store.authorizeOperatorRetry({ effectId: pending.effectId, requestId: input.requestId, reason: input.reason, authorizedBy });
|
|
145
|
+
return { recoveryEffect: pending, action: 'retry_authorized' };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
148
|
+
if (message !== 'WORKFLOW_SUPERVISOR_RECOVERY_OUTCOME_UNKNOWN') throw error;
|
|
149
|
+
if (input.supersedeUnknown !== true) throw error;
|
|
150
|
+
}
|
|
151
|
+
// The operator explicitly decided to stop waiting on an unknown
|
|
152
|
+
// submission. Record that decision durably and reserve one *new* turn in
|
|
153
|
+
// the same transaction; the unknown effect is never resent and never
|
|
154
|
+
// silently discarded.
|
|
155
|
+
const origin = `operator-recovery:${pending.effectId}:${input.requestId}`;
|
|
156
|
+
const supersededId = stableEffectId(origin);
|
|
157
|
+
const prompt = this.renderPrompt(task, supersededId, 'recovery', undefined,
|
|
158
|
+
`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.`);
|
|
159
|
+
const superseded = this.store.supersedeUnknownEffect({
|
|
160
|
+
effectId: pending.effectId, requestId: input.requestId, reason: input.reason, authorizedBy,
|
|
161
|
+
replacement: { effectId: supersededId, prompt },
|
|
162
|
+
});
|
|
163
|
+
return { recoveryEffect: superseded.recoveryEffect, action: 'unknown_superseded' };
|
|
164
|
+
}
|
|
165
|
+
const source = this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
|
|
166
|
+
if (!source || (input.sourceEffectId && source.effectId !== input.sourceEffectId)) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
|
|
167
|
+
const origin = `provider-recovery:${source.effectId}`;
|
|
168
|
+
const id = stableEffectId(origin);
|
|
169
|
+
if (this.store.providerResumeExhausted(source.effectId)) {
|
|
170
|
+
// The automatic resume ceiling is the exact mechanical fact that permits
|
|
171
|
+
// a further operator turn; without it this path must not exist.
|
|
172
|
+
const prompt = this.renderPrompt(task, id, 'recovery', undefined,
|
|
173
|
+
`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.`);
|
|
174
|
+
return { recoveryEffect: this.store.reserveOperatorProviderRecovery({
|
|
175
|
+
taskId: task.taskId, sourceEffectId: source.effectId, requestId: input.requestId, reason: input.reason,
|
|
176
|
+
authorizedBy, originKey: origin, effectId: id, prompt,
|
|
177
|
+
}), action: 'recovery_reserved' };
|
|
178
|
+
}
|
|
179
|
+
const prompt = this.renderPrompt(task, id, 'recovery', undefined,
|
|
180
|
+
`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.`);
|
|
181
|
+
return { recoveryEffect: this.store.reserveOperatorRecovery({
|
|
182
|
+
taskId: task.taskId, sourceEffectId: source.effectId, requestId: input.requestId, reason: input.reason, effectId: id, prompt,
|
|
183
|
+
}), action: 'recovery_reserved' };
|
|
184
|
+
}
|
|
185
|
+
reserveEnrollment(taskId: string, canonicalEffectId?: string): WorkflowSupervisorEffect {
|
|
186
|
+
const task = this.requireTask(taskId);
|
|
187
|
+
// A task that already reached a terminal supervisor action is not deliverable.
|
|
188
|
+
// Reserving another enrollment effect for it produced an "enrolled" result that
|
|
189
|
+
// could never be delivered, so the ControllerRound waited forever instead of
|
|
190
|
+
// surfacing the operator/provider decision that terminal state represents.
|
|
191
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
192
|
+
const originKey = `enrollment:${taskId}`;
|
|
193
|
+
// Supervisor owns the outer-turn effect identity. A lower ControllerRound may
|
|
194
|
+
// rotate when the semantic Work carrier changes, but the Requirement + exact
|
|
195
|
+
// conversation task must keep one enrollment effect. Reuse by origin before
|
|
196
|
+
// considering a newer lower-layer canonical effect id.
|
|
197
|
+
const existingForOrigin = this.store.getEffectByOriginKey(originKey);
|
|
198
|
+
let effect: WorkflowSupervisorEffect;
|
|
199
|
+
if (existingForOrigin) {
|
|
200
|
+
if (existingForOrigin.taskId !== taskId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT:${originKey}`);
|
|
201
|
+
if (existingForOrigin.kind !== 'enrollment') throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_KIND_CONFLICT:${originKey}`);
|
|
202
|
+
effect = existingForOrigin;
|
|
203
|
+
} else {
|
|
204
|
+
const id = canonicalEffectId ? validateEffectId(canonicalEffectId) : stableEffectId(originKey);
|
|
205
|
+
const existing = this.store.getEffect(id);
|
|
206
|
+
if (existing) {
|
|
207
|
+
if (existing.taskId !== taskId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT:${originKey}`);
|
|
208
|
+
if (existing.kind !== 'enrollment') throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_KIND_CONFLICT:${originKey}`);
|
|
209
|
+
effect = existing;
|
|
210
|
+
} else {
|
|
211
|
+
effect = this.store.reserveEffect({ taskId, effectId: id, kind: 'enrollment', originKey, prompt: this.renderPrompt(task, id, 'enrollment') });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const inheritedDispatch = this.hooks.inheritedEffectDispatch?.(task, effect);
|
|
215
|
+
if (inheritedDispatch && !this.store.effectApplied(effect.effectId)) {
|
|
216
|
+
this.store.recordEffectDispatchStarted(
|
|
217
|
+
effect.effectId,
|
|
218
|
+
inheritedDispatch.generation,
|
|
219
|
+
inheritedDispatch.dispatchId,
|
|
220
|
+
inheritedDispatch.evidence,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return effect;
|
|
224
|
+
}
|
|
225
|
+
observeEffect(input: { effectId: string; observationId: string; outcome: 'applied' | 'not_applied' | 'unknown'; evidence?: Record<string, unknown> }): void {
|
|
226
|
+
this.store.recordEffectObservation(validateEffectId(input.effectId), input.observationId, input.outcome, input.evidence);
|
|
227
|
+
}
|
|
228
|
+
getTask(taskId: string): WorkflowSupervisorTask | undefined { return this.store.getTask(taskId); }
|
|
229
|
+
getTaskByConversationId(conversationId: string): { task: WorkflowSupervisorTask; terminal?: WorkflowSupervisorTerminalState } | undefined {
|
|
230
|
+
const task = this.store.getTaskByConversationId(conversationId);
|
|
231
|
+
if (!task) return undefined;
|
|
232
|
+
const terminal = this.store.terminalAction(task.taskId);
|
|
233
|
+
return { task, ...(terminal ? { terminal } : {}) };
|
|
234
|
+
}
|
|
235
|
+
listTasks(activeOnly = false): WorkflowSupervisorTask[] {
|
|
236
|
+
const tasks = this.store.listTasks();
|
|
237
|
+
return activeOnly ? tasks.filter((task) => !this.store.terminalAction(task.taskId)) : tasks;
|
|
238
|
+
}
|
|
239
|
+
stopTask(taskId: string, reason = 'Stopped by operator request.'): { taskId: string; terminal: 'STOPPED'; deduplicated: boolean } {
|
|
240
|
+
return this.store.stopTask(taskId, reason);
|
|
241
|
+
}
|
|
242
|
+
bindBootstrapConversation(input: { taskId: string; conversationId: string; conversationUrl: string }): WorkflowSupervisorTask {
|
|
243
|
+
const identity = parseChatgptConversationIdentity(input.conversationUrl);
|
|
244
|
+
if (identity.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_CONVERSATION_MISMATCH');
|
|
245
|
+
const pendingMigration = this.store.freshConversationMigrationContext(input.taskId);
|
|
246
|
+
const task = this.store.bindBootstrapConversation(input.taskId, identity.conversationId, identity.canonicalUrl);
|
|
247
|
+
this.hooks.bootstrapConversationBound?.(task, pendingMigration ? {
|
|
248
|
+
migratedFromConversationId: pendingMigration.fromConversationId,
|
|
249
|
+
migrationRequestId: pendingMigration.requestId,
|
|
250
|
+
} : undefined);
|
|
251
|
+
return task;
|
|
252
|
+
}
|
|
253
|
+
getEffect(id: string): WorkflowSupervisorEffect | undefined { return this.store.getEffect(validateEffectId(id)); }
|
|
254
|
+
/** Read-only stall classification for one task; no lifecycle authority. */
|
|
255
|
+
taskStall(taskId: string): ReturnType<WorkflowSupervisorStore['taskStall']> { return this.store.taskStall(this.requireTask(taskId).taskId); }
|
|
256
|
+
/**
|
|
257
|
+
* Explicit operator move of one task's exact conversation. Chatting in a new
|
|
258
|
+
* conversation is never an automatic answer to provider backpressure: a 429 is
|
|
259
|
+
* answered by fewer requests, and only a conversation that is durably unusable
|
|
260
|
+
* is replaced. Requires the caller's expected current conversation so a stale
|
|
261
|
+
* decision cannot silently move a task that already advanced.
|
|
262
|
+
*/
|
|
263
|
+
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 } {
|
|
264
|
+
const task = this.requireTask(input.taskId);
|
|
265
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
266
|
+
if (!input.requestId.trim() || !input.reason.trim()) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_REASON_REQUIRED');
|
|
267
|
+
if (input.fresh === true) {
|
|
268
|
+
if (input.conversationId?.trim() || input.conversationUrl?.trim()) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_FRESH_CONVERSATION_CONFLICT');
|
|
269
|
+
const origin = `fresh-conversation:${task.taskId}:${input.requestId}`;
|
|
270
|
+
const effectId = stableEffectId(origin);
|
|
271
|
+
const migrated = this.store.migrateToFreshConversation({
|
|
272
|
+
taskId: task.taskId,
|
|
273
|
+
expectedConversationId: input.expectedConversationId,
|
|
274
|
+
requestId: input.requestId,
|
|
275
|
+
reason: input.reason,
|
|
276
|
+
authorizedBy: input.authorizedBy?.trim() || 'operator',
|
|
277
|
+
// A fresh conversation has no prior turns, so the replacement turn must
|
|
278
|
+
// carry the objective again instead of the bare "继续。" continuation.
|
|
279
|
+
replacement: { effectId, prompt: this.renderPrompt(task, effectId, 'enrollment') },
|
|
280
|
+
});
|
|
281
|
+
return { taskId: migrated.task.taskId, conversationId: migrated.task.conversationId, conversationUrl: migrated.task.conversationUrl, migrated: migrated.migrated, freshConversation: migrated.migrated };
|
|
282
|
+
}
|
|
283
|
+
if (!input.conversationId?.trim() || !input.conversationUrl?.trim()) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_CONVERSATION_REQUIRED');
|
|
284
|
+
const identity = parseChatgptConversationIdentity(input.conversationUrl);
|
|
285
|
+
if (identity.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_CONVERSATION_MISMATCH');
|
|
286
|
+
const migrated = this.store.migrateConversation({
|
|
287
|
+
taskId: task.taskId,
|
|
288
|
+
expectedConversationId: input.expectedConversationId,
|
|
289
|
+
conversationId: identity.conversationId,
|
|
290
|
+
conversationUrl: identity.canonicalUrl,
|
|
291
|
+
requestId: input.requestId,
|
|
292
|
+
reason: input.reason,
|
|
293
|
+
authorizedBy: input.authorizedBy?.trim() || 'operator',
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
taskId: migrated.task.taskId, conversationId: migrated.task.conversationId,
|
|
297
|
+
conversationUrl: migrated.task.conversationUrl, migrated: migrated.migrated,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** Mechanical provider re-dispatch budget for one effect, used by recovery to surface exhaustion. */
|
|
301
|
+
effectDispatchBudget(effectId: string): ReturnType<WorkflowSupervisorStore['effectDispatchBudget']> {
|
|
302
|
+
return this.store.effectDispatchBudget(validateEffectId(effectId));
|
|
303
|
+
}
|
|
304
|
+
continuationProof(input: { repoId?: string; activeReleaseId: string; notBefore: string }) { return this.store.continuationProof(input); }
|
|
305
|
+
browserDiscoverySnapshot() { return this.store.discoverySnapshot(); }
|
|
306
|
+
bootstrapProjectUrl(taskId: string): string {
|
|
307
|
+
const task = this.requireTask(taskId);
|
|
308
|
+
const scope = this.hooks.projectScopeForTask?.(task);
|
|
309
|
+
const title = scope?.title.trim();
|
|
310
|
+
if (!scope || !title) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_PROJECT_SCOPE_REQUIRED');
|
|
311
|
+
const projects = new Map<string, string>();
|
|
312
|
+
for (const conversation of this.store.discoverySnapshot().conversations) {
|
|
313
|
+
if (!projectMatchesScope(conversation, scope)) continue;
|
|
314
|
+
const value = conversation.projectUrl?.trim();
|
|
315
|
+
if (!value) continue;
|
|
316
|
+
try {
|
|
317
|
+
const parsed = new URL(value);
|
|
318
|
+
if (parsed.protocol !== 'https:' || parsed.hostname !== 'chatgpt.com') continue;
|
|
319
|
+
const match = /^\/g\/(g-p-[a-z0-9]+)(?:-[^/]+)?\/project\/?$/i.exec(parsed.pathname);
|
|
320
|
+
if (!match) continue;
|
|
321
|
+
const projectId = match[1]!.toLocaleLowerCase();
|
|
322
|
+
projects.set(projectId, `https://chatgpt.com/g/${match[1]!}/project`);
|
|
323
|
+
} catch {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (projects.size === 0) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_PROJECT_NOT_DISCOVERED');
|
|
328
|
+
if (projects.size !== 1) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_PROJECT_AMBIGUOUS');
|
|
329
|
+
return [...projects.values()][0]!;
|
|
330
|
+
}
|
|
331
|
+
recordBrowserDiscovery(source: string, conversations: readonly WorkflowSupervisorDiscoveredConversation[]) {
|
|
332
|
+
// Discovery is durable observation only. Creating a Supervisor task/effect
|
|
333
|
+
// requires the explicit Work/current-conversation enrollment path.
|
|
334
|
+
return this.store.recordDiscovery(source, conversations);
|
|
335
|
+
}
|
|
336
|
+
browserProjectScopes(): WorkflowSupervisorProjectScope[] {
|
|
337
|
+
const scopes = new Map<string, WorkflowSupervisorProjectScope>();
|
|
338
|
+
for (const task of this.store.listTasks()) {
|
|
339
|
+
const scope = this.hooks.projectScopeForTask?.(task);
|
|
340
|
+
if (!scope?.title.trim()) continue;
|
|
341
|
+
const normalized: WorkflowSupervisorProjectScope = {
|
|
342
|
+
title: scope.title.trim().slice(0, 512),
|
|
343
|
+
...(scope.aliases?.length ? { aliases: [...new Set(scope.aliases.map((value) => value.trim().slice(0, 256)).filter(Boolean))].slice(0, 16) } : {}),
|
|
344
|
+
...(scope.repoId?.trim() ? { repoId: scope.repoId.trim().slice(0, 256) } : {}),
|
|
345
|
+
...(scope.controllerHome?.trim() ? { controllerHome: scope.controllerHome.trim().slice(0, 2048) } : {}),
|
|
346
|
+
};
|
|
347
|
+
const key = `${normalized.title.toLocaleLowerCase()}\n${normalized.repoId ?? ''}\n${normalized.controllerHome ?? ''}`;
|
|
348
|
+
scopes.set(key, normalized);
|
|
349
|
+
}
|
|
350
|
+
return [...scopes.values()];
|
|
351
|
+
}
|
|
352
|
+
browserTasks(): WorkflowSupervisorBrowserTask[] {
|
|
353
|
+
const active = this.store.listTasks().flatMap((task) => {
|
|
354
|
+
if (this.store.terminalAction(task.taskId)) return [];
|
|
355
|
+
// A Computer page is an ephemeral delivery surface. An applied effect
|
|
356
|
+
// awaiting its receipt remains eligible for read-only provider-health
|
|
357
|
+
// observation, so a terminal stream error can reserve its one bounded
|
|
358
|
+
// recovery effect. Rendered assistant text never decides the workflow;
|
|
359
|
+
// only a persisted receipt does that.
|
|
360
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
361
|
+
// Any applied effect awaiting its receipt stays observable, including one
|
|
362
|
+
// whose bounded provider resume is already exhausted. Observation is
|
|
363
|
+
// read-only and mints nothing, so keeping it attached is what lets late
|
|
364
|
+
// provider/completion evidence resolve the turn instead of stranding it.
|
|
365
|
+
const providerTurnAwaitingReceipt = this.store.hasAppliedEffectAwaitingCompletion(task.taskId);
|
|
366
|
+
if (!pending && !providerTurnAwaitingReceipt) return [];
|
|
367
|
+
// Bootstrap has no exact conversation yet, so it cannot satisfy the
|
|
368
|
+
// normal Work-boundary predicate. Its already-persisted enrollment effect
|
|
369
|
+
// is the narrow authority to acquire one through Computer exactly once.
|
|
370
|
+
if (!task.conversationId.startsWith('bootstrap:')) {
|
|
371
|
+
// Optional lower-layer Work/Requirement compatibility must be task-local.
|
|
372
|
+
// A stale legacy task is allowed to fail closed, but it must never poison
|
|
373
|
+
// the global Supervisor delivery queue and block standalone tasks.
|
|
374
|
+
try { if (pending?.mode === 'send' && !this.browserTaskActive(task)) return []; }
|
|
375
|
+
catch { return []; }
|
|
376
|
+
}
|
|
377
|
+
return [{ task, mode: pending?.mode ?? 'reconcile' }];
|
|
378
|
+
});
|
|
379
|
+
// Fresh effects are bounded work with no prior external mutation. Outcome-
|
|
380
|
+
// unknown reconciliation remains durable and exactly-once, but cannot be
|
|
381
|
+
// allowed to monopolize the single native browser lane indefinitely.
|
|
382
|
+
active.sort((left, right) => {
|
|
383
|
+
const leftPriority = left.mode === 'send' ? 0 : 1;
|
|
384
|
+
const rightPriority = right.mode === 'send' ? 0 : 1;
|
|
385
|
+
return leftPriority - rightPriority;
|
|
386
|
+
});
|
|
387
|
+
return active.map(({ task }) => browserTask(task));
|
|
388
|
+
}
|
|
389
|
+
browserPoll(input: { conversationId: string; conversationUrl: string }): WorkflowSupervisorBrowserPollResult {
|
|
390
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
391
|
+
const projection = browserTask(task);
|
|
392
|
+
const terminal = this.store.terminalAction(task.taskId);
|
|
393
|
+
// STOPPED is an operator terminal, not a semantic completion. Return it
|
|
394
|
+
// before the normal external-effect activity gate so an already-running
|
|
395
|
+
// browser poll observes the stop and cannot turn it into another command.
|
|
396
|
+
if (terminal === 'STOPPED') return { authorized: true, task: projection, terminal };
|
|
397
|
+
if (terminal) return { authorized: true, task: projection, terminal };
|
|
398
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
399
|
+
// Read-only reconciliation belongs to the already-started effect even when
|
|
400
|
+
// its Work/conversation binding was retired. It never authorizes a resend.
|
|
401
|
+
if (pending?.mode === 'send' && !this.browserTaskActive(task)) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_TASK_INACTIVE');
|
|
402
|
+
if (!pending) return { authorized: true, task: projection };
|
|
403
|
+
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 } };
|
|
404
|
+
}
|
|
405
|
+
bootstrapPoll(taskId: string): WorkflowSupervisorBrowserPollResult {
|
|
406
|
+
const task = this.requireTask(taskId);
|
|
407
|
+
if (!task.conversationId.startsWith('bootstrap:')) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_PENDING');
|
|
408
|
+
const terminal = this.store.terminalAction(task.taskId);
|
|
409
|
+
if (terminal === 'STOPPED') return { authorized: true, task: browserTask(task), terminal };
|
|
410
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
411
|
+
if (pending) {
|
|
412
|
+
return {
|
|
413
|
+
authorized: true,
|
|
414
|
+
task: browserTask(task),
|
|
415
|
+
command: {
|
|
416
|
+
mode: pending.mode,
|
|
417
|
+
effectId: pending.effect.effectId,
|
|
418
|
+
kind: pending.effect.kind,
|
|
419
|
+
prompt: pending.effect.prompt,
|
|
420
|
+
dispatchGeneration: pending.generation,
|
|
421
|
+
conversationId: task.conversationId,
|
|
422
|
+
conversationUrl: task.conversationUrl,
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// Provider application and bootstrap identity binding are separate facts.
|
|
427
|
+
// A confirmed send may already be applied while ChatGPT has only just
|
|
428
|
+
// canonicalized the new /c/<conversation> route. Keep one read-only
|
|
429
|
+
// reconciliation command for that applied effect so the exact conversation
|
|
430
|
+
// can be bound without manufacturing another provider generation.
|
|
431
|
+
const applied = this.store.latestAppliedEffectWithoutCompletion(task.taskId);
|
|
432
|
+
if (!applied) return { authorized: true, task: browserTask(task) };
|
|
433
|
+
const dispatch = this.store.latestEffectDispatch(applied.effectId);
|
|
434
|
+
return {
|
|
435
|
+
authorized: true,
|
|
436
|
+
task: browserTask(task),
|
|
437
|
+
command: {
|
|
438
|
+
mode: 'reconcile',
|
|
439
|
+
effectId: applied.effectId,
|
|
440
|
+
kind: applied.kind,
|
|
441
|
+
prompt: applied.prompt,
|
|
442
|
+
dispatchGeneration: dispatch?.generation ?? 1,
|
|
443
|
+
conversationId: task.conversationId,
|
|
444
|
+
conversationUrl: task.conversationUrl,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
bootstrapBeginEffect(input: { taskId: string; effectId: string; dispatchId: string; dispatchGeneration: number }): boolean {
|
|
449
|
+
const task = this.requireTask(input.taskId);
|
|
450
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
451
|
+
if (!task.conversationId.startsWith('bootstrap:')) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_PENDING');
|
|
452
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
453
|
+
if (!pending || pending.effect.effectId !== validateEffectId(input.effectId) || pending.mode !== 'send') return false;
|
|
454
|
+
return this.store.recordEffectDispatchStarted(input.effectId, input.dispatchGeneration, input.dispatchId, {
|
|
455
|
+
surface: 'computer-bootstrap',
|
|
456
|
+
...(this.hooks.effectDispatchEvidence?.({ task, effectId: input.effectId }) ?? {}),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
bootstrapObserveEffect(input: { taskId: string; effectId: string; observationId: string; outcome: 'applied' | 'not_applied' | 'unknown'; evidence?: Record<string, unknown> }): void {
|
|
460
|
+
const task = this.requireTask(input.taskId);
|
|
461
|
+
const effect = this.store.getEffect(validateEffectId(input.effectId));
|
|
462
|
+
if (!effect || effect.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_EFFECT_TASK_MISMATCH');
|
|
463
|
+
if (input.outcome === 'not_applied') {
|
|
464
|
+
// This path is private to the provider dispatch adapter after its exact
|
|
465
|
+
// dispatch function returned without clicking Send. It is therefore a
|
|
466
|
+
// mechanical negative proof, unlike a missing post-send conversation
|
|
467
|
+
// marker, which remains outcome-unknown and must reconcile.
|
|
468
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
469
|
+
const dispatch = this.store.latestEffectDispatch(effect.effectId);
|
|
470
|
+
if (!pending || pending.effect.effectId !== effect.effectId || pending.mode !== 'reconcile'
|
|
471
|
+
|| !dispatch || pending.generation !== dispatch.generation) {
|
|
472
|
+
throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_APPLIED_PROOF_INVALID');
|
|
473
|
+
}
|
|
474
|
+
this.store.recordEffectNotAppliedProof(effect.effectId, input.observationId, {
|
|
475
|
+
surface: 'computer-bootstrap',
|
|
476
|
+
dispatch_generation: dispatch.generation,
|
|
477
|
+
pre_send_rejection: true,
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const evidence = { surface: 'computer-bootstrap', ...(input.evidence ?? {}) };
|
|
482
|
+
this.observeEffect({ effectId: effect.effectId, observationId: input.observationId, outcome: input.outcome, evidence });
|
|
483
|
+
if (input.outcome === 'applied') this.hooks.effectApplied?.(task, effect, { observationId: input.observationId, evidence });
|
|
484
|
+
}
|
|
485
|
+
/** Local Computer preparation returned before Send intent; persist in the existing effect ledger. */
|
|
486
|
+
recordProviderPreparationFailure(input: { taskId: string; effectId: string; reasonCode: string }): void {
|
|
487
|
+
const task = this.requireTask(input.taskId);
|
|
488
|
+
const effect = this.store.getEffect(input.effectId);
|
|
489
|
+
if (effect?.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_EFFECT_TASK_MISMATCH');
|
|
490
|
+
this.store.recordEffectPreparationFailure(input.effectId, input.reasonCode);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
browserBeginEffect(input: { conversationId: string; conversationUrl: string; effectId: string; dispatchId: string; dispatchGeneration: number; evidence?: Record<string, unknown> }): { started: boolean; mode: 'send' | 'reconcile'; generation: number } {
|
|
494
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
495
|
+
requireNonTerminalTask(this.store, task.taskId);
|
|
496
|
+
const pending = this.store.nextBrowserEffect(task.taskId);
|
|
497
|
+
const effectId = validateEffectId(input.effectId);
|
|
498
|
+
if (!pending || pending.effect.effectId !== effectId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_EFFECT_NOT_CURRENT');
|
|
499
|
+
if (pending.mode !== 'send' || pending.generation !== input.dispatchGeneration) return { started: false, mode: 'reconcile', generation: pending.generation };
|
|
500
|
+
// Re-check immediately before committing dispatch; a queued browser command
|
|
501
|
+
// must not outlive an explicit conversation rebind.
|
|
502
|
+
if (!this.browserTaskActive(task)) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_TASK_INACTIVE');
|
|
503
|
+
const snapshot = browserSnapshot(input.evidence);
|
|
504
|
+
if (!snapshot || browserTextHasEffect(snapshot.latestUserText, effectId) || !this.browserSnapshotMatchesSource(task, pending.effect, snapshot)) {
|
|
505
|
+
return { started: false, mode: 'reconcile', generation: pending.generation };
|
|
506
|
+
}
|
|
507
|
+
const dispatchEvidence = {
|
|
508
|
+
surface: typeof input.evidence?.surface === 'string' ? input.evidence.surface.slice(0, 128) : 'chrome-extension',
|
|
509
|
+
baseline_user_sha256: browserTextSha256(snapshot.latestUserText),
|
|
510
|
+
baseline_assistant_sha256: sha256(snapshot.latestAssistantResponse),
|
|
511
|
+
baseline_has_source_completion: Boolean(pending.effect.sourceCompletionFingerprint),
|
|
512
|
+
...(this.hooks.effectDispatchEvidence?.({ task, effectId }) ?? {}),
|
|
513
|
+
};
|
|
514
|
+
const started = this.store.recordEffectDispatchStarted(effectId, input.dispatchGeneration, input.dispatchId, dispatchEvidence);
|
|
515
|
+
return { started, mode: started ? 'send' : 'reconcile', generation: input.dispatchGeneration };
|
|
516
|
+
}
|
|
517
|
+
browserObserveEffect(input: { conversationId: string; conversationUrl: string; effectId: string; observationId: string; outcome: 'applied' | 'not_applied' | 'unknown'; evidence?: Record<string, unknown> }): { recorded: true } {
|
|
518
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
519
|
+
const effect = this.store.getEffect(validateEffectId(input.effectId));
|
|
520
|
+
if (!effect || effect.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_EFFECT_TASK_MISMATCH');
|
|
521
|
+
if (input.outcome !== 'not_applied') {
|
|
522
|
+
const evidence = sanitizeBrowserEvidence(input.evidence);
|
|
523
|
+
this.observeEffect({ effectId: effect.effectId, observationId: input.observationId, outcome: input.outcome, evidence });
|
|
524
|
+
if (input.outcome === 'applied') this.hooks.effectApplied?.(task, effect, { observationId: input.observationId, evidence });
|
|
525
|
+
return { recorded: true };
|
|
526
|
+
}
|
|
527
|
+
// A rendered page can still be stale, including an empty composer and the
|
|
528
|
+
// exact pre-send history. Absence in that page is never proof that the
|
|
529
|
+
// provider rejected a submission. Only the dispatch owner can attest a
|
|
530
|
+
// mechanical pre-send failure (bootstrapObserveEffect); browser observers
|
|
531
|
+
// cannot authorize re-dispatch of an outcome-unknown mutation.
|
|
532
|
+
this.store.recordEffectObservation(effect.effectId, input.observationId, 'unknown', {
|
|
533
|
+
...sanitizeBrowserEvidence(input.evidence), reconciliation: true, reason: 'not_applied_proof_incomplete',
|
|
534
|
+
});
|
|
535
|
+
return { recorded: true };
|
|
536
|
+
}
|
|
537
|
+
/** Dispatch-owner attestation. Only the adapter that reserved this generation may call it. */
|
|
538
|
+
browserObserveDispatchFailure(input: { conversationId: string; conversationUrl: string; effectId: string; observationId: string; dispatchGeneration: number; reason: string; surface?: string }): void {
|
|
539
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
540
|
+
const effect = this.store.getEffect(input.effectId);
|
|
541
|
+
if (effect?.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_EFFECT_TASK_MISMATCH');
|
|
542
|
+
const dispatch = this.store.latestEffectDispatch(input.effectId);
|
|
543
|
+
if (dispatch?.generation !== input.dispatchGeneration) throw new Error('WORKFLOW_SUPERVISOR_DISPATCH_GENERATION_CHANGED');
|
|
544
|
+
this.store.recordEffectNotAppliedProof(input.effectId, input.observationId, {
|
|
545
|
+
surface: input.surface?.trim().slice(0, 128) || 'macos-native', reason: input.reason, send_clicked: false, dispatch_generation: input.dispatchGeneration,
|
|
546
|
+
// This call is the dispatch owner's own mechanical return, produced before
|
|
547
|
+
// any Send click. Marking it as a pre-send rejection is what lets the one
|
|
548
|
+
// bounded budget refund apply, exactly as the bootstrap path does; without
|
|
549
|
+
// it a purely local obstacle permanently exhausted the effect.
|
|
550
|
+
pre_send_rejection: true,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
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 } {
|
|
554
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
555
|
+
if (!this.browserTaskActiveForExternalEffect(task)) return { state: 'inactive' };
|
|
556
|
+
const sourceEffect = this.store.latestAppliedEffectWithoutCompletion(task.taskId);
|
|
557
|
+
if (!sourceEffect) return { state: 'none' };
|
|
558
|
+
const recoveryId = stableEffectId(`provider-recovery:${sourceEffect.effectId}`);
|
|
559
|
+
const providerFailureCode = input.providerFailureCode?.trim();
|
|
560
|
+
const recoveryReason = providerFailureCode
|
|
561
|
+
? `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.`
|
|
562
|
+
: `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.`;
|
|
563
|
+
return this.store.observeProviderTurn({
|
|
564
|
+
taskId: task.taskId,
|
|
565
|
+
effectId: sourceEffect.effectId,
|
|
566
|
+
generating: input.generating,
|
|
567
|
+
receiptMissing: input.receiptMissing === true,
|
|
568
|
+
assistantDigest: sha256(`${input.latestAssistantResponse}\n${input.providerActivityText ?? ''}`),
|
|
569
|
+
providerFailureCode,
|
|
570
|
+
observedAtMs: input.observedAtMs,
|
|
571
|
+
graceMs: input.graceMs,
|
|
572
|
+
recovery: { effectId: recoveryId, prompt: this.renderPrompt(task, recoveryId, 'recovery', undefined, recoveryReason) },
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
async browserObserveAssistant(input: { conversationId: string; conversationUrl: string; responseText: string }): Promise<WorkflowAssistantObservationResult> {
|
|
576
|
+
const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
|
|
577
|
+
return await this.observeAssistantTurn({ taskId: task.taskId, conversationId: task.conversationId, responseText: input.responseText });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Repair the crash boundary where a CONTINUE completion was durably committed
|
|
582
|
+
* before successor reservation finished in a previous Runtime. This never
|
|
583
|
+
* replays the provider response; it only reads the committed causal receipt and
|
|
584
|
+
* reserves the exactly-once successor effect.
|
|
585
|
+
*/
|
|
586
|
+
async reconcileCommittedContinuations(limit = 16): Promise<{ scanned: number; reconciled: number }> {
|
|
587
|
+
const completions = this.store.listContinueCompletionsAwaitingSuccessor(limit);
|
|
588
|
+
let reconciled = 0;
|
|
589
|
+
for (const completion of completions) {
|
|
590
|
+
const task = this.store.getTask(completion.taskId);
|
|
591
|
+
if (!task || this.store.terminalAction(task.taskId)) continue;
|
|
592
|
+
const committed = this.reserveContinuation(task, completion);
|
|
593
|
+
if (committed.successorEffect) reconciled += 1;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// A non-terminal Supervisor task must always have one derivable causal
|
|
597
|
+
// obligation: an unapplied effect, an applied effect awaiting completion,
|
|
598
|
+
// or a committed completion whose successor/terminal resolution can be
|
|
599
|
+
// reconstructed from durable facts. Registration is intentionally a
|
|
600
|
+
// separate transaction from effect reservation, so a Runtime crash between
|
|
601
|
+
// them must converge here instead of leaving an "active" inert task.
|
|
602
|
+
const inertTasks = this.store.listTasksWithoutCausalObligation(limit);
|
|
603
|
+
for (const task of inertTasks) {
|
|
604
|
+
if (this.store.terminalAction(task.taskId)) continue;
|
|
605
|
+
const latest = this.store.getLatestCompletion(task.taskId);
|
|
606
|
+
if (!latest) {
|
|
607
|
+
this.reserveEnrollment(task.taskId);
|
|
608
|
+
reconciled += 1;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (latest.action === 'CONTINUE') {
|
|
612
|
+
const committed = this.reserveContinuation(task, latest);
|
|
613
|
+
if (committed.successorEffect) reconciled += 1;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const validator = latest.action === 'DONE' ? this.validators.completionContract : this.validators.userBlockerPolicy;
|
|
617
|
+
const validation = await validator(task, latest.proposal);
|
|
618
|
+
const correctionId = validation.valid ? undefined : stableEffectId(`completion:${latest.completionFingerprint}`);
|
|
619
|
+
this.store.resolveTerminal({
|
|
620
|
+
completionFingerprint: latest.completionFingerprint,
|
|
621
|
+
taskId: task.taskId,
|
|
622
|
+
action: latest.action,
|
|
623
|
+
accepted: validation.valid,
|
|
624
|
+
reason: validation.reason,
|
|
625
|
+
...(correctionId ? {
|
|
626
|
+
correction: {
|
|
627
|
+
effectId: correctionId,
|
|
628
|
+
prompt: this.renderPrompt(task, correctionId, 'correction', latest.proposal.checkpoint, validation.reason),
|
|
629
|
+
},
|
|
630
|
+
} : {}),
|
|
631
|
+
});
|
|
632
|
+
reconciled += 1;
|
|
633
|
+
}
|
|
634
|
+
return { scanned: completions.length + inertTasks.length, reconciled };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async observeAssistantTurn(input: WorkflowAssistantObservation): Promise<WorkflowAssistantObservationResult> {
|
|
638
|
+
const task = this.requireTask(input.taskId);
|
|
639
|
+
if (task.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_CONVERSATION_MISMATCH');
|
|
640
|
+
const responseSha256 = sha256(input.responseText);
|
|
641
|
+
const expectedEffect = this.store.latestAppliedEffectWithoutCompletion(task.taskId)
|
|
642
|
+
?? this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
|
|
643
|
+
// A live applied effect always wins: an old compact receipt must never be
|
|
644
|
+
// allowed to satisfy a newer causal obligation. Only when no effect is
|
|
645
|
+
// awaiting completion may an exact persisted response hash recover the
|
|
646
|
+
// original effect context for idempotent duplicate observation.
|
|
647
|
+
const priorCompletion = expectedEffect ? undefined : this.store.getCompletionByResponseSha256(task.taskId, responseSha256);
|
|
648
|
+
const expectedEffectId = expectedEffect?.effectId ?? priorCompletion?.sourceEffectId;
|
|
649
|
+
const parsed = parseSupervisorCompletion(
|
|
650
|
+
input.responseText,
|
|
651
|
+
expectedEffectId ? { task, effectId: expectedEffectId } : undefined,
|
|
652
|
+
);
|
|
653
|
+
const sourceEffect = this.store.getEffect(parsed.proposal.sourceEffectId);
|
|
654
|
+
if (!sourceEffect || sourceEffect.taskId !== task.taskId || !this.store.effectApplied(sourceEffect.effectId)) throw new Error('WORKFLOW_SUPERVISOR_CAUSAL_EFFECT_NOT_APPLIED');
|
|
655
|
+
const explicitIdentityProtocol = sourceEffect.prompt.includes('conversation_id=') && sourceEffect.prompt.includes('task_id=') && sourceEffect.prompt.includes('supervisor_state');
|
|
656
|
+
if (parsed.proposal.conversationId && parsed.proposal.conversationId !== task.conversationId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_CONVERSATION_MISMATCH');
|
|
657
|
+
if (parsed.proposal.taskId && parsed.proposal.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_TASK_MISMATCH');
|
|
658
|
+
if (explicitIdentityProtocol && !parsed.proposal.conversationId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_CONVERSATION_ID_REQUIRED');
|
|
659
|
+
if (explicitIdentityProtocol && !parsed.proposal.taskId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_TASK_ID_REQUIRED');
|
|
660
|
+
if (explicitIdentityProtocol && !parsed.proposal.supervisorState) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_STATE_REQUIRED');
|
|
661
|
+
if (explicitIdentityProtocol && !parsed.proposal.activeScope) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_ACTIVE_SCOPE_REQUIRED');
|
|
662
|
+
const expectedScope = typeof task.completionContract.requirement_id === 'string' && task.completionContract.requirement_id.trim()
|
|
663
|
+
? `requirement:${task.completionContract.requirement_id.trim()}`
|
|
664
|
+
: typeof task.continuationPolicy.active_scope === 'string' && task.continuationPolicy.active_scope.trim()
|
|
665
|
+
? task.continuationPolicy.active_scope.trim()
|
|
666
|
+
: undefined;
|
|
667
|
+
if (expectedScope && parsed.proposal.activeScope && parsed.proposal.activeScope !== expectedScope) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_ACTIVE_SCOPE_MISMATCH');
|
|
668
|
+
const controlBlockSha256 = sha256(parsed.controlBlock);
|
|
669
|
+
const completionFingerprint = sha256(jsonIdentity(task.taskId, task.conversationId, parsed.proposal.sourceEffectId, responseSha256, controlBlockSha256));
|
|
670
|
+
const completion: WorkflowSupervisorCompletion = { completionFingerprint, taskId: task.taskId, sourceEffectId: parsed.proposal.sourceEffectId, action: parsed.proposal.action, responseSha256, controlBlockSha256, proposal: parsed.proposal, committedAt: new Date().toISOString() };
|
|
671
|
+
const terminal = this.store.terminalAction(task.taskId);
|
|
672
|
+
if (terminal) throw new Error(`WORKFLOW_SUPERVISOR_TASK_TERMINAL:${terminal}`);
|
|
673
|
+
|
|
674
|
+
if (parsed.proposal.action === 'CONTINUE') {
|
|
675
|
+
const committed = this.reserveContinuation(task, completion);
|
|
676
|
+
return { action: 'CONTINUE', completionFingerprint, terminal: false, successorEffect: committed.successorEffect!, deduplicated: committed.deduplicated };
|
|
677
|
+
}
|
|
678
|
+
const committed = this.store.commitCompletion(completion);
|
|
679
|
+
|
|
680
|
+
const validator = parsed.proposal.action === 'DONE' ? this.validators.completionContract : this.validators.userBlockerPolicy;
|
|
681
|
+
const validation = await validator(task, parsed.proposal);
|
|
682
|
+
const correctionId = validation.valid ? undefined : stableEffectId(`completion:${completionFingerprint}`);
|
|
683
|
+
const resolved = this.store.resolveTerminal({ completionFingerprint, taskId: task.taskId, action: parsed.proposal.action, accepted: validation.valid, reason: validation.reason,
|
|
684
|
+
...(correctionId ? { correction: { effectId: correctionId, prompt: this.renderPrompt(task, correctionId, 'correction', parsed.proposal.reason === 'compact_receipt' ? undefined : parsed.proposal.checkpoint, validation.reason) } } : {}) });
|
|
685
|
+
return { action: parsed.proposal.action, completionFingerprint, terminal: validation.valid, ...(resolved.successorEffect ? { successorEffect: resolved.successorEffect } : {}), validation, deduplicated: committed.deduplicated || resolved.deduplicated };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* The autonomous transport records its final state through the Forge tool
|
|
690
|
+
* call, not through text rendered in the ChatGPT page. The exact task and
|
|
691
|
+
* applied effect are derived locally; callers cannot select a different
|
|
692
|
+
* conversation or effect by supplying model-authored identifiers.
|
|
693
|
+
*/
|
|
694
|
+
async observeAutomationReceipt(input: {
|
|
695
|
+
taskId: string;
|
|
696
|
+
conversationId: string;
|
|
697
|
+
status: WorkflowSupervisorAutomationStatus;
|
|
698
|
+
receiptId: string;
|
|
699
|
+
}): Promise<WorkflowAssistantObservationResult | { recorded: true }> {
|
|
700
|
+
if (input.status === 'working') return { recorded: true };
|
|
701
|
+
const task = this.requireTask(input.taskId);
|
|
702
|
+
if (task.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_CONVERSATION_MISMATCH');
|
|
703
|
+
const terminal = this.store.terminalAction(task.taskId);
|
|
704
|
+
if (terminal) throw new Error(`WORKFLOW_SUPERVISOR_TASK_TERMINAL:${terminal}`);
|
|
705
|
+
const expectedEffect = this.store.latestAppliedEffectWithoutCompletion(task.taskId)
|
|
706
|
+
?? this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
|
|
707
|
+
// A provider can reuse one tool name/status in successive outer turns.
|
|
708
|
+
// Include the locally selected effect in the durable receipt identity so a
|
|
709
|
+
// later `continue` cannot replay the first effect's completion.
|
|
710
|
+
const replayCompletion = expectedEffect ? undefined : this.store.getLatestCompletion(task.taskId);
|
|
711
|
+
if (!expectedEffect && (!replayCompletion
|
|
712
|
+
|| replayCompletion.proposal.checkpoint !== `automation:${input.receiptId}:${replayCompletion.sourceEffectId}`)) {
|
|
713
|
+
throw new Error('WORKFLOW_SUPERVISOR_CAUSAL_EFFECT_NOT_APPLIED');
|
|
714
|
+
}
|
|
715
|
+
const receiptForEffect = `${input.receiptId}:${expectedEffect?.effectId ?? replayCompletion!.sourceEffectId}`;
|
|
716
|
+
const responseText = `automation:${input.status}:${receiptForEffect}`;
|
|
717
|
+
const responseSha256 = sha256(responseText);
|
|
718
|
+
const priorCompletion = expectedEffect ? undefined : this.store.getCompletionByResponseSha256(task.taskId, responseSha256);
|
|
719
|
+
const sourceEffect = expectedEffect ?? (priorCompletion ? this.store.getEffect(priorCompletion.sourceEffectId) : undefined);
|
|
720
|
+
if (!sourceEffect || sourceEffect.taskId !== task.taskId || !this.store.effectApplied(sourceEffect.effectId)) {
|
|
721
|
+
throw new Error('WORKFLOW_SUPERVISOR_CAUSAL_EFFECT_NOT_APPLIED');
|
|
722
|
+
}
|
|
723
|
+
const action = input.status === 'continue' ? 'CONTINUE' : input.status === 'done' ? 'DONE' : 'NEEDS_USER';
|
|
724
|
+
const proposal = {
|
|
725
|
+
action: action as 'CONTINUE' | 'DONE' | 'NEEDS_USER',
|
|
726
|
+
sourceEffectId: sourceEffect.effectId,
|
|
727
|
+
checkpoint: `automation:${receiptForEffect}`,
|
|
728
|
+
reason: 'automation_tool_receipt',
|
|
729
|
+
evidence: [],
|
|
730
|
+
conversationId: task.conversationId,
|
|
731
|
+
taskId: task.taskId,
|
|
732
|
+
supervisorState: action === 'CONTINUE' ? 'running' as const : action === 'DONE' ? 'done' as const : 'needs_user' as const,
|
|
733
|
+
};
|
|
734
|
+
const controlBlockSha256 = sha256(responseText);
|
|
735
|
+
const completionFingerprint = sha256(jsonIdentity(task.taskId, task.conversationId, sourceEffect.effectId, responseSha256, controlBlockSha256));
|
|
736
|
+
const completion: WorkflowSupervisorCompletion = { completionFingerprint, taskId: task.taskId, sourceEffectId: sourceEffect.effectId, action: proposal.action, responseSha256, controlBlockSha256, proposal, committedAt: new Date().toISOString() };
|
|
737
|
+
if (proposal.action === 'CONTINUE') {
|
|
738
|
+
const committed = this.reserveContinuation(task, completion);
|
|
739
|
+
return { action: 'CONTINUE', completionFingerprint, terminal: false, successorEffect: committed.successorEffect!, deduplicated: committed.deduplicated };
|
|
740
|
+
}
|
|
741
|
+
const committed = this.store.commitCompletion(completion);
|
|
742
|
+
const validation = proposal.action === 'DONE'
|
|
743
|
+
? await this.validators.completionContract(task, proposal)
|
|
744
|
+
: await this.validators.userBlockerPolicy(task, proposal);
|
|
745
|
+
const correctionId = validation.valid ? undefined : stableEffectId(`completion:${completionFingerprint}`);
|
|
746
|
+
const resolved = this.store.resolveTerminal({
|
|
747
|
+
completionFingerprint, taskId: task.taskId, action: proposal.action, accepted: validation.valid, reason: validation.reason,
|
|
748
|
+
...(correctionId ? { correction: { effectId: correctionId, prompt: this.renderPrompt(task, correctionId, 'correction', proposal.checkpoint, validation.reason) } } : {}),
|
|
749
|
+
});
|
|
750
|
+
return { action: proposal.action, completionFingerprint, terminal: validation.valid, ...(resolved.successorEffect ? { successorEffect: resolved.successorEffect } : {}), validation, deduplicated: committed.deduplicated || resolved.deduplicated };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Model CONTINUE and its causal successor commit in the same authority transaction. */
|
|
754
|
+
private reserveContinuation(task: WorkflowSupervisorTask, completion: WorkflowSupervisorCompletion) {
|
|
755
|
+
const nextId = stableEffectId(`completion:${completion.completionFingerprint}`);
|
|
756
|
+
const checkpoint = completion.proposal.reason === 'compact_receipt' ? undefined : completion.proposal.checkpoint;
|
|
757
|
+
return this.store.commitCompletion(completion, {
|
|
758
|
+
effectId: nextId,
|
|
759
|
+
kind: 'continuation',
|
|
760
|
+
prompt: this.renderPrompt(task, nextId, 'continuation', checkpoint),
|
|
761
|
+
// Rebuild from the same durable goal in a new logical conversation after
|
|
762
|
+
// a complete 15-receipt cycle; never migrate an uncompleted mutation.
|
|
763
|
+
...(task.continuationPolicy.kind === 'standalone_supervisor'
|
|
764
|
+
? { freshEnrollmentPrompt: this.renderPrompt(task, nextId, 'enrollment') } : {}),
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private renderPrompt(
|
|
769
|
+
task: WorkflowSupervisorTask,
|
|
770
|
+
effectId: string,
|
|
771
|
+
kind: WorkflowEffectKind,
|
|
772
|
+
checkpoint?: string,
|
|
773
|
+
correctionReason?: string,
|
|
774
|
+
lowerLayerContext?: string,
|
|
775
|
+
): string {
|
|
776
|
+
const canonicalObjective = this.hooks.canonicalObjectiveForTask?.(task)?.trim();
|
|
777
|
+
const promptTask = canonicalObjective ? { ...task, objective: canonicalObjective } : task;
|
|
778
|
+
const cognition = this.hooks.advisoryCognitionForTask?.(promptTask);
|
|
779
|
+
return renderSupervisorPrompt(promptTask, effectId, kind, checkpoint, correctionReason, lowerLayerContext, cognition);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private browserTaskActive(task: WorkflowSupervisorTask): boolean { return this.hooks.browserTaskActive?.(task) ?? true; }
|
|
783
|
+
private browserTaskActiveForExternalEffect(task: WorkflowSupervisorTask): boolean {
|
|
784
|
+
return this.store.hasAppliedEffectAwaitingCompletion(task.taskId) || this.browserTaskActive(task);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Progress is a mechanical change in the canonical Work this turn is meant to
|
|
789
|
+
* advance, never the provider's willingness to answer again. Both endpoints of
|
|
790
|
+
* the comparison are durable facts: the Work projection captured in this
|
|
791
|
+
* effect's dispatch evidence and the current projection. A missing signal on
|
|
792
|
+
* either side is unknown, so it proves neither progress nor its absence.
|
|
793
|
+
*/
|
|
794
|
+
private automationTurnChangedNothing(task: WorkflowSupervisorTask, effectId: string): boolean {
|
|
795
|
+
const dispatched = boundedBrowserText(this.store.latestEffectDispatch(effectId)?.evidence.work_progress_fingerprint, 256);
|
|
796
|
+
const current = boundedBrowserText(this.hooks.workProgressFingerprint?.(task), 256);
|
|
797
|
+
return Boolean(dispatched && current && dispatched === current);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private browserSnapshotMatchesSource(task: WorkflowSupervisorTask, effect: WorkflowSupervisorEffect, snapshot: { latestUserText: string; latestAssistantResponse: string }): boolean {
|
|
801
|
+
if (!effect.sourceCompletionFingerprint) return true;
|
|
802
|
+
const completion = this.store.getCompletion(effect.sourceCompletionFingerprint);
|
|
803
|
+
if (!completion || completion.taskId !== task.taskId) return false;
|
|
804
|
+
// Text-parsed completions are causally anchored to the exact assistant page
|
|
805
|
+
// response that produced the Supervisor receipt. Automation tool receipts are
|
|
806
|
+
// different: their responseSha256 hashes the synthetic durable receipt carrier,
|
|
807
|
+
// not text rendered in ChatGPT. The page can also collapse the prior automated
|
|
808
|
+
// prompt to ordinary visible prose, so neither the response hash nor its effect
|
|
809
|
+
// marker is a valid prerequisite for delivering the already-reserved successor.
|
|
810
|
+
// The durable receipt has already bound task, conversation, and source effect;
|
|
811
|
+
// retain that local causal fence and skip only the unavailable page-text proof.
|
|
812
|
+
const automationToolReceipt = completion.proposal.reason === 'automation_tool_receipt';
|
|
813
|
+
const sourceEffect = this.store.getEffect(completion.sourceEffectId);
|
|
814
|
+
if (!sourceEffect || sourceEffect.taskId !== task.taskId) return false;
|
|
815
|
+
if (automationToolReceipt) return true;
|
|
816
|
+
if (sha256(snapshot.latestAssistantResponse) !== completion.responseSha256) return false;
|
|
817
|
+
return normalizeBrowserText(snapshot.latestUserText) === normalizeBrowserText(sourceEffect.prompt)
|
|
818
|
+
|| browserTextHasEffect(snapshot.latestUserText, sourceEffect.effectId);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private requireTask(taskId: string): WorkflowSupervisorTask { const task = this.store.getTask(taskId); if (!task) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN'); return task; }
|
|
822
|
+
private requireBrowserTask(conversationId: string, conversationUrl: string): WorkflowSupervisorTask {
|
|
823
|
+
const observed = parseChatgptConversationIdentity(conversationUrl);
|
|
824
|
+
if (observed.conversationId !== conversationId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_CONVERSATION_MISMATCH');
|
|
825
|
+
const task = this.store.getTaskByConversationId(conversationId);
|
|
826
|
+
if (!task) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_CONVERSATION_NOT_ENROLLED');
|
|
827
|
+
const registered = parseChatgptConversationIdentity(task.conversationUrl);
|
|
828
|
+
// Project routing is presentation metadata. A ChatGPT conversation can move
|
|
829
|
+
// between `/c/<id>` and a Project-prefixed route without becoming a different
|
|
830
|
+
// provider conversation, so the durable id is the only identity fence here.
|
|
831
|
+
if (task.conversationId !== conversationId || registered.conversationId !== conversationId || observed.conversationId !== conversationId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_CONVERSATION_MISMATCH');
|
|
832
|
+
return task;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function boundedBrowserText(value: unknown, max: number): string | undefined { return typeof value === 'string' && Buffer.byteLength(value, 'utf8') <= max ? value : undefined; }
|
|
837
|
+
/**
|
|
838
|
+
* A task that already reached DONE/NEEDS_USER can never deliver another effect.
|
|
839
|
+
* Reserving into it reported `enrolled` and left the caller waiting for a turn
|
|
840
|
+
* that the terminal action forbids.
|
|
841
|
+
*/
|
|
842
|
+
function requireNonTerminalTask(store: WorkflowSupervisorStore, taskId: string): void {
|
|
843
|
+
const terminal = store.terminalAction(taskId);
|
|
844
|
+
if (terminal) throw new Error(`WORKFLOW_SUPERVISOR_TASK_TERMINAL:${terminal}`);
|
|
845
|
+
}
|
|
846
|
+
function browserSnapshot(evidence: Record<string, unknown> | undefined): { latestUserText: string; latestAssistantResponse: string } | undefined {
|
|
847
|
+
const latestUserText = boundedBrowserText(evidence?.latest_user_text, 128 * 1024);
|
|
848
|
+
const latestAssistantResponse = boundedBrowserText(evidence?.latest_assistant_response, 512 * 1024);
|
|
849
|
+
if (latestUserText === undefined || latestAssistantResponse === undefined) return undefined;
|
|
850
|
+
return { latestUserText, latestAssistantResponse };
|
|
851
|
+
}
|
|
852
|
+
function normalizeBrowserText(value: string): string { return value.replace(/\s+/g, ' ').trim(); }
|
|
853
|
+
function browserTextSha256(value: string): string { return sha256(normalizeBrowserText(value)); }
|
|
854
|
+
function browserTextHasEffect(value: string, effectId: string): boolean { return value.includes(renderEffectMarker(effectId)); }
|
|
855
|
+
const PERSISTED_BROWSER_EVIDENCE_KEYS = new Set(['exact_user_message', 'observation_fingerprint', 'reconciliation', 'reason', 'surface', 'target_marker_present']);
|
|
856
|
+
function sanitizeBrowserEvidence(evidence: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
857
|
+
if (!evidence) return {};
|
|
858
|
+
const sanitized: Record<string, unknown> = {};
|
|
859
|
+
for (const [key, value] of Object.entries(evidence)) {
|
|
860
|
+
if (!PERSISTED_BROWSER_EVIDENCE_KEYS.has(key)) continue;
|
|
861
|
+
if (typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) sanitized[key] = value;
|
|
862
|
+
else if (typeof value === 'string' && value.length <= 512) sanitized[key] = value;
|
|
863
|
+
}
|
|
864
|
+
return sanitized;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function browserTask(task: WorkflowSupervisorTask): WorkflowSupervisorBrowserTask { return { taskId: task.taskId, conversationId: task.conversationId, conversationUrl: task.conversationUrl }; }
|
|
868
|
+
function jsonIdentity(...values: string[]): string { return JSON.stringify(values); }
|