@moretea-labs/forge 1.7.2 → 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 +121 -62
- package/CHANGELOG.md +40 -0
- package/CLAUDE.md +57 -62
- package/CONTRIBUTING.md +3 -2
- package/README.md +2 -2
- package/SKILL.md +0 -1
- package/THIRD_PARTY_NOTICES.md +5 -2
- package/adapters/browser/index.ts +2 -0
- package/adapters/chatgpt/browser-delivery-host.ts +77 -0
- package/adapters/chatgpt/browser-delivery-runtime.ts +1256 -0
- package/adapters/chatgpt/controller-binding-store.ts +134 -0
- package/adapters/chatgpt/controller-round-host.ts +68 -0
- package/adapters/chatgpt/controller-round-settlement-store.ts +99 -0
- package/adapters/chatgpt/provider-delivery.ts +340 -0
- package/adapters/chatgpt/work-conversation-binding-store.ts +218 -0
- package/adapters/chatgpt/wsl-bridge-delivery-host.ts +63 -0
- 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-contract.ts +7 -0
- package/adapters/computer/desktop-operator-discovery.ts +181 -0
- package/adapters/computer/desktop-operator-negotiation.ts +174 -0
- package/adapters/computer/desktop-operator-provider.ts +312 -0
- package/adapters/computer/index.ts +1 -0
- package/adapters/computer/interaction-target-authority.ts +1068 -0
- package/adapters/controller-process/binding-store.ts +34 -0
- package/adapters/controller-process/controller-host.ts +26 -0
- package/adapters/mcp/access-mode.ts +132 -0
- package/adapters/mcp/audit.ts +32 -0
- package/adapters/mcp/auth.ts +406 -0
- package/adapters/mcp/instructions.ts +37 -0
- package/adapters/mcp/legacy-context.ts +18 -0
- package/adapters/mcp/multi-repository.ts +421 -0
- package/adapters/mcp/oauth.ts +464 -0
- package/adapters/mcp/paths.ts +143 -0
- package/adapters/mcp/policy.ts +232 -0
- package/adapters/mcp/proxy-env.ts +62 -0
- package/adapters/mcp/readiness-projection.ts +24 -0
- package/adapters/mcp/redaction.ts +21 -0
- package/adapters/mcp/repo.ts +24 -0
- package/adapters/mcp/runtime-gateway/artifact-adapter.ts +52 -0
- package/adapters/mcp/runtime-gateway/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 +1167 -0
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
- package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +206 -0
- package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
- package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +130 -0
- package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
- package/adapters/mcp/runtime-gateway/execution-control-adapter.ts +28 -0
- package/adapters/mcp/runtime-gateway/execution-tools.ts +173 -0
- package/adapters/mcp/runtime-gateway/filesystem-adapter.ts +38 -0
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +412 -0
- package/adapters/mcp/runtime-gateway/legacy-ios-tool-adapter.ts +105 -0
- package/adapters/mcp/runtime-gateway/maintenance-adapter.ts +44 -0
- package/adapters/mcp/runtime-gateway/model-adapter.ts +63 -0
- package/adapters/mcp/runtime-gateway/persisted-check-process.ts +3 -0
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +380 -0
- package/adapters/mcp/runtime-gateway/process-tools.ts +457 -0
- package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +88 -0
- package/adapters/mcp/runtime-gateway/recovery-adapter.ts +285 -0
- package/adapters/mcp/runtime-gateway/recovery-client-adapter.ts +70 -0
- package/adapters/mcp/runtime-gateway/recovery-tool-contract.ts +99 -0
- package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +79 -0
- package/adapters/mcp/runtime-gateway/result-adapter.ts +114 -0
- package/adapters/mcp/runtime-gateway/router.ts +242 -0
- package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1200 -0
- package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +574 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-shared.ts +27 -0
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +78 -0
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +187 -0
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +45 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +962 -0
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +325 -0
- 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-validation-reconciler.ts +1 -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 +1092 -0
- package/adapters/mcp/setup.ts +789 -0
- package/adapters/mcp/tool-capability-cache.ts +87 -0
- package/adapters/mcp/tool-mapping/access-tools.ts +387 -0
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +5139 -0
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +145 -0
- package/adapters/mcp/tool-mapping/repository-tools.ts +1682 -0
- package/adapters/mcp/tool-mapping/tools.ts +9 -0
- package/adapters/mcp/tool-mapping/umbrella-tools.ts +117 -0
- package/adapters/mcp/toolset-names.ts +79 -0
- package/adapters/mcp/toolset-selection.ts +41 -0
- package/adapters/mcp/toolset.ts +337 -0
- package/adapters/mcp/transports/http-observation.ts +365 -0
- package/adapters/mcp/transports/http.ts +933 -0
- package/adapters/mcp/transports/oauth-http.ts +322 -0
- package/adapters/mcp/transports/session-registry.ts +519 -0
- package/adapters/mcp/transports/stdio.ts +8 -0
- package/adapters/mcp/tunnels/openai-secure-tunnel.ts +223 -0
- package/adapters/mcp/types.ts +41 -0
- package/assets/AGENTS.md +2 -16
- package/assets/CLAUDE.md +2 -16
- package/assets/hooks/AGENTS.md +3 -14
- package/assets/hooks/CLAUDE.md +3 -14
- package/assets/hooks/hook-input.sh +9 -2
- package/assets/hooks/lib/workflow-state.sh +28 -10
- package/assets/hooks/prompt-guard.sh +63 -37
- package/assets/hooks/security-sentinel.sh +2 -1
- package/assets/hooks/session-start-context.sh +7 -5
- package/assets/hooks/stop-orchestrator.sh +13 -3
- package/assets/initializer-question-pack.v4.json +11 -7
- package/assets/partials/05-workflow.partial.md +18 -73
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -2
- package/assets/partials-agents/04-task-protocol.partial.md +14 -59
- package/assets/partials-agents/06-quality-safety.partial.md +11 -16
- package/assets/partials-agents/08-deep-docs.partial.md +1 -2
- package/assets/plan-map.json +11 -11
- package/assets/plugin-registry.v1.json +11 -3
- package/assets/plugin-trust-policy.v1.json +6 -0
- package/assets/recovery/ForgeRecovery.ps1 +100 -0
- package/assets/recovery/forge-wsl-rescue +271 -0
- package/assets/reference-configs/agentic-development-flow.md +25 -0
- package/assets/reference-configs/development-protocol.md +11 -15
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/global-working-rules.md +28 -1
- package/assets/reference-configs/harness-overview.md +22 -112
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/architecture-event.ts +3 -34
- package/assets/templates/helpers/architecture-queue.sh +61 -10
- package/assets/templates/helpers/archive-workflow.sh +90 -180
- package/assets/templates/helpers/capability-config.ts +8 -11
- package/assets/templates/helpers/capability-resolver.ts +37 -19
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +1 -9
- package/assets/templates/helpers/check-task-sync.sh +82 -11
- package/assets/templates/helpers/check-task-workflow.sh +1 -3
- package/assets/templates/helpers/context-contract-sync.sh +1 -12
- package/assets/templates/helpers/contract-worktree.sh +26 -24
- package/assets/templates/helpers/ensure-task-workflow.sh +6 -8
- package/assets/templates/helpers/inspect-project-state.ts +1 -1
- package/assets/templates/helpers/migrate-project-template.sh +1 -1
- package/assets/templates/helpers/migrate-workflow-docs.ts +53 -239
- package/assets/templates/helpers/new-plan.sh +2 -2
- package/assets/templates/helpers/plan-to-todo.sh +3 -31
- package/assets/templates/helpers/sprint-backlog.sh +1 -1
- package/assets/templates/helpers/switch-plan.sh +1 -1
- package/assets/templates/helpers/workflow-contract.ts +1 -0
- package/assets/workflow-contract.v1.json +63 -171
- package/assets/workflows/xiaohongshu/xiaohongshu.collect-metrics.v1.draft.json +118 -0
- package/assets/workflows/xiaohongshu/xiaohongshu.publish-image-note.v1.draft.json +316 -0
- package/assets/workflows/xiaohongshu/xiaohongshu.publish-long-text.v1.draft.json +324 -0
- package/docs/architecture/CURRENT.md +233 -28
- package/docs/architecture/EVOLUTION.md +13 -0
- package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +2 -1
- 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/20260831-independent-windows-wsl-host-rescue.md +59 -0
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +120 -0
- package/docs/architecture/decisions/20260904-repository-runtime-data-boundary.md +42 -0
- package/docs/architecture/decisions/20260907-assistant-learning-loop.md +37 -0
- package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +311 -0
- package/docs/architecture/decisions/20260907-maintenance-diagnosis-bounded-read.md +39 -0
- 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 +8 -0
- package/docs/architecture/domains/workflow-engine.md +7 -1
- package/docs/architecture/index.md +3 -3
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +19 -8
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +124 -0
- package/docs/forge-chatgpt-mcp-setup.md +5 -3
- package/docs/forge-plugin-management.md +20 -4
- package/docs/forge-runtime-self-healing-loop.md +2 -0
- package/docs/operations/benchmark-cross-version-freeze.md +38 -0
- package/docs/operations/controller-browser-plugin.md +3 -1
- package/docs/operations/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/platform-support.md +16 -8
- package/docs/operations/platform-support.zh-CN.md +2 -2
- package/docs/operations/releasing.md +2 -0
- package/docs/operations/standalone-disaster-recovery.md +38 -5
- package/docs/operations/uu-remote-rescue.md +41 -0
- package/docs/operations/windows-wsl-host-rescue.md +133 -0
- package/docs/public-usage-guide.md +1 -1
- package/docs/public-usage-guide.zh-CN.md +1 -1
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/02-connect-chatgpt.md +4 -4
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +4 -4
- package/docs/wiki/Implementation.md +1 -1
- package/package.json +46 -26
- 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 +3 -0
- package/packages/kernel/controller/application/continuation-service.ts +281 -0
- package/packages/kernel/controller/application/controller-round-service.ts +41 -0
- package/packages/kernel/controller/application/controller-service.ts +30 -0
- package/packages/kernel/controller/domain/controller-round-transition-policy.ts +529 -0
- package/packages/kernel/controller/domain/controller-round.ts +110 -0
- package/packages/kernel/controller/domain/execution-quality.ts +135 -0
- package/packages/kernel/controller/domain/types.ts +56 -0
- package/packages/kernel/controller/index.ts +1 -0
- package/packages/kernel/controller/infrastructure/controller-binding-store.ts +84 -0
- package/packages/kernel/controller/infrastructure/controller-round-store.ts +1970 -0
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +804 -0
- package/packages/kernel/controller/ports/controller-host.ts +18 -0
- package/packages/kernel/goal/api/index.ts +1 -0
- package/packages/kernel/goal/domain/types.ts +40 -0
- package/packages/kernel/goal/index.ts +1 -0
- package/packages/kernel/identity/api/index.ts +15 -0
- package/packages/kernel/identity/application/identity-service.ts +67 -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/scope.ts +63 -0
- package/packages/kernel/identity/domain/semantic-sync.ts +81 -0
- package/packages/kernel/identity/domain/types.ts +44 -0
- package/packages/kernel/identity/domain/user-request.ts +93 -0
- package/packages/kernel/identity/domain/workspace.ts +56 -0
- package/packages/kernel/identity/index.ts +1 -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/identity-store.ts +75 -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/memory/api/index.ts +5 -0
- package/packages/kernel/memory/application/experience-service.ts +118 -0
- package/packages/kernel/memory/application/outcome-service.ts +77 -0
- package/packages/kernel/memory/domain/experience.ts +103 -0
- package/packages/kernel/memory/domain/operational-prior.ts +92 -0
- package/packages/kernel/memory/index.ts +1 -0
- package/packages/kernel/scheduler/api/index.ts +4 -0
- package/packages/kernel/scheduler/application/eligibility.ts +252 -0
- package/packages/kernel/scheduler/application/schedule-service.ts +27 -0
- package/packages/kernel/scheduler/application/settlement.ts +182 -0
- package/packages/kernel/scheduler/domain/schedule.ts +236 -0
- package/packages/kernel/scheduler/index.ts +1 -0
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +805 -0
- package/packages/kernel/work/api/index.ts +16 -0
- package/packages/kernel/work/application/work-service.ts +123 -0
- package/packages/kernel/work/domain/check-receipt.ts +43 -0
- package/packages/kernel/work/domain/context-closure-receipt.ts +32 -0
- package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
- package/packages/kernel/work/domain/engineering-contracts.ts +157 -0
- package/packages/kernel/work/domain/engineering-design.ts +286 -0
- package/packages/kernel/work/domain/engineering-profile.ts +238 -0
- package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
- package/packages/kernel/work/domain/execution-concurrency.ts +247 -0
- package/packages/kernel/work/domain/execution-snapshot.ts +2 -0
- package/packages/kernel/work/domain/implementation-review.ts +193 -0
- package/packages/kernel/work/domain/objective-graph.ts +119 -0
- package/packages/kernel/work/domain/project-engineering-contract.ts +224 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +73 -0
- package/{src/runtime/control-plane/facade/work-state-machine.ts → packages/kernel/work/domain/state-machine.ts} +37 -66
- package/packages/kernel/work/domain/types.ts +534 -0
- package/packages/kernel/work/index.ts +1 -0
- package/packages/kernel/work/infrastructure/work-contract-store.ts +1967 -0
- package/packages/kernel/work/ports/work-contract-store.ts +21 -0
- package/packages/plugin-runtime/browser/index.ts +3 -0
- package/packages/plugin-runtime/browser/provider-registry.ts +110 -0
- package/packages/plugin-runtime/browser/runtime-contract.ts +150 -0
- package/packages/plugin-runtime/browser/session-authority.ts +62 -0
- 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 +8 -0
- package/packages/plugin-runtime/computer/provider-error.ts +26 -0
- package/packages/plugin-runtime/computer/provider-registration.ts +52 -0
- package/packages/plugin-runtime/computer/provider-registry.ts +63 -0
- package/packages/plugin-runtime/computer/provider.ts +19 -0
- package/packages/plugin-runtime/computer/target-authority.ts +215 -0
- package/packages/plugin-runtime/computer/target-persistence.ts +54 -0
- package/packages/plugin-runtime/external/index.ts +1 -0
- package/packages/plugin-runtime/external/unix-jsonl-transport.ts +581 -0
- package/packages/plugin-runtime/index.ts +3 -0
- package/packages/protocols/browser/index.ts +1 -0
- package/packages/protocols/browser/session.ts +98 -0
- package/packages/protocols/computer/contract.ts +225 -0
- package/packages/protocols/computer/index.ts +1 -0
- package/packages/protocols/failure/contract.ts +158 -0
- package/packages/protocols/failure/index.ts +1 -0
- package/packages/protocols/handoff/index.ts +1 -0
- package/packages/protocols/handoff/status.ts +22 -0
- package/packages/protocols/mcp/execution-context.ts +11 -0
- package/packages/protocols/mcp/index.ts +2 -0
- package/packages/protocols/mcp/tool-contract.ts +42 -0
- package/packages/workflow-runtime/api/index.ts +2 -0
- package/packages/workflow-runtime/application/interpreter.ts +342 -0
- package/packages/workflow-runtime/domain/workflow-asset.ts +223 -0
- package/packages/workflow-runtime/index.ts +1 -0
- package/scripts/activate-source-baseline.ts +7 -9
- package/scripts/assemble-template.ts +1 -1
- package/scripts/bootstrap-runtime-maintenance-recovery.sh +4 -1
- package/scripts/canary-chatgpt-provider-delivery.ts +97 -0
- package/scripts/canary-controller-semantic-wait.ts +183 -0
- package/scripts/canary-global-hook.sh +2 -2
- package/scripts/check-mcp-compatibility.ts +85 -7
- package/scripts/check-platform-support.mjs +28 -2
- package/scripts/check-public-docs.sh +1 -1
- package/scripts/check-repository-hygiene.mjs +92 -0
- package/scripts/check-runtime-architecture.mjs +1643 -127
- package/scripts/check-source-duplication.mjs +9 -1
- package/scripts/controller-ngrok-rotation.sh +5 -4
- package/scripts/create-project-dirs.sh +10 -74
- package/scripts/forge-local-recovery-helper.mjs +282 -0
- package/scripts/forge-uu-remote-rescue-helper.d.mts +31 -0
- package/scripts/forge-uu-remote-rescue-helper.mjs +409 -0
- package/scripts/forge-windows-host-recovery-helper.mjs +136 -0
- package/scripts/hook-shim.sh +2 -2
- package/scripts/init-project.sh +12 -70
- package/scripts/install-desktop-operator-registration.ts +4 -2
- package/scripts/install-independent-windows-wsl-recovery.ts +125 -0
- package/scripts/install-standalone-recovery.ts +122 -10
- package/scripts/install-uu-remote-rescue-registration.ts +74 -0
- package/scripts/install-windows-host-recovery-registration.ts +22 -0
- package/scripts/lib/controller-home-env.sh +6 -3
- package/scripts/lib/project-init-lib.sh +42 -582
- package/scripts/migrate-chatgpt-automation-schedules.ts +2 -2
- package/scripts/migrate-windows-wsl-controller-home.ts +306 -0
- package/scripts/open-source-audit-allowlist.txt +11 -1
- package/scripts/portability-bindings.mjs +70 -0
- package/scripts/run-bounded-command.mjs +60 -0
- package/scripts/run-bun-test-file.ts +22 -6
- package/scripts/run-governed-gate.ts +4 -2
- 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 +6 -1
- package/scripts/smoke-runtime-recovery.ts +13 -11
- package/scripts/smoke-schedule-engine.ts +24 -16
- 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/test-governance.ts +51 -2
- 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 +17 -4
- package/src/cli/chatgpt-browser/bind-server.ts +2 -1
- package/src/cli/chatgpt-browser/binding.ts +20 -10
- package/src/cli/chatgpt-browser/bridge-extension.ts +75 -0
- package/src/cli/chatgpt-browser/bridge-provider.ts +376 -11
- package/src/cli/chatgpt-browser/engine.ts +10 -11
- package/src/cli/chatgpt-browser/native-provider.ts +24 -83
- package/src/cli/chatgpt-browser/oracle-provider.ts +5 -3
- package/src/cli/chatgpt-browser/types.ts +1 -0
- package/src/cli/commands/bootstrap-control.ts +334 -0
- package/src/cli/commands/brain-assistant.ts +52 -0
- package/src/cli/commands/brain.ts +191 -40
- package/src/cli/commands/chatgpt.ts +32 -67
- package/src/cli/commands/computer.ts +437 -0
- package/src/cli/commands/doctor.ts +18 -1
- package/src/cli/commands/init-hook.ts +71 -61
- package/src/cli/commands/init.ts +3 -1
- package/src/cli/commands/mcp.ts +16 -10
- package/src/cli/commands/plugin.ts +38 -12
- package/src/cli/commands/recovery.ts +507 -80
- package/src/cli/commands/runtime.ts +12 -7
- package/src/cli/commands/setup-profile.ts +97 -39
- package/src/cli/commands/status.ts +2 -1
- package/src/cli/controller/check-runner.ts +500 -100
- 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/types.ts +7 -1
- package/src/cli/controller/context-pack.ts +28 -6
- 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 +148 -0
- package/src/cli/controller/types.ts +10 -70
- package/src/cli/controller/worklog.ts +67 -13
- package/src/cli/editing/edit-operation-contract.ts +92 -0
- package/src/cli/editing/edit-session.ts +290 -20
- package/src/cli/github/plugin.ts +17 -12
- package/src/cli/hook/route-registry.ts +3 -5
- package/src/cli/hook/runtime.ts +49 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/local-bridge/connector-freshness.ts +2 -2
- package/src/cli/local-bridge/console-automations.ts +20 -15
- package/src/cli/local-bridge/console-view-models.ts +3 -12
- package/src/cli/local-bridge/facade-api.ts +105 -205
- package/src/cli/local-bridge/job-store.ts +1 -1
- package/src/cli/local-bridge/server.ts +48 -174
- package/src/cli/local-bridge/types.ts +0 -1
- package/src/cli/mcp/access-mode.ts +2 -132
- package/src/cli/mcp/access-tools.ts +2 -387
- package/src/cli/mcp/audit.ts +2 -32
- package/src/cli/mcp/auth.ts +2 -404
- package/src/cli/mcp/instructions.ts +2 -34
- package/src/cli/mcp/legacy-context.ts +2 -16
- package/src/cli/mcp/legacy-tool-service.ts +2 -5308
- package/src/cli/mcp/multi-repository.ts +2 -436
- package/src/cli/mcp/oauth-client-retirement.ts +74 -0
- package/src/cli/mcp/oauth.ts +2 -375
- package/src/cli/mcp/openai-secure-tunnel.ts +2 -0
- package/src/cli/mcp/paths.ts +2 -140
- package/src/cli/mcp/policy.ts +2 -232
- package/src/cli/mcp/post-finalize-work-attribution.ts +2 -150
- package/src/cli/mcp/proxy-env.ts +2 -62
- package/src/cli/mcp/readiness-projection.ts +2 -24
- package/src/cli/mcp/redaction.ts +2 -21
- package/src/cli/mcp/repo.ts +2 -24
- package/src/cli/mcp/repository-tools.ts +2 -1613
- package/src/cli/mcp/server.ts +2 -951
- package/src/cli/mcp/setup.ts +2 -737
- package/src/cli/mcp/tool-capability-cache.ts +2 -87
- package/src/cli/mcp/tool-contract.ts +2 -14
- package/src/cli/mcp/tools.ts +2 -9
- package/src/cli/mcp/toolset-names.ts +2 -70
- package/src/cli/mcp/toolset-selection.ts +2 -41
- package/src/cli/mcp/toolset.ts +2 -336
- package/src/cli/mcp/transports/AGENTS.md +1 -10
- package/src/cli/mcp/transports/CLAUDE.md +1 -10
- package/src/cli/mcp/transports/http.ts +2 -1414
- package/src/cli/mcp/transports/session-registry.ts +2 -490
- package/src/cli/mcp/transports/stdio.ts +2 -8
- package/src/cli/mcp/types.ts +2 -41
- package/src/cli/mcp/umbrella-tools.ts +2 -117
- package/src/cli/repositories/command-executor.ts +91 -39
- package/src/cli/repositories/command-scope.ts +139 -4
- package/src/cli/repositories/controller-home.ts +167 -1
- package/src/cli/repositories/ephemeral-workspace.ts +20 -30
- package/src/cli/repositories/git-command-executor.ts +2 -2
- package/src/cli/repositories/identity.ts +13 -0
- package/src/cli/repositories/registry.ts +145 -60
- package/src/cli/repositories/repository-snapshot.ts +53 -7
- package/src/cli/repositories/runtime-storage.ts +27 -2
- package/src/cli/repositories/safe-patch.ts +34 -38
- package/src/cli/repositories/selected-path-actions.ts +22 -2
- package/src/cli/repositories/structured-git.ts +167 -27
- package/src/cli/repository/session-cache.ts +92 -6
- package/src/cli/runtime/helper-runner.ts +7 -0
- package/src/cli/runtime-invocation.ts +8 -1
- package/src/cli/tools/codegraph.ts +44 -25
- package/src/core/adoption/manifest-templates.ts +1 -1
- package/src/core/adoption/plan.ts +3 -26
- package/src/core/adoption/workflow-contract-plan.ts +17 -9
- package/src/effects/process-runner.ts +8 -14
- package/src/runtime/context/assistant-context.ts +167 -0
- package/src/runtime/context/assistant-work-context.ts +209 -0
- package/src/runtime/context/automatic-learning.ts +565 -0
- package/src/runtime/context/codegraph-cache-boundary.ts +255 -0
- package/src/runtime/context/codegraph-read-provider.ts +55 -22
- package/src/runtime/context/codegraph-sidecar.cjs +8 -1
- package/src/runtime/context/cognitive-attention.ts +173 -0
- package/src/runtime/context/cognitive-skill-canary.ts +158 -0
- package/src/runtime/context/context-closure.ts +305 -0
- package/src/runtime/context/context-pack-process.ts +103 -0
- package/src/runtime/context/context-plane.ts +203 -0
- package/src/runtime/context/direct-controller-learning.ts +298 -0
- package/src/runtime/context/project-engineering-contract.ts +60 -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/bootstrap/capability-provider.ts +74 -0
- package/src/runtime/control-plane/bootstrap/control-api.ts +57 -0
- package/src/runtime/control-plane/bootstrap/index.ts +5 -0
- package/src/runtime/control-plane/bootstrap/state-machine.ts +141 -0
- package/src/runtime/control-plane/bootstrap/store.ts +21 -0
- package/src/runtime/control-plane/bootstrap/types.ts +87 -0
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +531 -0
- package/src/runtime/control-plane/codegraph-cache-retention.ts +164 -0
- package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +282 -0
- package/src/runtime/control-plane/execution/controller-authority-recovery.ts +380 -0
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +113 -16
- package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
- package/src/runtime/control-plane/execution/implementation-review-content.ts +95 -0
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
- package/src/runtime/control-plane/execution/retained-work-resume.ts +439 -102
- package/src/runtime/control-plane/execution/session-store.ts +31 -1
- package/src/runtime/control-plane/execution/validation.ts +34 -11
- package/src/runtime/control-plane/execution/work-authority-reconciler.ts +87 -0
- package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +31 -44
- package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +46 -53
- package/src/runtime/control-plane/execution/work-execution-support.ts +85 -164
- package/src/runtime/control-plane/execution/work-handle-authority.ts +349 -73
- package/src/runtime/control-plane/execution/work-handle-store.ts +55 -2
- package/src/runtime/control-plane/execution/work-head-settlement.ts +108 -0
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +173 -28
- package/src/runtime/control-plane/execution/work-operation-service.ts +92 -40
- package/src/runtime/control-plane/execution/work-preparation-service.ts +145 -269
- 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 +543 -225
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +59 -45
- package/src/runtime/control-plane/execution/work-verification-context.ts +1 -1
- package/src/runtime/control-plane/execution/work-verification-service.ts +662 -52
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +205 -16
- package/src/runtime/control-plane/facade/capability-registry.ts +380 -44
- 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/controller-session-store.ts +2 -488
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +6 -22
- package/src/runtime/control-plane/facade/goal-workloop.ts +565 -1202
- package/src/runtime/control-plane/facade/handoff-inbox-application.ts +145 -0
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +266 -52
- package/src/runtime/control-plane/facade/index.ts +4 -3
- package/src/runtime/control-plane/facade/operation-digest.ts +34 -73
- package/src/runtime/control-plane/facade/plan-contract-store.ts +671 -245
- package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
- package/src/runtime/control-plane/facade/repository-work-admission.ts +31 -23
- package/src/runtime/control-plane/facade/requirement-authority.ts +121 -0
- package/src/runtime/control-plane/facade/requirement-board.ts +9 -4
- package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +85 -0
- package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
- package/src/runtime/control-plane/facade/semantic-admission.ts +8 -16
- package/src/runtime/control-plane/facade/suggested-actions.ts +15 -15
- package/src/runtime/control-plane/facade/types.ts +218 -620
- package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
- package/src/runtime/control-plane/facade/work-contract-store.ts +2 -1043
- package/src/runtime/control-plane/generated-cache-retention.ts +336 -19
- package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +13 -2
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +274 -84
- package/src/runtime/control-plane/global-scheduler/periodic-cleanup-entry.ts +55 -0
- package/src/runtime/control-plane/global-scheduler/periodic-cleanup-process.ts +98 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -2
- package/src/runtime/control-plane/global-scheduler/scheduler-worker-entry.ts +10 -0
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +227 -109
- package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +18 -6
- 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 +160 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +2 -177
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +139 -670
- package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +25 -6
- package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -194
- package/src/runtime/control-plane/lifecycle-retention-metrics.ts +60 -0
- package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
- package/src/runtime/control-plane/persistence/experience-store.ts +302 -0
- package/src/runtime/control-plane/persistence/operational-prior-store.ts +348 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +230 -80
- package/src/runtime/control-plane/persistence/sqlite-store.ts +654 -78
- package/src/runtime/control-plane/persistence/workflow-content-store.ts +98 -0
- package/src/runtime/control-plane/persistence/workflow-registry-store.ts +186 -0
- package/src/runtime/control-plane/persistence/workflow-run-store.ts +123 -0
- package/src/runtime/control-plane/release-retention.ts +154 -26
- package/src/runtime/control-plane/repository-namespace-retention.ts +208 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +844 -38
- package/src/runtime/control-plane/runtime-generation.ts +6 -0
- package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
- package/src/runtime/control-plane/workspace/semantic-sync-service.ts +479 -0
- package/src/runtime/control-plane/workspace/workspace-store.ts +249 -0
- package/src/runtime/diagnostics/incident-repair.ts +573 -0
- package/src/runtime/diagnostics/mcp-timing.ts +239 -6
- package/src/runtime/diagnostics/performance.ts +6 -14
- package/src/runtime/diagnostics/read-only-tool.ts +1 -1
- package/src/runtime/evidence/artifact-store.ts +216 -35
- package/src/runtime/evidence/evidence-store.ts +112 -8
- package/src/runtime/evidence/operational-shadow.ts +38 -0
- package/src/runtime/evidence/output-handle-index.ts +225 -0
- package/src/runtime/evidence/process-check-receipt.ts +5 -43
- package/src/runtime/evidence/result-store.ts +158 -44
- package/src/runtime/evidence/sensitive-output.ts +4 -4
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -1
- package/src/runtime/execution/jobs/store.ts +124 -6
- package/src/runtime/execution/managed-workspace.ts +112 -5
- package/src/runtime/execution/process-runtime/canary.ts +19 -5
- package/src/runtime/execution/process-runtime/check-classification.ts +48 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +24 -10
- package/src/runtime/execution/process-runtime/check-receipt.ts +1 -1
- package/src/runtime/execution/process-runtime/check-result-retention.ts +132 -0
- package/src/runtime/execution/process-runtime/check-result.ts +177 -2
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +58 -9
- package/src/runtime/execution/process-runtime/check-storage.ts +187 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +117 -114
- 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 +4 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +2 -1
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +283 -73
- package/src/runtime/execution/process-runtime/persisted-check.ts +130 -23
- 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 +125 -64
- package/src/runtime/execution/process-runtime/runtime.ts +167 -17
- package/src/runtime/execution/process-runtime/store.ts +30 -2
- package/src/runtime/execution/process-runtime/types.ts +27 -0
- package/src/runtime/execution/workers/executor.ts +18 -19
- package/src/runtime/execution/workers/worker-entry.ts +1 -1
- package/src/runtime/gateway/mcp/execution-tools.ts +2 -207
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +2 -61
- package/src/runtime/gateway/mcp/persisted-check-process.ts +2 -3
- package/src/runtime/gateway/mcp/process-tools.ts +2 -289
- package/src/runtime/gateway/mcp/router.ts +2 -1125
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +2 -565
- package/src/runtime/gateway/mcp/runtime-tools.ts +2 -6655
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +2 -1
- package/src/runtime/maintenance/cleanup.ts +38 -24
- package/src/runtime/platform/browser-product-discovery.ts +174 -0
- package/src/runtime/platform/computer-platform.ts +14 -0
- package/src/runtime/platform/executable-discovery.ts +54 -0
- package/src/runtime/platform/fetch-http-server.ts +90 -0
- package/src/runtime/platform/service-inventory.ts +130 -0
- package/src/runtime/platform/service-manager.ts +146 -0
- package/src/runtime/platform/windows-host-discovery.ts +118 -0
- package/src/runtime/platform/windows-recovery-deployment.ts +67 -0
- package/src/runtime/plugins/action-application.ts +192 -0
- package/src/runtime/plugins/app-store-connect-adapter.ts +47 -35
- package/src/runtime/plugins/browser-adapter.ts +898 -135
- package/src/runtime/plugins/browser-automation-service.ts +33 -34
- package/src/runtime/plugins/browser-macos-bridge.ts +386 -46
- package/src/runtime/plugins/browser-manifest-surface.ts +75 -17
- package/src/runtime/plugins/browser-native-session-service.ts +64 -14
- package/src/runtime/plugins/browser-provider-registry.ts +2 -109
- package/src/runtime/plugins/browser-registration.ts +32 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -154
- package/src/runtime/plugins/browser-runtime.ts +55 -10
- package/src/runtime/plugins/browser-session-authority.ts +101 -291
- package/src/runtime/plugins/browser-session-legacy-migration.ts +203 -0
- package/src/runtime/plugins/browser-session-store.ts +243 -80
- package/src/runtime/plugins/browser-session-types.ts +2 -98
- package/src/runtime/plugins/capability-authorization-grants.ts +272 -59
- package/src/runtime/plugins/catalog-trust.ts +82 -0
- 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 +1050 -0
- package/src/runtime/plugins/config-store.ts +119 -0
- package/src/runtime/plugins/desktop-operator-external-policy.ts +964 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +123 -12
- package/src/runtime/plugins/errors.ts +8 -1
- package/src/runtime/plugins/external-adapter.ts +82 -323
- package/src/runtime/plugins/external-provider-policy.ts +48 -0
- package/src/runtime/plugins/external-registration.ts +30 -2
- package/src/runtime/plugins/external-unix-socket-probe.cjs +9 -3
- package/src/runtime/plugins/external-unix-socket.ts +64 -193
- package/src/runtime/plugins/first-party-external-registration.ts +163 -0
- package/src/runtime/plugins/first-party-registry.ts +3 -1
- package/src/runtime/plugins/github-adapter.ts +1 -2
- package/src/runtime/plugins/gmail-adapter.ts +29 -8
- package/src/runtime/plugins/google-calendar-adapter.ts +8 -8
- package/src/runtime/plugins/google-shared.ts +45 -53
- package/src/runtime/plugins/google-tasks-adapter.ts +8 -8
- package/src/runtime/plugins/ios-adapter.ts +2 -3
- package/src/runtime/plugins/lightweight-action.ts +208 -2
- package/src/runtime/plugins/local-system-adapter.ts +192 -2
- package/src/runtime/plugins/macos-capability-broker.ts +15 -105
- 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/resend-adapter.ts +15 -24
- package/src/runtime/plugins/store.ts +1003 -387
- package/src/runtime/plugins/types.ts +28 -1
- package/src/runtime/plugins/uu-remote-rescue-registration.ts +155 -0
- package/src/runtime/plugins/windows-host-recovery-install.ts +68 -0
- package/src/runtime/plugins/windows-host-recovery-registration.ts +85 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +102 -430
- package/src/runtime/projections/controller-context.ts +12 -6
- package/src/runtime/projections/git-status-sampler.ts +73 -22
- package/src/runtime/projections/invalidation.ts +7 -3
- package/src/runtime/projections/materialized-view.ts +93 -43
- package/src/runtime/recovery/actions.ts +3 -3
- package/src/runtime/recovery/capability-action-application.ts +229 -0
- package/src/runtime/recovery/index.ts +3 -0
- package/src/runtime/recovery/lifecycle-inventory.ts +338 -0
- package/src/runtime/recovery/local-jobs-repair.ts +14 -14
- package/src/runtime/recovery/maintenance-executor.ts +701 -168
- package/src/runtime/recovery/quarantine-retention.ts +256 -0
- package/src/runtime/recovery/store.ts +29 -11
- package/src/runtime/release/identity.ts +6 -0
- 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/active-execution-power-assertion.ts +139 -0
- package/src/runtime/root/assistant-learning-loop.ts +45 -0
- package/src/runtime/root/browser-session-composition.ts +59 -0
- package/src/runtime/root/computer-composition.ts +376 -0
- package/src/runtime/root/computer-target-composition.ts +16 -0
- package/src/runtime/root/computer-target-persistence.ts +39 -0
- package/src/runtime/root/controller-progression-composition.ts +192 -0
- package/src/runtime/root/controller-round-composition.ts +126 -0
- package/src/runtime/root/controller-services.ts +2 -0
- package/src/runtime/root/deployment-topology.ts +106 -0
- package/src/runtime/root/entry.ts +8 -6
- package/src/runtime/root/gateway-adapter.ts +3 -3
- package/src/runtime/root/known-good-recovery.ts +184 -0
- package/src/runtime/root/local-bridge.ts +5 -3
- package/src/runtime/root/mcp-transport.ts +116 -23
- package/src/runtime/root/ownership.ts +261 -9
- package/src/runtime/root/package-connector-service.ts +212 -74
- package/src/runtime/root/package-runtime-release.ts +199 -33
- package/src/runtime/root/package-runtime-service.ts +77 -108
- package/src/runtime/root/release-execution-canary.ts +103 -0
- package/src/runtime/root/release-loader.ts +150 -0
- package/src/runtime/root/release-manifest.ts +185 -7
- package/src/runtime/root/release-materialize.ts +474 -31
- package/src/runtime/root/release-store.ts +425 -37
- package/src/runtime/root/runtime-lane.ts +191 -0
- package/src/runtime/root/runtime.ts +379 -32
- package/src/runtime/root/scheduled-controller-composition.ts +76 -0
- package/src/runtime/root/scheduler.ts +12 -4
- package/src/runtime/root/service-runner.ts +3 -3
- package/src/runtime/root/service.ts +73 -45
- package/src/runtime/root/status.ts +49 -0
- package/src/runtime/root/types.ts +55 -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/local-bridge-surface.ts +1 -1
- package/src/runtime/shared/process-environment.ts +36 -10
- package/src/runtime/shared/response-budget.ts +1 -3
- package/src/runtime/shared/storage-capacity.ts +68 -0
- package/src/runtime/standalone-recovery/controller-home-migration.ts +1108 -0
- package/src/runtime/standalone-recovery/core.ts +4609 -495
- package/src/runtime/standalone-recovery/entry.ts +642 -81
- package/src/runtime/standalone-recovery/independent-host-rescue.ts +172 -0
- package/src/runtime/standalone-recovery/installer.ts +399 -177
- package/src/runtime/standalone-recovery/mcp-server.ts +79 -0
- package/src/runtime/standalone-recovery/mutation-identity-contract.ts +21 -0
- package/src/runtime/standalone-recovery/operation-lock.ts +160 -0
- package/src/runtime/standalone-recovery/performance.ts +173 -0
- package/src/runtime/standalone-recovery/release.ts +58 -5
- package/src/runtime/standalone-recovery/service-labels.ts +5 -2
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
- package/src/runtime/watchdog/workflow-watchdog.ts +1 -1
- package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +11 -4
- package/src/runtime/workflow/schedules/engine.ts +298 -484
- package/src/runtime/workflow/schedules/pending-activation.ts +1 -1
- package/src/runtime/workflow/schedules/settlement.ts +2 -191
- package/src/runtime/workflow/schedules/store.ts +2 -607
- package/src/runtime/workflow/schedules/types.ts +2 -127
- package/src/runtime/workflow/schedules/work-continuation.ts +220 -16
- package/src/runtime/workflows/first-party/xiaohongshu.ts +122 -0
- package/src/runtime/workflows/runtime.ts +482 -0
- package/src/testing/test-governance.ts +95 -30
- 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/assets/templates/helpers/archive-architecture-request.sh +0 -244
- package/docs/architecture/history/README.md +0 -7
- package/docs/architecture/history/chatgpt-handoff-facade.md +0 -397
- package/docs/architecture/history/global-hook-runtime.md +0 -166
- package/docs/architecture/history/ios-semantic-automation-provider-v2.md +0 -170
- package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +0 -63
- package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +0 -68
- package/docs/architecture/requests/archive/2026/root.md +0 -68
- package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +0 -76
- package/docs/architecture/requests/archive/2026/verification-evals-checks.md +0 -68
- package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +0 -69
- package/scripts/archive-architecture-request.sh +0 -4
- package/scripts/benchmark-background-check-overlap.ts +0 -228
- package/scripts/benchmark-controller-hotpath.ts +0 -212
- package/scripts/benchmark-controller-round2.ts +0 -139
- package/scripts/benchmark-ios-agent-device.ts +0 -514
- package/scripts/benchmark-mcp-host.sh +0 -117
- package/scripts/benchmark-mcp-transport.ts +0 -170
- 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 -67
- package/scripts/package-source-archive.sh +0 -147
- package/scripts/public-release-files.txt +0 -39
- package/scripts/public-release.ts +0 -174
- package/scripts/publish-release-tarball.sh +0 -27
- package/scripts/route-nl-vs-ts-eval.ts +0 -679
- package/scripts/run-skill-evals.ts +0 -1022
- package/scripts/stage-runtime-release.ts +0 -39
- 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/direct-edit-work-completion.ts +0 -385
- package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -1913
- package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
- package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -449
- package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -1118
- package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -93
- 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 -382
- package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
|
@@ -0,0 +1,1451 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { resolveWorkflowSupervisorForgeHome, workflowSupervisorDatabasePathValue, workflowSupervisorRootPath } from './paths';
|
|
5
|
+
import type { WorkflowEffectKind, WorkflowEffectOutcome, WorkflowSupervisorCompletion, WorkflowSupervisorContinuationProof, WorkflowSupervisorDiscoverySnapshot, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTaskStall, WorkflowSupervisorTerminalState } from './types';
|
|
6
|
+
export type { WorkflowSupervisorTaskStall } from './types';
|
|
7
|
+
|
|
8
|
+
interface Statement { get(...params: unknown[]): unknown; all(...params: unknown[]): unknown[]; run(...params: unknown[]): unknown; finalize?(): void }
|
|
9
|
+
interface Database { exec(sql: string): void; prepare(sql: string): Statement; close(): void }
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
function databaseConstructor(): new (path: string) => Database {
|
|
13
|
+
const module = require(process.versions.bun ? 'bun:sqlite' : 'node:sqlite') as { Database?: new (path: string) => Database; DatabaseSync?: new (path: string) => Database };
|
|
14
|
+
const Constructor = module.Database ?? module.DatabaseSync;
|
|
15
|
+
if (!Constructor) throw new Error('WORKFLOW_SUPERVISOR_SQLITE_UNAVAILABLE');
|
|
16
|
+
return Constructor;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function statement<T>(db: Database, sql: string, fn: (statement: Statement) => T): T {
|
|
20
|
+
const prepared = db.prepare(sql);
|
|
21
|
+
try { return fn(prepared); } finally { prepared.finalize?.(); }
|
|
22
|
+
}
|
|
23
|
+
function now(): string { return new Date().toISOString(); }
|
|
24
|
+
function objectiveSha256(value: string): string { return createHash('sha256').update(value).digest('hex'); }
|
|
25
|
+
function projectMetadataFromConversationUrl(value: string, observedTitle?: string, observedUrl?: string): { projectTitle?: string; projectUrl: string } | undefined {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(value);
|
|
28
|
+
const match = /^\/g\/(g-p-[a-z0-9]+)(?:-([^/]+))?\/c\/[a-z0-9-]+\/?$/i.exec(parsed.pathname);
|
|
29
|
+
const slug = match?.[2]?.trim();
|
|
30
|
+
if (parsed.protocol !== 'https:' || parsed.hostname !== 'chatgpt.com' || !match?.[1]) return undefined;
|
|
31
|
+
let observedProjectId: string | undefined;
|
|
32
|
+
try {
|
|
33
|
+
const observed = new URL(observedUrl ?? '');
|
|
34
|
+
if (observed.origin === parsed.origin) observedProjectId = /^\/g\/(g-p-[a-z0-9]+)(?:-[^/]+)?\/project\/?$/i.exec(observed.pathname)?.[1];
|
|
35
|
+
} catch { /* An unproven title cannot override the canonical route. */ }
|
|
36
|
+
const projectTitle = observedProjectId?.toLowerCase() === match[1].toLowerCase()
|
|
37
|
+
? observedTitle ?? slug?.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim()
|
|
38
|
+
: slug?.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
39
|
+
return {
|
|
40
|
+
...(projectTitle ? { projectTitle } : {}),
|
|
41
|
+
projectUrl: `https://chatgpt.com/g/${match[1]}/project`,
|
|
42
|
+
};
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function json(value: unknown): string {
|
|
48
|
+
if (Array.isArray(value)) return `[${value.map((item) => json(item)).join(',')}]`;
|
|
49
|
+
if (value && typeof value === 'object') {
|
|
50
|
+
const entries = Object.entries(value as Record<string, unknown>).sort(([a],[b]) => a.localeCompare(b));
|
|
51
|
+
return `{${entries.map(([key,item]) => `${JSON.stringify(key)}:${json(item)}`).join(',')}}`;
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { resolveWorkflowSupervisorForgeHome } from './paths';
|
|
57
|
+
export function workflowSupervisorRoot(forgeHome?: string): string {
|
|
58
|
+
const root = workflowSupervisorRootPath(forgeHome);
|
|
59
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
60
|
+
return root;
|
|
61
|
+
}
|
|
62
|
+
export function workflowSupervisorDatabasePath(forgeHome?: string): string {
|
|
63
|
+
workflowSupervisorRoot(forgeHome);
|
|
64
|
+
return workflowSupervisorDatabasePathValue(forgeHome);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function openDatabase(forgeHome?: string): Database {
|
|
68
|
+
const Constructor = databaseConstructor();
|
|
69
|
+
const db = new Constructor(workflowSupervisorDatabasePath(forgeHome));
|
|
70
|
+
db.exec('PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;');
|
|
71
|
+
db.exec(`
|
|
72
|
+
CREATE TABLE IF NOT EXISTS supervisor_schema (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL);
|
|
73
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
74
|
+
task_id TEXT PRIMARY KEY, conversation_id TEXT NOT NULL UNIQUE, conversation_url TEXT NOT NULL, objective TEXT NOT NULL,
|
|
75
|
+
completion_contract_json TEXT NOT NULL, continuation_policy_json TEXT NOT NULL, user_blocker_policy_json TEXT NOT NULL, created_at TEXT NOT NULL
|
|
76
|
+
);
|
|
77
|
+
CREATE TABLE IF NOT EXISTS completions (
|
|
78
|
+
completion_fingerprint TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(task_id), source_effect_id TEXT NOT NULL,
|
|
79
|
+
action TEXT NOT NULL, response_sha256 TEXT NOT NULL, control_block_sha256 TEXT NOT NULL, proposal_json TEXT NOT NULL, committed_at TEXT NOT NULL,
|
|
80
|
+
UNIQUE(task_id, source_effect_id, response_sha256, control_block_sha256)
|
|
81
|
+
);
|
|
82
|
+
CREATE UNIQUE INDEX IF NOT EXISTS completions_one_per_source_effect
|
|
83
|
+
ON completions(task_id, source_effect_id);
|
|
84
|
+
CREATE TABLE IF NOT EXISTS effects (
|
|
85
|
+
effect_id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(task_id), kind TEXT NOT NULL, origin_key TEXT NOT NULL UNIQUE,
|
|
86
|
+
source_completion_fingerprint TEXT, prompt_text TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
87
|
+
UNIQUE(task_id, source_completion_fingerprint)
|
|
88
|
+
);
|
|
89
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
90
|
+
event_id INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL REFERENCES tasks(task_id), event_key TEXT NOT NULL UNIQUE,
|
|
91
|
+
kind TEXT NOT NULL, effect_id TEXT, completion_fingerprint TEXT, payload_json TEXT NOT NULL, occurred_at TEXT NOT NULL
|
|
92
|
+
);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS events_task_order ON events(task_id, event_id);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS events_effect_kind_order ON events(effect_id, kind, event_id);
|
|
95
|
+
CREATE TABLE IF NOT EXISTS discovered_conversations (
|
|
96
|
+
source TEXT NOT NULL, conversation_id TEXT NOT NULL, canonical_url TEXT NOT NULL, title TEXT,
|
|
97
|
+
project_title TEXT, project_url TEXT, observed_at TEXT NOT NULL,
|
|
98
|
+
PRIMARY KEY(source, conversation_id)
|
|
99
|
+
);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS discovered_conversations_observed ON discovered_conversations(observed_at, conversation_id);
|
|
101
|
+
INSERT OR IGNORE INTO supervisor_schema(version, applied_at) VALUES (1, datetime('now'));
|
|
102
|
+
`);
|
|
103
|
+
const schema = statement(db, 'SELECT MAX(version) AS version FROM supervisor_schema', (s) => s.get()) as { version?: number } | undefined;
|
|
104
|
+
if (schema?.version !== 1) { db.close(); throw new Error(`WORKFLOW_SUPERVISOR_SCHEMA_UNSUPPORTED:${String(schema?.version ?? 'missing')}`); }
|
|
105
|
+
return db;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function taskFromRow(row: Record<string, unknown>): WorkflowSupervisorTask {
|
|
109
|
+
return {
|
|
110
|
+
taskId: String(row.task_id), conversationId: String(row.conversation_id), conversationUrl: String(row.conversation_url), objective: String(row.objective),
|
|
111
|
+
completionContract: JSON.parse(String(row.completion_contract_json)) as Record<string, unknown>, continuationPolicy: JSON.parse(String(row.continuation_policy_json)) as Record<string, unknown>,
|
|
112
|
+
userBlockerPolicy: JSON.parse(String(row.user_blocker_policy_json)) as Record<string, unknown>, createdAt: String(row.created_at),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function boundedText(value: unknown): string | undefined {
|
|
117
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function explicitRequirementTaskUpgrade(input: WorkflowSupervisorTaskInput): { requirementId: string; repoId: string } | undefined {
|
|
121
|
+
if (input.completionContract.kind !== 'forge_requirement_done'
|
|
122
|
+
|| input.continuationPolicy.kind !== 'forge_goal_outer_turn'
|
|
123
|
+
|| input.userBlockerPolicy.kind !== 'forge_requirement_waiting_for_user') return undefined;
|
|
124
|
+
const requirementId = boundedText(input.completionContract.requirement_id);
|
|
125
|
+
const blockerRequirementId = boundedText(input.userBlockerPolicy.requirement_id);
|
|
126
|
+
const repoId = boundedText(input.completionContract.repo_id);
|
|
127
|
+
const blockerRepoId = boundedText(input.userBlockerPolicy.repo_id);
|
|
128
|
+
if (!requirementId || blockerRequirementId !== requirementId || !repoId || blockerRepoId !== repoId) return undefined;
|
|
129
|
+
if (boundedText(input.continuationPolicy.exact_conversation_id) !== input.conversationId
|
|
130
|
+
|| boundedText(input.continuationPolicy.exact_conversation_url) !== input.conversationUrl) return undefined;
|
|
131
|
+
return { requirementId, repoId };
|
|
132
|
+
}
|
|
133
|
+
function effectFromRow(row: Record<string, unknown>): WorkflowSupervisorEffect {
|
|
134
|
+
return { effectId: String(row.effect_id), taskId: String(row.task_id), kind: String(row.kind) as WorkflowEffectKind,
|
|
135
|
+
...(row.source_completion_fingerprint ? { sourceCompletionFingerprint: String(row.source_completion_fingerprint) } : {}), prompt: String(row.prompt_text), createdAt: String(row.created_at) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function completionFromRow(row: Record<string, unknown>): WorkflowSupervisorCompletion {
|
|
139
|
+
return {
|
|
140
|
+
completionFingerprint: String(row.completion_fingerprint), taskId: String(row.task_id), sourceEffectId: String(row.source_effect_id),
|
|
141
|
+
action: String(row.action) as WorkflowSupervisorCompletion['action'], responseSha256: String(row.response_sha256), controlBlockSha256: String(row.control_block_sha256),
|
|
142
|
+
proposal: JSON.parse(String(row.proposal_json)) as WorkflowSupervisorCompletion['proposal'], committedAt: String(row.committed_at),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function parsedObject(value: unknown): Record<string, unknown> {
|
|
146
|
+
try { const parsed = JSON.parse(String(value)); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {}; }
|
|
147
|
+
catch { return {}; }
|
|
148
|
+
}
|
|
149
|
+
function storedGeneration(value: unknown): number {
|
|
150
|
+
const generation = Number(parsedObject(value).generation);
|
|
151
|
+
return Number.isInteger(generation) && generation > 0 ? generation : 1;
|
|
152
|
+
}
|
|
153
|
+
function latestNotAppliedProofEventId(db: Database, effectId: string): number {
|
|
154
|
+
const notApplied = statement(db, "SELECT event_id FROM events WHERE effect_id = ? AND kind = 'effect_not_applied' ORDER BY event_id DESC LIMIT 1", (s) => s.get(effectId)) as { event_id?: number } | undefined;
|
|
155
|
+
return Number(notApplied?.event_id ?? 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A durable operator supersession retires an effect from the causal obligation; it is never replayed. */
|
|
159
|
+
function effectSuperseded(db: Database, effectId: string): boolean {
|
|
160
|
+
return Boolean(statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_outcome_unknown_superseded' LIMIT 1", (s) => s.get(effectId)));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function providerResumeExhaustedWithin(db: Database, effectId: string): boolean {
|
|
164
|
+
return Boolean(statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'assistant_recovery_exhausted' LIMIT 1", (s) => s.get(effectId)));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function providerRecoveryDepthWithin(db: Database, effectId: string): number {
|
|
168
|
+
let currentEffectId = effectId;
|
|
169
|
+
let depth = 0;
|
|
170
|
+
const seen = new Set<string>();
|
|
171
|
+
while (true) {
|
|
172
|
+
if (seen.has(currentEffectId)) throw new Error('WORKFLOW_SUPERVISOR_PROVIDER_RECOVERY_CHAIN_CORRUPT');
|
|
173
|
+
seen.add(currentEffectId);
|
|
174
|
+
const row = statement(db, 'SELECT kind,origin_key FROM effects WHERE effect_id = ?', (s) => s.get(currentEffectId)) as { kind?: string; origin_key?: string } | undefined;
|
|
175
|
+
if (!row) throw new Error('WORKFLOW_SUPERVISOR_PROVIDER_RECOVERY_CHAIN_CORRUPT');
|
|
176
|
+
const originKey = String(row.origin_key ?? '');
|
|
177
|
+
if (row.kind !== 'recovery' || !originKey.startsWith('provider-recovery:')) return depth;
|
|
178
|
+
depth += 1;
|
|
179
|
+
currentEffectId = originKey.slice('provider-recovery:'.length).trim();
|
|
180
|
+
if (!currentEffectId) throw new Error('WORKFLOW_SUPERVISOR_PROVIDER_RECOVERY_CHAIN_CORRUPT');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function operatorAuthorizedRefundCount(db: Database, effectId: string): number {
|
|
185
|
+
const row = statement(db, "SELECT COUNT(*) AS total FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_budget_refunded' AND json_extract(payload_json,'$.reason') = 'operator_authorized_retry'", (s) => s.get(effectId)) as { total?: number } | undefined;
|
|
186
|
+
return Number(row?.total ?? 0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function oldestUnappliedEffect(db: Database, taskId: string): WorkflowSupervisorEffect | undefined {
|
|
190
|
+
const row = statement(db, `SELECT e.* FROM effects e
|
|
191
|
+
WHERE e.task_id = ? AND NOT EXISTS (
|
|
192
|
+
SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied'
|
|
193
|
+
) AND NOT EXISTS (
|
|
194
|
+
SELECT 1 FROM events superseded WHERE superseded.effect_id = e.effect_id AND superseded.kind = 'effect_outcome_unknown_superseded'
|
|
195
|
+
) ORDER BY e.created_at, e.effect_id LIMIT 1`, (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
196
|
+
return row ? effectFromRow(row) : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Mechanical ceiling on how many times one un-applied Supervisor effect may be
|
|
201
|
+
* submitted to the provider. A proven non-application may legitimately be
|
|
202
|
+
* retried, but ChatGPT is one rate-limited shared resource: without a ceiling a
|
|
203
|
+
* single stuck effect minted an unbounded generation chain (live evidence:
|
|
204
|
+
* single effects were re-sent 130-166 times over hours), which is provider
|
|
205
|
+
* request volume that no Work actually asked for. Three total attempts is the
|
|
206
|
+
* hard ceiling: one normal submission plus at most two causally authorized retries.
|
|
207
|
+
*/
|
|
208
|
+
export const WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS = 3;
|
|
209
|
+
/** A broken preparation cannot silently poll forever across Runtime restarts. */
|
|
210
|
+
export const WORKFLOW_SUPERVISOR_MAX_PREFLIGHT_RELEASE_ATTEMPTS = 3;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Once a provider has accepted a Supervisor effect, recovery never replays that
|
|
214
|
+
* source mutation. It may emit at most two separately identified recovery turns,
|
|
215
|
+
* matching the repository-wide three-provider-attempt ceiling (initial turn plus
|
|
216
|
+
* two causal retries). The depth is derived from effect origin links, not stored
|
|
217
|
+
* as another lifecycle state.
|
|
218
|
+
*/
|
|
219
|
+
export const WORKFLOW_SUPERVISOR_MAX_PROVIDER_RECOVERY_TURNS = 2;
|
|
220
|
+
export const WORKFLOW_SUPERVISOR_DISPATCH_RETRY_BASE_MS = 30_000;
|
|
221
|
+
export const WORKFLOW_SUPERVISOR_DISPATCH_RETRY_MAX_MS = 10 * 60_000;
|
|
222
|
+
export const WORKFLOW_SUPERVISOR_UNKNOWN_OBSERVATION_BASE_MS = 5_000;
|
|
223
|
+
export const WORKFLOW_SUPERVISOR_UNKNOWN_OBSERVATION_MAX_MS = 60_000;
|
|
224
|
+
/**
|
|
225
|
+
* An explicit operator recovery may refund the mechanical retry budget for an
|
|
226
|
+
* effect whose latest dispatch carries a mechanical pre-send negative proof.
|
|
227
|
+
* That is a safety statement about *this* effect, not a reset of provider
|
|
228
|
+
* budgets, so it stays bounded: exceeding it requires a new explicit decision
|
|
229
|
+
* rather than another silent attempt.
|
|
230
|
+
*/
|
|
231
|
+
export const WORKFLOW_SUPERVISOR_MAX_OPERATOR_REFUNDS = 2;
|
|
232
|
+
/**
|
|
233
|
+
* A standalone Supervisor task's conversation is its delivery surface. If that
|
|
234
|
+
* exact conversation becomes permanently unusable, an explicit operator may
|
|
235
|
+
* move the task to a replacement conversation. The budget keeps a transient
|
|
236
|
+
* provider problem from turning into a conversation-hopping loop.
|
|
237
|
+
*/
|
|
238
|
+
export const WORKFLOW_SUPERVISOR_MAX_CONVERSATION_MIGRATIONS = 2;
|
|
239
|
+
export const WORKFLOW_SUPERVISOR_MIN_TURN_INTERVAL_MS = 3 * 60_000;
|
|
240
|
+
|
|
241
|
+
/** Spaced retries keep a persistent local obstacle from becoming a request storm. */
|
|
242
|
+
export function workflowSupervisorDispatchRetryDelayMs(generation: number): number {
|
|
243
|
+
const exponent = Math.max(0, Math.min(8, Math.trunc(generation) - 1));
|
|
244
|
+
return Math.min(WORKFLOW_SUPERVISOR_DISPATCH_RETRY_MAX_MS, WORKFLOW_SUPERVISOR_DISPATCH_RETRY_BASE_MS * 2 ** exponent);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface WorkflowSupervisorEffectDispatchBudget {
|
|
248
|
+
effectId: string;
|
|
249
|
+
generations: number;
|
|
250
|
+
maxGenerations: number;
|
|
251
|
+
lastDispatchedAtMs?: number;
|
|
252
|
+
retryDelayMs?: number;
|
|
253
|
+
exhausted: boolean;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
interface EffectDispatchLedger {
|
|
258
|
+
generations: number;
|
|
259
|
+
budgetRefunds: number;
|
|
260
|
+
lastEventId: number;
|
|
261
|
+
lastGeneration: number;
|
|
262
|
+
lastOccurredAtMs: number;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
interface EffectUnknownObservationLedger {
|
|
266
|
+
sameFingerprintCount: number;
|
|
267
|
+
lastOccurredAtMs: number;
|
|
268
|
+
fingerprint?: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function unknownObservationFingerprintFromObject(payload: Record<string, unknown>): string | undefined {
|
|
272
|
+
const explicit = boundedText(payload.observation_fingerprint);
|
|
273
|
+
if (explicit) return `explicit:${explicit}`;
|
|
274
|
+
// A transport may fail before a Browser snapshot exists. The canonical
|
|
275
|
+
// ledger still has to collapse repeated identical transport failures from
|
|
276
|
+
// native messaging, extensions, or older adapters. A changed reason/surface
|
|
277
|
+
// is materially new evidence and receives a fresh bounded budget.
|
|
278
|
+
const reason = boundedText(payload.reason) ?? 'unknown';
|
|
279
|
+
const surface = boundedText(payload.surface) ?? '';
|
|
280
|
+
const reconciliation = payload.reconciliation === true ? 'reconcile' : 'observe';
|
|
281
|
+
return `fallback:${surface}:${reconciliation}:${reason}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function unknownObservationFingerprint(payloadJson: unknown): string | undefined {
|
|
285
|
+
return unknownObservationFingerprintFromObject(parsedObject(payloadJson));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function effectUnknownObservationLedger(db: Database, effectId: string, afterEventId: number): EffectUnknownObservationLedger {
|
|
289
|
+
const rows = statement(db, "SELECT event_id,payload_json,occurred_at FROM events WHERE effect_id = ? AND kind = 'effect_unknown' AND event_id > ? ORDER BY event_id DESC LIMIT 32", (s) => s.all(effectId, afterEventId)) as Array<{ event_id?: number; payload_json?: string; occurred_at?: string }>;
|
|
290
|
+
const latest = rows[0];
|
|
291
|
+
if (!latest) return { sameFingerprintCount: 0, lastOccurredAtMs: 0 };
|
|
292
|
+
const fingerprint = unknownObservationFingerprint(latest.payload_json);
|
|
293
|
+
if (!fingerprint) return { sameFingerprintCount: 0, lastOccurredAtMs: 0 };
|
|
294
|
+
let sameFingerprintCount = 0;
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
if (unknownObservationFingerprint(row.payload_json) !== fingerprint) break;
|
|
297
|
+
sameFingerprintCount += 1;
|
|
298
|
+
}
|
|
299
|
+
const occurredAtMs = Date.parse(String(latest.occurred_at ?? ''));
|
|
300
|
+
return {
|
|
301
|
+
sameFingerprintCount,
|
|
302
|
+
lastOccurredAtMs: Number.isFinite(occurredAtMs) ? occurredAtMs : 0,
|
|
303
|
+
fingerprint,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function unknownObservationDelayMs(count: number): number {
|
|
308
|
+
const exponent = Math.max(0, Math.min(8, Math.trunc(count) - 1));
|
|
309
|
+
return Math.min(
|
|
310
|
+
WORKFLOW_SUPERVISOR_UNKNOWN_OBSERVATION_MAX_MS,
|
|
311
|
+
WORKFLOW_SUPERVISOR_UNKNOWN_OBSERVATION_BASE_MS * 2 ** exponent,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function effectDispatchLedger(db: Database, effectId: string): EffectDispatchLedger {
|
|
316
|
+
const rows = statement(db, "SELECT event_id,payload_json,occurred_at FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_started' ORDER BY event_id", (s) => s.all(effectId)) as Array<{ event_id?: number; payload_json?: string; occurred_at?: string }>;
|
|
317
|
+
const last = rows[rows.length - 1];
|
|
318
|
+
const refundRows = statement(db, "SELECT event_id FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_budget_refunded'", (s) => s.all(effectId)) as Array<{ event_id?: number }>;
|
|
319
|
+
const payload = parsedObject(last?.payload_json);
|
|
320
|
+
const payloadMs = Number(payload.dispatched_at_ms);
|
|
321
|
+
const lastOccurredAtMs = Number.isFinite(payloadMs) && payloadMs > 0
|
|
322
|
+
? payloadMs
|
|
323
|
+
: Date.parse(String(last?.occurred_at ?? ''));
|
|
324
|
+
return {
|
|
325
|
+
generations: rows.length,
|
|
326
|
+
budgetRefunds: refundRows.length,
|
|
327
|
+
lastEventId: Number(last?.event_id ?? 0),
|
|
328
|
+
lastGeneration: last?.event_id ? storedGeneration(last.payload_json) : 0,
|
|
329
|
+
lastOccurredAtMs: Number.isFinite(lastOccurredAtMs) ? lastOccurredAtMs : 0,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/** Derived entirely from the Supervisor effect event log; no second retry authority. */
|
|
333
|
+
function effectPreflightFailureReleaseIds(db: Database, effectId: string): string[] {
|
|
334
|
+
const rows = statement(db, "SELECT payload_json FROM events WHERE effect_id = ? AND kind = 'effect_preflight_rejected' ORDER BY event_id", (s) => s.all(effectId)) as Array<{ payload_json?: string }>;
|
|
335
|
+
return rows.map((row) => String(parsedObject(row.payload_json).active_release_id ?? 'unidentified-runtime'));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export class WorkflowSupervisorStore {
|
|
339
|
+
private readonly db: Database;
|
|
340
|
+
private closed = false;
|
|
341
|
+
/** Mechanical clock for provider retry spacing. Durable event timestamps keep wall-clock ISO. */
|
|
342
|
+
private readonly clockMs: () => number;
|
|
343
|
+
private readonly activeReleaseId: () => string | undefined;
|
|
344
|
+
|
|
345
|
+
constructor(readonly forgeHome?: string, options: { now?: () => number; activeReleaseId?: () => string | undefined } = {}) {
|
|
346
|
+
this.db = openDatabase(forgeHome);
|
|
347
|
+
this.clockMs = options.now ?? Date.now;
|
|
348
|
+
this.activeReleaseId = options.activeReleaseId ?? (() => undefined);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private preflightBlocked(db: Database, effectId: string): boolean {
|
|
352
|
+
const tried = effectPreflightFailureReleaseIds(db, effectId);
|
|
353
|
+
return tried.length >= WORKFLOW_SUPERVISOR_MAX_PREFLIGHT_RELEASE_ATTEMPTS
|
|
354
|
+
|| tried.includes(this.activeReleaseId()?.trim() || 'unidentified-runtime');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
close(): void {
|
|
358
|
+
if (this.closed) return;
|
|
359
|
+
this.closed = true;
|
|
360
|
+
this.db.close();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private database(): Database {
|
|
364
|
+
if (this.closed) throw new Error('WORKFLOW_SUPERVISOR_STORE_CLOSED');
|
|
365
|
+
return this.db;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private transaction<T>(fn: (db: Database) => T): T {
|
|
369
|
+
const db = this.database();
|
|
370
|
+
db.exec('BEGIN IMMEDIATE');
|
|
371
|
+
try { const result = fn(db); db.exec('COMMIT'); return result; } catch (error) { db.exec('ROLLBACK'); throw error; }
|
|
372
|
+
}
|
|
373
|
+
private read<T>(fn: (db: Database) => T): T { return fn(this.database()); }
|
|
374
|
+
|
|
375
|
+
recordDiscovery(source: string, conversations: readonly WorkflowSupervisorDiscoveredConversation[]): WorkflowSupervisorDiscoverySnapshot {
|
|
376
|
+
const normalizedSource = source.trim();
|
|
377
|
+
if (!/^[a-z0-9][a-z0-9._:-]{0,127}$/i.test(normalizedSource)) throw new Error('WORKFLOW_SUPERVISOR_DISCOVERY_SOURCE_INVALID');
|
|
378
|
+
const observedAt = now();
|
|
379
|
+
this.transaction((db) => {
|
|
380
|
+
for (const conversation of conversations) {
|
|
381
|
+
const project = projectMetadataFromConversationUrl(conversation.canonicalUrl, conversation.projectTitle, conversation.projectUrl)
|
|
382
|
+
?? { projectTitle: conversation.projectTitle, projectUrl: conversation.projectUrl };
|
|
383
|
+
statement(db, `INSERT INTO discovered_conversations(source,conversation_id,canonical_url,title,project_title,project_url,observed_at)
|
|
384
|
+
VALUES (?,?,?,?,?,?,?)
|
|
385
|
+
ON CONFLICT(source,conversation_id) DO UPDATE SET canonical_url=excluded.canonical_url,title=excluded.title,
|
|
386
|
+
project_title=CASE WHEN excluded.project_url IS NOT NULL AND discovered_conversations.project_url IS NOT NULL
|
|
387
|
+
AND excluded.project_url<>discovered_conversations.project_url THEN excluded.project_title
|
|
388
|
+
ELSE COALESCE(excluded.project_title,discovered_conversations.project_title) END,
|
|
389
|
+
project_url=COALESCE(excluded.project_url,discovered_conversations.project_url),observed_at=excluded.observed_at`,
|
|
390
|
+
(s) => s.run(normalizedSource, conversation.conversationId, conversation.canonicalUrl, conversation.title ?? null, project.projectTitle ?? null, project.projectUrl ?? null, observedAt));
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
return this.discoverySnapshot();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
discoverySnapshot(): WorkflowSupervisorDiscoverySnapshot {
|
|
397
|
+
return this.read((db) => {
|
|
398
|
+
const rows = statement(db, 'SELECT * FROM discovered_conversations ORDER BY observed_at DESC, conversation_id, source', (s) => s.all()) as Record<string, unknown>[];
|
|
399
|
+
const byConversation = new Map<string, WorkflowSupervisorDiscoveredConversation>();
|
|
400
|
+
let observedAt = '';
|
|
401
|
+
for (const row of rows) {
|
|
402
|
+
const conversationId = String(row.conversation_id ?? '');
|
|
403
|
+
const canonicalUrl = String(row.canonical_url ?? '');
|
|
404
|
+
// Validate retained observations too: an older adapter may have tagged
|
|
405
|
+
// every sidebar conversation with the project page it happened to scan.
|
|
406
|
+
const project = projectMetadataFromConversationUrl(canonicalUrl, row.project_title ? String(row.project_title) : undefined, row.project_url ? String(row.project_url) : undefined)
|
|
407
|
+
?? { projectTitle: row.project_title ? String(row.project_title) : undefined, projectUrl: row.project_url ? String(row.project_url) : undefined };
|
|
408
|
+
const rowObservedAt = String(row.observed_at ?? '');
|
|
409
|
+
if (rowObservedAt > observedAt) observedAt = rowObservedAt;
|
|
410
|
+
const current = byConversation.get(conversationId);
|
|
411
|
+
const candidate: WorkflowSupervisorDiscoveredConversation = {
|
|
412
|
+
conversationId,
|
|
413
|
+
canonicalUrl,
|
|
414
|
+
...(row.title ? { title: String(row.title) } : {}),
|
|
415
|
+
...(project.projectTitle ? { projectTitle: project.projectTitle } : {}),
|
|
416
|
+
...(project.projectUrl ? { projectUrl: project.projectUrl } : {}),
|
|
417
|
+
};
|
|
418
|
+
if (!current) byConversation.set(conversationId, candidate);
|
|
419
|
+
else if (!current.projectTitle && candidate.projectTitle && (!current.projectUrl || current.projectUrl === candidate.projectUrl)) byConversation.set(conversationId, { ...current, projectTitle: candidate.projectTitle, ...(candidate.projectUrl ? { projectUrl: candidate.projectUrl } : {}) });
|
|
420
|
+
}
|
|
421
|
+
return { observedAt, conversations: [...byConversation.values()] };
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
registerTask(input: WorkflowSupervisorTaskInput): WorkflowSupervisorTask {
|
|
426
|
+
return this.transaction((db) => {
|
|
427
|
+
const createdAt = now();
|
|
428
|
+
statement(db, 'INSERT OR IGNORE INTO tasks VALUES (?, ?, ?, ?, ?, ?, ?, ?)', (s) => s.run(input.taskId, input.conversationId, input.conversationUrl, input.objective, json(input.completionContract), json(input.continuationPolicy), json(input.userBlockerPolicy), createdAt));
|
|
429
|
+
let row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
430
|
+
if (!row) {
|
|
431
|
+
row = statement(db, 'SELECT * FROM tasks WHERE conversation_id = ?', (s) => s.get(input.conversationId)) as Record<string, unknown> | undefined;
|
|
432
|
+
if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
|
|
433
|
+
const existing = taskFromRow(row);
|
|
434
|
+
if (existing.conversationUrl !== input.conversationUrl) throw new Error('WORKFLOW_SUPERVISOR_TASK_CONVERSATION_CONFLICT');
|
|
435
|
+
const existingRepo = typeof existing.completionContract.repo_id === 'string' ? existing.completionContract.repo_id : existing.continuationPolicy.repo_id;
|
|
436
|
+
const incomingRepo = typeof input.completionContract.repo_id === 'string' ? input.completionContract.repo_id : input.continuationPolicy.repo_id;
|
|
437
|
+
if (typeof existingRepo === 'string' && typeof incomingRepo === 'string' && existingRepo !== incomingRepo) throw new Error('WORKFLOW_SUPERVISOR_TASK_REPOSITORY_CONFLICT');
|
|
438
|
+
return existing;
|
|
439
|
+
}
|
|
440
|
+
const task = taskFromRow(row);
|
|
441
|
+
const taskRepo = typeof task.completionContract.repo_id === 'string' ? task.completionContract.repo_id : task.continuationPolicy.repo_id;
|
|
442
|
+
const inputRepo = typeof input.completionContract.repo_id === 'string' ? input.completionContract.repo_id : input.continuationPolicy.repo_id;
|
|
443
|
+
const { checkout_id: incomingCheckout, ...incomingCompletion } = input.completionContract;
|
|
444
|
+
const { checkout_id: selectedCheckout, ...selectedCompletion } = task.completionContract;
|
|
445
|
+
// Registration may fill an initially missing execution hint. It cannot
|
|
446
|
+
// replace a selected checkout or change task identity/semantic obligations.
|
|
447
|
+
const sameExecutionContextOwner = typeof taskRepo === 'string' && taskRepo === inputRepo
|
|
448
|
+
&& task.conversationId === input.conversationId && task.conversationUrl === input.conversationUrl
|
|
449
|
+
&& task.objective === input.objective
|
|
450
|
+
&& json(selectedCompletion) === json(incomingCompletion)
|
|
451
|
+
&& json(task.continuationPolicy) === json(input.continuationPolicy)
|
|
452
|
+
&& json(task.userBlockerPolicy) === json(input.userBlockerPolicy);
|
|
453
|
+
// A retry of the original registration cannot erase the selected hint.
|
|
454
|
+
if (sameExecutionContextOwner && selectedCheckout !== undefined && incomingCheckout === undefined) return task;
|
|
455
|
+
const initialCheckoutContext = sameExecutionContextOwner && selectedCheckout === undefined
|
|
456
|
+
&& task.continuationPolicy.checkout_id === undefined
|
|
457
|
+
&& typeof incomingCheckout === 'string' && /^checkout_[a-zA-Z0-9_-]{8,120}$/.test(incomingCheckout);
|
|
458
|
+
if (initialCheckoutContext) {
|
|
459
|
+
statement(db, 'UPDATE tasks SET completion_contract_json = ? WHERE task_id = ?', s => s.run(json(input.completionContract), task.taskId));
|
|
460
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', s => s.run(
|
|
461
|
+
task.taskId, `task-checkout-context:${task.taskId}`, 'task_execution_context_enriched', json({ repo_id: taskRepo, checkout_id: incomingCheckout }), createdAt,
|
|
462
|
+
));
|
|
463
|
+
return { ...task, completionContract: input.completionContract };
|
|
464
|
+
}
|
|
465
|
+
const bootstrapUpgrade = task.continuationPolicy.bootstrap === true
|
|
466
|
+
&& input.continuationPolicy.bootstrap !== true
|
|
467
|
+
&& taskRepo === inputRepo
|
|
468
|
+
&& task.conversationId === input.conversationId
|
|
469
|
+
&& task.conversationUrl === input.conversationUrl;
|
|
470
|
+
if (bootstrapUpgrade) {
|
|
471
|
+
statement(db, `UPDATE tasks
|
|
472
|
+
SET objective = ?, completion_contract_json = ?, continuation_policy_json = ?, user_blocker_policy_json = ?
|
|
473
|
+
WHERE task_id = ?`, (s) => s.run(
|
|
474
|
+
input.objective,
|
|
475
|
+
json(input.completionContract),
|
|
476
|
+
json(input.continuationPolicy),
|
|
477
|
+
json(input.userBlockerPolicy),
|
|
478
|
+
input.taskId,
|
|
479
|
+
));
|
|
480
|
+
const upgraded = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
481
|
+
if (!upgraded) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
|
|
482
|
+
return taskFromRow(upgraded);
|
|
483
|
+
}
|
|
484
|
+
const projectBootstrap = task.continuationPolicy.kind === 'forge_project_conversation_outer_turn';
|
|
485
|
+
const sameProjectBootstrapIdentity = projectBootstrap
|
|
486
|
+
&& task.conversationId === input.conversationId
|
|
487
|
+
&& task.conversationUrl === input.conversationUrl
|
|
488
|
+
&& (!taskRepo || !inputRepo || taskRepo === inputRepo);
|
|
489
|
+
if (sameProjectBootstrapIdentity) {
|
|
490
|
+
const explicitRequirementUpgrade = explicitRequirementTaskUpgrade(input);
|
|
491
|
+
if (input.continuationPolicy.kind === 'forge_goal_outer_turn') {
|
|
492
|
+
if (!explicitRequirementUpgrade || !taskRepo || taskRepo !== explicitRequirementUpgrade.repoId) {
|
|
493
|
+
throw new Error('WORKFLOW_SUPERVISOR_TASK_ID_CONFLICT');
|
|
494
|
+
}
|
|
495
|
+
statement(db, `UPDATE tasks
|
|
496
|
+
SET objective = ?, completion_contract_json = ?, continuation_policy_json = ?, user_blocker_policy_json = ?
|
|
497
|
+
WHERE task_id = ?`, (s) => s.run(
|
|
498
|
+
input.objective,
|
|
499
|
+
json(input.completionContract),
|
|
500
|
+
json(input.continuationPolicy),
|
|
501
|
+
json(input.userBlockerPolicy),
|
|
502
|
+
input.taskId,
|
|
503
|
+
));
|
|
504
|
+
const upgraded = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
505
|
+
if (!upgraded) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
|
|
506
|
+
return taskFromRow(upgraded);
|
|
507
|
+
}
|
|
508
|
+
return task;
|
|
509
|
+
}
|
|
510
|
+
if (task.conversationId !== input.conversationId
|
|
511
|
+
|| task.conversationUrl !== input.conversationUrl
|
|
512
|
+
|| task.objective !== input.objective
|
|
513
|
+
|| json(task.completionContract) !== json(input.completionContract)
|
|
514
|
+
|| json(task.continuationPolicy) !== json(input.continuationPolicy)
|
|
515
|
+
|| json(task.userBlockerPolicy) !== json(input.userBlockerPolicy)) {
|
|
516
|
+
throw new Error('WORKFLOW_SUPERVISOR_TASK_ID_CONFLICT');
|
|
517
|
+
}
|
|
518
|
+
return task;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
reviseTaskObjective(input: {
|
|
522
|
+
taskId: string;
|
|
523
|
+
objective: string;
|
|
524
|
+
expectedObjectiveSha256: string;
|
|
525
|
+
requestId: string;
|
|
526
|
+
authorizedBy: string;
|
|
527
|
+
refreshEffect?: { effectId: string; expectedPrompt: string; prompt: string };
|
|
528
|
+
}): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; refreshedEffect?: WorkflowSupervisorEffect } {
|
|
529
|
+
return this.transaction((db) => {
|
|
530
|
+
const objective = input.objective.trim();
|
|
531
|
+
if (!objective || objective.length > 8_000) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_INVALID');
|
|
532
|
+
if (!/^[0-9a-f]{64}$/i.test(input.expectedObjectiveSha256)) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_EXPECTED_SHA_INVALID');
|
|
533
|
+
if (!input.requestId.trim()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_REQUIRED');
|
|
534
|
+
const targetSha = objectiveSha256(objective);
|
|
535
|
+
const eventKey = `task-objective-revise:${input.taskId}:${input.requestId}`;
|
|
536
|
+
const existingEvent = statement(db, 'SELECT payload_json FROM events WHERE event_key = ?', (s) => s.get(eventKey)) as { payload_json?: string } | undefined;
|
|
537
|
+
if (existingEvent) {
|
|
538
|
+
const payload = parsedObject(existingEvent.payload_json);
|
|
539
|
+
if (boundedText(payload.objective_sha256) !== targetSha) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_CONFLICT');
|
|
540
|
+
const taskRow = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
541
|
+
if (!taskRow) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
542
|
+
const refreshedEffectId = boundedText(payload.refreshed_effect_id);
|
|
543
|
+
const refreshedRow = refreshedEffectId
|
|
544
|
+
? statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(refreshedEffectId)) as Record<string, unknown> | undefined
|
|
545
|
+
: undefined;
|
|
546
|
+
return {
|
|
547
|
+
task: taskFromRow(taskRow),
|
|
548
|
+
changed: payload.changed === true,
|
|
549
|
+
deduplicated: true,
|
|
550
|
+
objectiveSha256: targetSha,
|
|
551
|
+
...(refreshedRow ? { refreshedEffect: effectFromRow(refreshedRow) } : {}),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
555
|
+
if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
556
|
+
const terminal = statement(db, "SELECT kind FROM events WHERE task_id = ? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') ORDER BY event_id DESC LIMIT 1", (s) => s.get(input.taskId)) as { kind?: string } | undefined;
|
|
557
|
+
if (terminal?.kind) throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
|
|
558
|
+
const task = taskFromRow(row);
|
|
559
|
+
const currentSha = objectiveSha256(task.objective);
|
|
560
|
+
if (currentSha !== input.expectedObjectiveSha256.toLowerCase()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_CONFLICT');
|
|
561
|
+
|
|
562
|
+
let refreshedEffect: WorkflowSupervisorEffect | undefined;
|
|
563
|
+
if (input.refreshEffect) {
|
|
564
|
+
const effectRow = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(input.refreshEffect!.effectId)) as Record<string, unknown> | undefined;
|
|
565
|
+
if (!effectRow) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
566
|
+
const effect = effectFromRow(effectRow);
|
|
567
|
+
if (effect.taskId !== input.taskId) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT');
|
|
568
|
+
if (effect.prompt !== input.refreshEffect.expectedPrompt) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_PROMPT_CONFLICT');
|
|
569
|
+
const dispatched = effectDispatchLedger(db, effect.effectId).generations > 0;
|
|
570
|
+
const applied = Boolean(statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effect.effectId)));
|
|
571
|
+
const superseded = effectSuperseded(db, effect.effectId);
|
|
572
|
+
if (!dispatched && !applied && !superseded) {
|
|
573
|
+
statement(db, 'UPDATE effects SET prompt_text = ? WHERE effect_id = ?', (s) => s.run(input.refreshEffect!.prompt, effect.effectId));
|
|
574
|
+
const updatedEffect = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(effect.effectId)) as Record<string, unknown> | undefined;
|
|
575
|
+
if (!updatedEffect) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
576
|
+
refreshedEffect = effectFromRow(updatedEffect);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const changed = task.objective !== objective;
|
|
581
|
+
if (changed) statement(db, 'UPDATE tasks SET objective = ? WHERE task_id = ?', (s) => s.run(objective, input.taskId));
|
|
582
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
|
|
583
|
+
input.taskId,
|
|
584
|
+
eventKey,
|
|
585
|
+
'task_objective_revised',
|
|
586
|
+
json({
|
|
587
|
+
request_id: input.requestId,
|
|
588
|
+
authorized_by: input.authorizedBy,
|
|
589
|
+
previous_objective_sha256: currentSha,
|
|
590
|
+
objective_sha256: targetSha,
|
|
591
|
+
changed,
|
|
592
|
+
...(refreshedEffect ? { refreshed_effect_id: refreshedEffect.effectId } : {}),
|
|
593
|
+
}),
|
|
594
|
+
now(),
|
|
595
|
+
));
|
|
596
|
+
const updatedTask = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
597
|
+
if (!updatedTask) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
|
|
598
|
+
return { task: taskFromRow(updatedTask), changed, deduplicated: false, objectiveSha256: targetSha, ...(refreshedEffect ? { refreshedEffect } : {}) };
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
getTask(taskId: string): WorkflowSupervisorTask | undefined { return this.read((db) => { const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(taskId)); return row ? taskFromRow(row as Record<string, unknown>) : undefined; }); }
|
|
602
|
+
bindBootstrapConversation(taskId: string, conversationId: string, conversationUrl: string): WorkflowSupervisorTask {
|
|
603
|
+
return this.transaction((db) => {
|
|
604
|
+
const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
605
|
+
if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
606
|
+
const task = taskFromRow(row);
|
|
607
|
+
if (!task.conversationId.startsWith('bootstrap:')) {
|
|
608
|
+
if (task.conversationId === conversationId && task.conversationUrl === conversationUrl) return task;
|
|
609
|
+
throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_ALREADY_BOUND');
|
|
610
|
+
}
|
|
611
|
+
// The migration event ledger is the sole authority for previously used
|
|
612
|
+
// source conversations. Do not rebind any retired source, including one
|
|
613
|
+
// from an earlier 15-turn rotation (not only the immediately prior chat).
|
|
614
|
+
const retired = statement(db, `SELECT 1 AS ok FROM events
|
|
615
|
+
WHERE task_id=? AND kind='conversation_migrated'
|
|
616
|
+
AND json_extract(payload_json,'$.from_conversation_id')=? LIMIT 1`,
|
|
617
|
+
(s) => s.get(taskId, conversationId));
|
|
618
|
+
if (retired) throw new Error('WORKFLOW_SUPERVISOR_FRESH_CONVERSATION_REBOUND_SOURCE');
|
|
619
|
+
const conflict = statement(db, 'SELECT task_id FROM tasks WHERE conversation_id = ? AND task_id <> ?', (s) => s.get(conversationId, taskId)) as { task_id?: string } | undefined;
|
|
620
|
+
if (conflict) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_CONVERSATION_CONFLICT');
|
|
621
|
+
statement(db, 'UPDATE tasks SET conversation_id = ?, conversation_url = ? WHERE task_id = ?', (s) => s.run(conversationId, conversationUrl, taskId));
|
|
622
|
+
const bound = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
623
|
+
if (!bound) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
|
|
624
|
+
return taskFromRow(bound);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
getTaskByConversationId(conversationId: string): WorkflowSupervisorTask | undefined { return this.read((db) => { const row = statement(db, 'SELECT * FROM tasks WHERE conversation_id = ?', (s) => s.get(conversationId)); return row ? taskFromRow(row as Record<string, unknown>) : undefined; }); }
|
|
628
|
+
listTasks(): WorkflowSupervisorTask[] { return this.read((db) => statement(db, 'SELECT * FROM tasks ORDER BY created_at, task_id', (s) => s.all()).map((row) => taskFromRow(row as Record<string, unknown>))); }
|
|
629
|
+
listTasksWithoutCausalObligation(limit = 16): WorkflowSupervisorTask[] {
|
|
630
|
+
const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 128));
|
|
631
|
+
return this.read((db) => statement(db, `SELECT t.* FROM tasks t
|
|
632
|
+
WHERE NOT EXISTS (
|
|
633
|
+
SELECT 1 FROM events terminal
|
|
634
|
+
WHERE terminal.task_id = t.task_id
|
|
635
|
+
AND terminal.kind IN ('terminal_done','terminal_needs_user','terminal_stopped')
|
|
636
|
+
)
|
|
637
|
+
AND NOT EXISTS (
|
|
638
|
+
SELECT 1 FROM effects e
|
|
639
|
+
WHERE e.task_id = t.task_id
|
|
640
|
+
AND NOT EXISTS (
|
|
641
|
+
SELECT 1 FROM events applied
|
|
642
|
+
WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied'
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
AND NOT EXISTS (
|
|
646
|
+
SELECT 1 FROM effects e
|
|
647
|
+
WHERE e.task_id = t.task_id
|
|
648
|
+
AND EXISTS (
|
|
649
|
+
SELECT 1 FROM events applied
|
|
650
|
+
WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied'
|
|
651
|
+
)
|
|
652
|
+
AND NOT EXISTS (
|
|
653
|
+
SELECT 1 FROM completions c
|
|
654
|
+
WHERE c.task_id = e.task_id AND c.source_effect_id = e.effect_id
|
|
655
|
+
)
|
|
656
|
+
)
|
|
657
|
+
ORDER BY t.created_at, t.task_id
|
|
658
|
+
LIMIT ?`, (s) => s.all(boundedLimit))
|
|
659
|
+
.map((row) => taskFromRow(row as Record<string, unknown>)));
|
|
660
|
+
}
|
|
661
|
+
getEffect(effectId: string): WorkflowSupervisorEffect | undefined { return this.read((db) => { const row = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(effectId)); return row ? effectFromRow(row as Record<string, unknown>) : undefined; }); }
|
|
662
|
+
currentUnappliedEffect(taskId: string): WorkflowSupervisorEffect | undefined { return this.read((db) => oldestUnappliedEffect(db, taskId)); }
|
|
663
|
+
getEffectByOriginKey(originKey: string): WorkflowSupervisorEffect | undefined { return this.read((db) => { const row = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(originKey)); return row ? effectFromRow(row as Record<string, unknown>) : undefined; }); }
|
|
664
|
+
getCompletion(completionFingerprint: string): WorkflowSupervisorCompletion | undefined { return this.read((db) => { const row = statement(db, 'SELECT * FROM completions WHERE completion_fingerprint = ?', (s) => s.get(completionFingerprint)); return row ? completionFromRow(row as Record<string, unknown>) : undefined; }); }
|
|
665
|
+
getCompletionBySourceEffectId(taskId: string, sourceEffectId: string): WorkflowSupervisorCompletion | undefined {
|
|
666
|
+
return this.read((db) => {
|
|
667
|
+
const row = statement(db, 'SELECT * FROM completions WHERE task_id = ? AND source_effect_id = ? LIMIT 1', (s) => s.get(taskId, sourceEffectId)) as Record<string, unknown> | undefined;
|
|
668
|
+
return row ? completionFromRow(row) : undefined;
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
listContinueCompletionsAwaitingSuccessor(limit = 16): WorkflowSupervisorCompletion[] {
|
|
672
|
+
const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 128));
|
|
673
|
+
return this.read((db) => statement(db, `SELECT c.* FROM completions c
|
|
674
|
+
WHERE c.action = 'CONTINUE'
|
|
675
|
+
AND NOT EXISTS (SELECT 1 FROM effects successor WHERE successor.origin_key = 'completion:' || c.completion_fingerprint)
|
|
676
|
+
AND NOT EXISTS (SELECT 1 FROM events terminal WHERE terminal.task_id = c.task_id AND terminal.kind IN ('terminal_done','terminal_needs_user','terminal_stopped'))
|
|
677
|
+
ORDER BY c.committed_at DESC, c.completion_fingerprint DESC LIMIT ?`, (s) => s.all(boundedLimit))
|
|
678
|
+
.map((row) => completionFromRow(row as Record<string, unknown>)));
|
|
679
|
+
}
|
|
680
|
+
getCompletionByResponseSha256(taskId: string, responseSha256: string): WorkflowSupervisorCompletion | undefined {
|
|
681
|
+
return this.read((db) => {
|
|
682
|
+
const rows = statement(db, 'SELECT * FROM completions WHERE task_id = ? AND response_sha256 = ? ORDER BY committed_at, completion_fingerprint LIMIT 2', (s) => s.all(taskId, responseSha256)) as Record<string, unknown>[];
|
|
683
|
+
if (rows.length > 1) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_COMPLETION_AMBIGUOUS');
|
|
684
|
+
return rows[0] ? completionFromRow(rows[0]) : undefined;
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
/** The only replay candidate when no applied effect is awaiting a receipt. */
|
|
688
|
+
getLatestCompletion(taskId: string): WorkflowSupervisorCompletion | undefined {
|
|
689
|
+
return this.read((db) => {
|
|
690
|
+
const row = statement(db, 'SELECT * FROM completions WHERE task_id = ? ORDER BY committed_at DESC, completion_fingerprint DESC LIMIT 1', (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
691
|
+
return row ? completionFromRow(row) : undefined;
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
continuationProof(input: { repoId?: string; activeReleaseId: string; notBefore: string }): WorkflowSupervisorContinuationProof | undefined {
|
|
695
|
+
if (!Number.isFinite(Date.parse(input.notBefore))) throw new Error('WORKFLOW_SUPERVISOR_PROOF_BOUNDARY_INVALID');
|
|
696
|
+
const activeReleaseId = boundedText(input.activeReleaseId);
|
|
697
|
+
if (!activeReleaseId) throw new Error('WORKFLOW_SUPERVISOR_PROOF_RELEASE_REQUIRED');
|
|
698
|
+
return this.read((db) => {
|
|
699
|
+
const taskRows = statement(db, 'SELECT * FROM tasks ORDER BY created_at DESC, task_id DESC', (s) => s.all()) as Record<string, unknown>[];
|
|
700
|
+
for (const taskRow of taskRows) {
|
|
701
|
+
const task = taskFromRow(taskRow);
|
|
702
|
+
const taskRepoId = boundedText(task.completionContract.repo_id) ?? boundedText(task.continuationPolicy.repo_id);
|
|
703
|
+
if (input.repoId?.trim() && taskRepoId !== input.repoId.trim()) continue;
|
|
704
|
+
const completionRows = statement(db, 'SELECT * FROM completions WHERE task_id = ? AND committed_at >= ? ORDER BY committed_at, completion_fingerprint', (s) => s.all(task.taskId, input.notBefore)) as Record<string, unknown>[];
|
|
705
|
+
const completions = completionRows.map(completionFromRow);
|
|
706
|
+
// Release acceptance proves continuation liveness, not semantic task completion.
|
|
707
|
+
// Requiring DONE here deadlocks a long-running Supervisor goal against its own known-good gate.
|
|
708
|
+
for (let index = 0; index + 1 < completions.length; index += 1) {
|
|
709
|
+
const first = completions[index]!;
|
|
710
|
+
const second = completions[index + 1]!;
|
|
711
|
+
if (first.action !== 'CONTINUE' || second.action !== 'CONTINUE') continue;
|
|
712
|
+
const rows = [first, second].map((completion) => statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(completion.sourceEffectId)) as Record<string, unknown> | undefined);
|
|
713
|
+
if (rows.some((row) => !row)) continue;
|
|
714
|
+
const [firstEffect, secondEffect] = rows.map((row) => effectFromRow(row!)) as [WorkflowSupervisorEffect, WorkflowSupervisorEffect];
|
|
715
|
+
if (secondEffect.sourceCompletionFingerprint !== first.completionFingerprint) continue;
|
|
716
|
+
const runtimeInstanceIds: string[] = [];
|
|
717
|
+
let valid = true;
|
|
718
|
+
for (const effect of [firstEffect, secondEffect]) {
|
|
719
|
+
const dispatchRows = statement(db, "SELECT payload_json FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_started' AND occurred_at >= ? ORDER BY event_id", (s) => s.all(effect.effectId, input.notBefore)) as Array<{ payload_json?: string }>;
|
|
720
|
+
if (dispatchRows.length !== 1) { valid = false; break; }
|
|
721
|
+
const evidence = parsedObject(dispatchRows[0]?.payload_json);
|
|
722
|
+
if (boundedText(evidence.active_release_id) !== activeReleaseId) { valid = false; break; }
|
|
723
|
+
const runtimeInstanceId = boundedText(evidence.runtime_instance_id);
|
|
724
|
+
if (!runtimeInstanceId) { valid = false; break; }
|
|
725
|
+
runtimeInstanceIds.push(runtimeInstanceId);
|
|
726
|
+
const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' AND occurred_at >= ? LIMIT 1", (s) => s.get(effect.effectId, input.notBefore));
|
|
727
|
+
if (!applied) { valid = false; break; }
|
|
728
|
+
}
|
|
729
|
+
const minimumRuntimeInstances = task.continuationPolicy.kind === 'standalone_supervisor' ? 1 : 2;
|
|
730
|
+
if (!valid || new Set(runtimeInstanceIds).size < minimumRuntimeInstances) continue;
|
|
731
|
+
return {
|
|
732
|
+
taskId: task.taskId, conversationId: task.conversationId, activeReleaseId,
|
|
733
|
+
actions: ['CONTINUE', 'CONTINUE'],
|
|
734
|
+
completionFingerprints: [first.completionFingerprint, second.completionFingerprint],
|
|
735
|
+
sourceEffectIds: [first.sourceEffectId, second.sourceEffectId],
|
|
736
|
+
runtimeInstanceIds: [...new Set(runtimeInstanceIds)],
|
|
737
|
+
firstCommittedAt: first.committedAt, lastCommittedAt: second.committedAt,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return undefined;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
latestEffectDispatch(effectId: string): { eventId: number; generation: number; evidence: Record<string, unknown> } | undefined {
|
|
745
|
+
return this.read((db) => {
|
|
746
|
+
const row = statement(db, "SELECT event_id,payload_json FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_started' ORDER BY event_id DESC LIMIT 1", (s) => s.get(effectId)) as { event_id?: number; payload_json?: string } | undefined;
|
|
747
|
+
if (!row?.event_id) return undefined;
|
|
748
|
+
return { eventId: Number(row.event_id), generation: storedGeneration(row.payload_json), evidence: parsedObject(row.payload_json) };
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Select the one browser command for a task's oldest un-applied effect.
|
|
753
|
+
*
|
|
754
|
+
* A dispatch generation is only minted from the canonical negative proof that
|
|
755
|
+
* the previous send never reached the conversation, and only while the effect
|
|
756
|
+
* stays inside its mechanical retry budget and retry window. Mutation retries
|
|
757
|
+
* retain a hard ceiling. Outcome-unknown reconciliation is read-only and stays
|
|
758
|
+
* eligible forever at a capped cadence so late provider evidence can recover
|
|
759
|
+
* the same effect without user intervention or another send.
|
|
760
|
+
*/
|
|
761
|
+
nextBrowserEffect(
|
|
762
|
+
taskId: string,
|
|
763
|
+
options: { nowMs?: number } = {},
|
|
764
|
+
): { effect: WorkflowSupervisorEffect; mode: 'send' | 'reconcile'; generation: number } | undefined {
|
|
765
|
+
return this.read((db) => {
|
|
766
|
+
const effect = oldestUnappliedEffect(db, taskId);
|
|
767
|
+
if (!effect) return undefined;
|
|
768
|
+
const ledger = effectDispatchLedger(db, effect.effectId);
|
|
769
|
+
const nowMs = options.nowMs ?? this.clockMs();
|
|
770
|
+
if (ledger.generations === 0) {
|
|
771
|
+
// A fresh continuation is one further provider turn of the same task.
|
|
772
|
+
// Spacing is measured from the completion that authorized it, so a fast
|
|
773
|
+
// provider reply cannot turn the outer-turn chain into a request storm.
|
|
774
|
+
const source = effect.sourceCompletionFingerprint
|
|
775
|
+
? statement(db, 'SELECT committed_at FROM completions WHERE completion_fingerprint = ?', (s) => s.get(effect.sourceCompletionFingerprint!)) as { committed_at?: string } | undefined
|
|
776
|
+
: undefined;
|
|
777
|
+
const committedAtMs = Date.parse(String(source?.committed_at ?? ''));
|
|
778
|
+
if (Number.isFinite(committedAtMs) && nowMs - committedAtMs < WORKFLOW_SUPERVISOR_MIN_TURN_INTERVAL_MS) return undefined;
|
|
779
|
+
if (this.preflightBlocked(db, effect.effectId)) return undefined;
|
|
780
|
+
return { effect, mode: 'send', generation: 1 };
|
|
781
|
+
}
|
|
782
|
+
const retryAuthorized = latestNotAppliedProofEventId(db, effect.effectId) > ledger.lastEventId;
|
|
783
|
+
if (!retryAuthorized) {
|
|
784
|
+
const unknownLedger = effectUnknownObservationLedger(db, effect.effectId, ledger.lastEventId);
|
|
785
|
+
if (unknownLedger.sameFingerprintCount > 0
|
|
786
|
+
&& nowMs - unknownLedger.lastOccurredAtMs < unknownObservationDelayMs(unknownLedger.sameFingerprintCount)) {
|
|
787
|
+
return undefined;
|
|
788
|
+
}
|
|
789
|
+
return { effect, mode: 'reconcile', generation: ledger.lastGeneration };
|
|
790
|
+
}
|
|
791
|
+
if (ledger.generations >= WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + ledger.budgetRefunds) return undefined;
|
|
792
|
+
if (this.preflightBlocked(db, effect.effectId)) return undefined;
|
|
793
|
+
if (nowMs - ledger.lastOccurredAtMs < workflowSupervisorDispatchRetryDelayMs(ledger.lastGeneration)) {
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
return { effect, mode: 'send', generation: ledger.lastGeneration + 1 };
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
effectDispatchBudget(effectId: string): WorkflowSupervisorEffectDispatchBudget {
|
|
800
|
+
return this.read((db) => {
|
|
801
|
+
const ledger = effectDispatchLedger(db, effectId);
|
|
802
|
+
const exhausted = ledger.generations >= WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + ledger.budgetRefunds
|
|
803
|
+
&& !statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId));
|
|
804
|
+
return {
|
|
805
|
+
effectId,
|
|
806
|
+
generations: ledger.generations,
|
|
807
|
+
maxGenerations: WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS,
|
|
808
|
+
...(ledger.lastOccurredAtMs ? { lastDispatchedAtMs: ledger.lastOccurredAtMs } : {}),
|
|
809
|
+
...(ledger.generations ? { retryDelayMs: workflowSupervisorDispatchRetryDelayMs(ledger.lastGeneration) } : {}),
|
|
810
|
+
exhausted,
|
|
811
|
+
};
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Read-only classification of why a non-terminal task is or is not advancing.
|
|
816
|
+
* It derives only from the existing effect/observation ledger, so it adds no
|
|
817
|
+
* second durable state and no new lifecycle authority.
|
|
818
|
+
*/
|
|
819
|
+
taskStall(taskId: string): WorkflowSupervisorTaskStall {
|
|
820
|
+
if (this.terminalAction(taskId)) return { state: 'terminal' };
|
|
821
|
+
const next = this.nextBrowserEffect(taskId);
|
|
822
|
+
if (next) {
|
|
823
|
+
if (next.mode === 'send') return { state: 'deliverable', effectId: next.effect.effectId, mode: 'send', generation: next.generation };
|
|
824
|
+
return { state: 'spaced', effectId: next.effect.effectId, reason: 'unknown_observation_spacing' };
|
|
825
|
+
}
|
|
826
|
+
const pending = this.currentUnappliedEffect(taskId);
|
|
827
|
+
if (pending) {
|
|
828
|
+
const ledger = this.read((db) => ({ ...effectDispatchLedger(db, pending.effectId), proof: latestNotAppliedProofEventId(db, pending.effectId), preflightBlocked: this.preflightBlocked(db, pending.effectId) }));
|
|
829
|
+
if (ledger.preflightBlocked) return { state: 'spaced', effectId: pending.effectId, reason: 'pre_send_infrastructure_blocked' };
|
|
830
|
+
const ceiling = WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + ledger.budgetRefunds;
|
|
831
|
+
const retryAuthorized = ledger.generations === 0 || ledger.proof > ledger.lastEventId;
|
|
832
|
+
// An exhausted retry ceiling is the operator-facing state: no amount of
|
|
833
|
+
// waiting can mint another generation. Everything else is ordinary
|
|
834
|
+
// mechanical spacing and needs no operator action.
|
|
835
|
+
if (retryAuthorized && ledger.generations >= ceiling) {
|
|
836
|
+
return { state: 'retryable', effectId: pending.effectId, generations: ledger.generations, maxGenerations: ceiling };
|
|
837
|
+
}
|
|
838
|
+
return { state: 'spaced', effectId: pending.effectId, reason: retryAuthorized ? 'retry_spacing' : 'unknown_observation_spacing' };
|
|
839
|
+
}
|
|
840
|
+
const leaf = this.latestAppliedLeafEffectWithoutCompletion(taskId);
|
|
841
|
+
if (leaf && this.providerResumeExhausted(leaf.effectId)) return { state: 'provider_resume_exhausted', effectId: leaf.effectId };
|
|
842
|
+
// An applied effect awaiting its receipt is a live obligation even when its
|
|
843
|
+
// bounded resume is exhausted and its recovery child was retired, so it is
|
|
844
|
+
// never reported as inert.
|
|
845
|
+
if (this.hasAppliedEffectAwaitingCompletion(taskId)) {
|
|
846
|
+
return { state: 'spaced', ...(leaf ? { effectId: leaf.effectId } : {}), reason: 'awaiting_completion_receipt' };
|
|
847
|
+
}
|
|
848
|
+
return { state: 'inert' };
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Explicit operator authority to continue a *mechanically proven* un-applied
|
|
852
|
+
* effect after its retry ceiling. The proof is the dispatch owner's persisted
|
|
853
|
+
* pre-send negative result, never page absence, so this can never replay a
|
|
854
|
+
* submitted mutation. Idempotent per request id and bounded per effect.
|
|
855
|
+
*/
|
|
856
|
+
authorizeOperatorRetry(input: { effectId: string; requestId: string; reason: string; authorizedBy: string }): { refunds: number; generations: number; deduplicated: boolean } {
|
|
857
|
+
return this.transaction((db) => {
|
|
858
|
+
const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(input.effectId)) as { task_id?: string } | undefined;
|
|
859
|
+
if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
860
|
+
if (statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(input.effectId))) {
|
|
861
|
+
throw new Error('WORKFLOW_SUPERVISOR_EFFECT_ALREADY_APPLIED');
|
|
862
|
+
}
|
|
863
|
+
if (statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_outcome_unknown_superseded' LIMIT 1", (s) => s.get(input.effectId))) {
|
|
864
|
+
throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_EFFECT_SUPERSEDED');
|
|
865
|
+
}
|
|
866
|
+
const ledger = effectDispatchLedger(db, input.effectId);
|
|
867
|
+
if (ledger.generations === 0 || latestNotAppliedProofEventId(db, input.effectId) <= ledger.lastEventId) {
|
|
868
|
+
throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_OUTCOME_UNKNOWN');
|
|
869
|
+
}
|
|
870
|
+
const key = `effect-dispatch-operator-refund:${input.effectId}:${input.requestId}`;
|
|
871
|
+
const existing = statement(db, 'SELECT 1 AS ok FROM events WHERE event_key = ?', (s) => s.get(key));
|
|
872
|
+
if (existing) return { refunds: operatorAuthorizedRefundCount(db, input.effectId), generations: ledger.generations, deduplicated: true };
|
|
873
|
+
if (operatorAuthorizedRefundCount(db, input.effectId) >= WORKFLOW_SUPERVISOR_MAX_OPERATOR_REFUNDS) {
|
|
874
|
+
throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_REFUND_BUDGET_EXHAUSTED');
|
|
875
|
+
}
|
|
876
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
877
|
+
effect.task_id!, key, 'effect_dispatch_budget_refunded', input.effectId,
|
|
878
|
+
json({
|
|
879
|
+
reason: 'operator_authorized_retry', request_id: input.requestId, authorized_by: input.authorizedBy,
|
|
880
|
+
reason_text: input.reason, prior_generations: ledger.generations, attestation: 'mechanical_pre_send_negative_proof',
|
|
881
|
+
}), now()));
|
|
882
|
+
return { refunds: operatorAuthorizedRefundCount(db, input.effectId), generations: ledger.generations, deduplicated: false };
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Explicit operator authority to retire an effect whose submission outcome is
|
|
887
|
+
* genuinely unknown. It is never resent; the durable fact records who decided
|
|
888
|
+
* to stop waiting, and the replacement turn is reserved in the same
|
|
889
|
+
* transaction so the task can never sit in a half-superseded state. Requires
|
|
890
|
+
* the caller to have stated the intent.
|
|
891
|
+
*/
|
|
892
|
+
supersedeUnknownEffect(input: {
|
|
893
|
+
effectId: string;
|
|
894
|
+
requestId: string;
|
|
895
|
+
reason: string;
|
|
896
|
+
authorizedBy: string;
|
|
897
|
+
replacement: { effectId: string; prompt: string };
|
|
898
|
+
}): { superseded: boolean; recoveryEffect: WorkflowSupervisorEffect } {
|
|
899
|
+
const originKey = `operator-recovery:${input.effectId}:${input.requestId}`;
|
|
900
|
+
return this.transaction((db) => {
|
|
901
|
+
const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(input.effectId)) as { task_id?: string } | undefined;
|
|
902
|
+
if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
903
|
+
const taskId = effect.task_id;
|
|
904
|
+
const reserve = (): WorkflowSupervisorEffect => this.reserveEffectWithin(db, {
|
|
905
|
+
taskId, effectId: input.replacement.effectId, kind: 'recovery', originKey, prompt: input.replacement.prompt,
|
|
906
|
+
});
|
|
907
|
+
const existingReplacement = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(originKey)) as Record<string, unknown> | undefined;
|
|
908
|
+
if (existingReplacement) return { superseded: true, recoveryEffect: effectFromRow(existingReplacement) };
|
|
909
|
+
if (statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(input.effectId))) {
|
|
910
|
+
throw new Error('WORKFLOW_SUPERVISOR_EFFECT_ALREADY_APPLIED');
|
|
911
|
+
}
|
|
912
|
+
const ledger = effectDispatchLedger(db, input.effectId);
|
|
913
|
+
if (ledger.generations === 0) throw new Error('WORKFLOW_SUPERVISOR_SUPERSEDE_NOT_DISPATCHED');
|
|
914
|
+
if (latestNotAppliedProofEventId(db, input.effectId) > ledger.lastEventId) {
|
|
915
|
+
throw new Error('WORKFLOW_SUPERVISOR_SUPERSEDE_NOT_APPLIED_PROVEN');
|
|
916
|
+
}
|
|
917
|
+
const key = `effect-outcome-unknown-superseded:${input.effectId}:${input.requestId}`;
|
|
918
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
919
|
+
taskId, key, 'effect_outcome_unknown_superseded', input.effectId,
|
|
920
|
+
json({
|
|
921
|
+
request_id: input.requestId, authorized_by: input.authorizedBy, reason: input.reason,
|
|
922
|
+
generation: ledger.lastGeneration, replacement_effect_id: input.replacement.effectId,
|
|
923
|
+
attestation: 'explicit_operator_authorization',
|
|
924
|
+
}), now()));
|
|
925
|
+
return { superseded: true, recoveryEffect: reserve() };
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
terminalAction(taskId: string): WorkflowSupervisorTerminalState | undefined {
|
|
929
|
+
return this.read((db) => {
|
|
930
|
+
const row = statement(db, "SELECT kind FROM events WHERE task_id = ? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') ORDER BY event_id DESC LIMIT 1", (s) => s.get(taskId)) as { kind?: string } | undefined;
|
|
931
|
+
return row?.kind === 'terminal_done' ? 'DONE' : row?.kind === 'terminal_needs_user' ? 'NEEDS_USER' : row?.kind === 'terminal_stopped' ? 'STOPPED' : undefined;
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
stopTask(taskId: string, reason: string): { taskId: string; terminal: 'STOPPED'; deduplicated: boolean } {
|
|
935
|
+
return this.transaction((db) => {
|
|
936
|
+
const task = statement(db, 'SELECT task_id FROM tasks WHERE task_id = ?', (s) => s.get(taskId)) as { task_id?: string } | undefined;
|
|
937
|
+
if (!task) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
938
|
+
const existing = statement(db, "SELECT kind FROM events WHERE task_id = ? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') ORDER BY event_id DESC LIMIT 1", (s) => s.get(taskId)) as { kind?: string } | undefined;
|
|
939
|
+
if (existing?.kind === 'terminal_stopped') return { taskId, terminal: 'STOPPED', deduplicated: true };
|
|
940
|
+
if (existing?.kind) {
|
|
941
|
+
const terminal = existing.kind === 'terminal_done' ? 'DONE' : 'NEEDS_USER';
|
|
942
|
+
throw new Error(`WORKFLOW_SUPERVISOR_TASK_TERMINAL:${terminal}`);
|
|
943
|
+
}
|
|
944
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(taskId, `task-stop:${taskId}`, 'terminal_stopped', json({ reason }), now()));
|
|
945
|
+
return { taskId, terminal: 'STOPPED', deduplicated: false };
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
effectApplied(effectId: string): boolean { return this.read((db) => Boolean(statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId)))); }
|
|
949
|
+
providerResumeExhausted(effectId: string): boolean { return this.read((db) => Boolean(statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'assistant_recovery_exhausted' LIMIT 1", (s) => s.get(effectId)))); }
|
|
950
|
+
latestAppliedEffectWithoutCompletion(taskId: string): WorkflowSupervisorEffect | undefined {
|
|
951
|
+
return this.read((db) => {
|
|
952
|
+
const row = statement(db, `SELECT e.* FROM effects e
|
|
953
|
+
WHERE e.task_id = ?
|
|
954
|
+
AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied')
|
|
955
|
+
AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.task_id = e.task_id AND c.source_effect_id = e.effect_id)
|
|
956
|
+
AND NOT EXISTS (SELECT 1 FROM effects child WHERE child.origin_key = 'provider-recovery:' || e.effect_id)
|
|
957
|
+
AND NOT EXISTS (SELECT 1 FROM events exhausted WHERE exhausted.effect_id = e.effect_id AND exhausted.kind = 'assistant_recovery_exhausted')
|
|
958
|
+
ORDER BY (SELECT MAX(event_id) FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied') DESC
|
|
959
|
+
LIMIT 1`, (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
960
|
+
return row ? effectFromRow(row) : undefined;
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
latestAppliedLeafEffectWithoutCompletion(taskId: string): WorkflowSupervisorEffect | undefined {
|
|
964
|
+
return this.read((db) => {
|
|
965
|
+
const row = statement(db, `SELECT e.* FROM effects e
|
|
966
|
+
WHERE e.task_id = ?
|
|
967
|
+
AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied')
|
|
968
|
+
AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.task_id = e.task_id AND c.source_effect_id = e.effect_id)
|
|
969
|
+
AND NOT EXISTS (SELECT 1 FROM effects child WHERE child.origin_key = 'provider-recovery:' || e.effect_id)
|
|
970
|
+
ORDER BY (SELECT MAX(event_id) FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied') DESC
|
|
971
|
+
LIMIT 1`, (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
972
|
+
return row ? effectFromRow(row) : undefined;
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* An applied browser effect remains a Supervisor-owned external observation
|
|
978
|
+
* obligation even when its lower ControllerRound temporarily waits. This
|
|
979
|
+
* intentionally includes effects that already have a bounded provider
|
|
980
|
+
* recovery child; the native adapter must stay alive long enough to observe
|
|
981
|
+
* and dispatch that recovery instead of abandoning the browser task.
|
|
982
|
+
*/
|
|
983
|
+
hasAppliedEffectAwaitingCompletion(taskId: string): boolean {
|
|
984
|
+
return this.read((db) => Boolean(statement(db, `SELECT 1 AS ok FROM effects e
|
|
985
|
+
WHERE e.task_id = ?
|
|
986
|
+
AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied')
|
|
987
|
+
AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.task_id = e.task_id AND c.source_effect_id = e.effect_id)
|
|
988
|
+
LIMIT 1`, (s) => s.get(taskId))));
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
observeProviderTurn(input: {
|
|
992
|
+
taskId: string;
|
|
993
|
+
effectId: string;
|
|
994
|
+
generating: boolean;
|
|
995
|
+
assistantDigest: string;
|
|
996
|
+
providerFailureCode?: string;
|
|
997
|
+
receiptMissing?: boolean;
|
|
998
|
+
observedAtMs: number;
|
|
999
|
+
graceMs: number;
|
|
1000
|
+
recovery: { effectId: string; prompt: string };
|
|
1001
|
+
}): { state: 'none' | 'generating' | 'idle_pending' | 'recovery_reserved' | 'exhausted'; recoveryEffect?: WorkflowSupervisorEffect } {
|
|
1002
|
+
if (!Number.isFinite(input.observedAtMs)) throw new Error('WORKFLOW_SUPERVISOR_PROVIDER_OBSERVED_AT_INVALID');
|
|
1003
|
+
const graceMs = Math.max(1_000, Math.min(10 * 60_000, Math.floor(input.graceMs)));
|
|
1004
|
+
const digest = input.assistantDigest.trim().slice(0, 128);
|
|
1005
|
+
const observedAt = new Date(input.observedAtMs).toISOString();
|
|
1006
|
+
return this.transaction((db) => {
|
|
1007
|
+
const row = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(input.effectId)) as Record<string, unknown> | undefined;
|
|
1008
|
+
if (!row) return { state: 'none' };
|
|
1009
|
+
const effect = effectFromRow(row);
|
|
1010
|
+
if (effect.taskId !== input.taskId) throw new Error('WORKFLOW_SUPERVISOR_PROVIDER_EFFECT_TASK_MISMATCH');
|
|
1011
|
+
const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effect.effectId));
|
|
1012
|
+
const completed = statement(db, 'SELECT 1 AS ok FROM completions WHERE task_id = ? AND source_effect_id = ? LIMIT 1', (s) => s.get(input.taskId, effect.effectId));
|
|
1013
|
+
if (!applied || completed) return { state: 'none' };
|
|
1014
|
+
const providerRecoveryDepth = providerRecoveryDepthWithin(db, effect.effectId);
|
|
1015
|
+
const recoveryOrigin = `provider-recovery:${effect.effectId}`;
|
|
1016
|
+
const providerFailureCode = input.providerFailureCode?.trim().slice(0, 128);
|
|
1017
|
+
if (providerFailureCode) {
|
|
1018
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
1019
|
+
input.taskId,
|
|
1020
|
+
`assistant-provider-failed:${effect.effectId}:${providerFailureCode}`,
|
|
1021
|
+
'assistant_provider_failed',
|
|
1022
|
+
effect.effectId,
|
|
1023
|
+
json({ code: providerFailureCode, assistant_digest: digest }),
|
|
1024
|
+
observedAt,
|
|
1025
|
+
));
|
|
1026
|
+
}
|
|
1027
|
+
const existingRecovery = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(recoveryOrigin)) as Record<string, unknown> | undefined;
|
|
1028
|
+
if (existingRecovery) return { state: 'recovery_reserved', recoveryEffect: effectFromRow(existingRecovery) };
|
|
1029
|
+
if (providerFailureCode) {
|
|
1030
|
+
if (providerRecoveryDepth >= WORKFLOW_SUPERVISOR_MAX_PROVIDER_RECOVERY_TURNS) {
|
|
1031
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, `assistant-recovery-exhausted:${effect.effectId}`, 'assistant_recovery_exhausted', effect.effectId, json({ assistant_digest: digest, provider_failure_code: providerFailureCode, exactly_once_resume: true, recovery_depth: providerRecoveryDepth, max_recovery_turns: WORKFLOW_SUPERVISOR_MAX_PROVIDER_RECOVERY_TURNS }), observedAt));
|
|
1032
|
+
return { state: 'exhausted' };
|
|
1033
|
+
}
|
|
1034
|
+
const recoveryEffect = this.reserveEffectWithin(db, { taskId: input.taskId, effectId: input.recovery.effectId, kind: 'recovery', originKey: recoveryOrigin, prompt: input.recovery.prompt });
|
|
1035
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, `assistant-recovery-reserved:${effect.effectId}`, 'assistant_recovery_reserved', effect.effectId, json({ recovery_effect_id: recoveryEffect.effectId, provider_failure_code: providerFailureCode, exactly_once_resume: true }), observedAt));
|
|
1036
|
+
return { state: 'recovery_reserved', recoveryEffect };
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const latest = statement(db, `SELECT event_id,kind,payload_json,occurred_at FROM events
|
|
1040
|
+
WHERE effect_id = ? AND kind IN ('assistant_provider_generating','assistant_provider_idle')
|
|
1041
|
+
ORDER BY event_id DESC LIMIT 1`, (s) => s.get(effect.effectId)) as { event_id?: number; kind?: string; payload_json?: string; occurred_at?: string } | undefined;
|
|
1042
|
+
const latestEvidence = parsedObject(latest?.payload_json);
|
|
1043
|
+
const desiredKind = input.generating ? 'assistant_provider_generating' : 'assistant_provider_idle';
|
|
1044
|
+
const sameState = latest?.kind === desiredKind && latestEvidence.assistant_digest === digest;
|
|
1045
|
+
if (!sameState) {
|
|
1046
|
+
const eventKey = `assistant-provider-state:${effect.effectId}:${desiredKind}:${Number(latest?.event_id ?? 0) + 1}:${digest || 'empty'}`;
|
|
1047
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, eventKey, desiredKind, effect.effectId, json({ assistant_digest: digest }), observedAt));
|
|
1048
|
+
return { state: input.generating ? 'generating' : 'idle_pending' };
|
|
1049
|
+
}
|
|
1050
|
+
const unchangedSinceMs = Date.parse(String(latest?.occurred_at ?? ''));
|
|
1051
|
+
if (!Number.isFinite(unchangedSinceMs) || input.observedAtMs - unchangedSinceMs < graceMs) {
|
|
1052
|
+
return { state: input.generating ? 'generating' : 'idle_pending' };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Ordinary idle text cannot authorize recovery. Only a stable, exact
|
|
1056
|
+
// assistant response proven to lack its receipt can reserve the existing
|
|
1057
|
+
// bounded, separately identified recovery; never replay the source effect.
|
|
1058
|
+
if (!input.generating && !input.receiptMissing) return { state: 'idle_pending' };
|
|
1059
|
+
|
|
1060
|
+
// Progress, not the provider's visible "generating" control, is the
|
|
1061
|
+
// liveness authority. A frozen page can leave that control present for
|
|
1062
|
+
// hours after tool/output progress stopped. Once the assistant/activity
|
|
1063
|
+
// digest is unchanged for the existing grace window, reserve the next
|
|
1064
|
+
// separately identified recovery turn without replaying the applied source.
|
|
1065
|
+
// The causal chain is bounded to two automatic recovery turns.
|
|
1066
|
+
if (providerRecoveryDepth >= WORKFLOW_SUPERVISOR_MAX_PROVIDER_RECOVERY_TURNS) {
|
|
1067
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, `assistant-recovery-exhausted:${effect.effectId}`, 'assistant_recovery_exhausted', effect.effectId, json({ assistant_digest: digest, exactly_once_resume: true, stale_generation: input.generating, recovery_depth: providerRecoveryDepth, max_recovery_turns: WORKFLOW_SUPERVISOR_MAX_PROVIDER_RECOVERY_TURNS }), observedAt));
|
|
1068
|
+
return { state: 'exhausted' };
|
|
1069
|
+
}
|
|
1070
|
+
const recoveryEffect = this.reserveEffectWithin(db, { taskId: input.taskId, effectId: input.recovery.effectId, kind: 'recovery', originKey: recoveryOrigin, prompt: input.recovery.prompt });
|
|
1071
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, `assistant-recovery-reserved:${effect.effectId}`, 'assistant_recovery_reserved', effect.effectId, json({ recovery_effect_id: recoveryEffect.effectId, exactly_once_resume: true, stale_generation: input.generating }), observedAt));
|
|
1072
|
+
return { state: 'recovery_reserved', recoveryEffect };
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
reserveEffect(input: { taskId: string; effectId: string; kind: WorkflowEffectKind; originKey: string; sourceCompletionFingerprint?: string; prompt: string }): WorkflowSupervisorEffect {
|
|
1077
|
+
return this.transaction((db) => this.reserveEffectWithin(db, input));
|
|
1078
|
+
}
|
|
1079
|
+
reserveOperatorRecovery(input: { taskId: string; sourceEffectId: string; requestId: string; reason: string; effectId: string; prompt: string }): WorkflowSupervisorEffect {
|
|
1080
|
+
return this.transaction((db) => {
|
|
1081
|
+
if (oldestUnappliedEffect(db, input.taskId)) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_OUTCOME_UNKNOWN');
|
|
1082
|
+
const source = statement(db, `SELECT e.* FROM effects e WHERE e.task_id = ?
|
|
1083
|
+
AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id=e.effect_id AND applied.kind='effect_applied')
|
|
1084
|
+
AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.source_effect_id=e.effect_id)
|
|
1085
|
+
AND NOT EXISTS (SELECT 1 FROM effects child WHERE child.origin_key='provider-recovery:' || e.effect_id)
|
|
1086
|
+
ORDER BY e.created_at DESC LIMIT 1`, (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
1087
|
+
if (source?.effect_id !== input.sourceEffectId) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
|
|
1088
|
+
if (statement(db, "SELECT 1 FROM events WHERE task_id=? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') LIMIT 1", (s) => s.get(input.taskId))) throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
|
|
1089
|
+
const effect = this.reserveEffectWithin(db, { taskId: input.taskId, effectId: input.effectId, kind: 'recovery', originKey: `provider-recovery:${input.sourceEffectId}`, prompt: input.prompt });
|
|
1090
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
1091
|
+
input.taskId, `assistant-recovery-reserved:${input.sourceEffectId}`, 'assistant_recovery_reserved', input.sourceEffectId,
|
|
1092
|
+
json({ recovery_effect_id: effect.effectId, requested_by: 'user', request_id: input.requestId, reason: input.reason, exactly_once_resume: true }), now()));
|
|
1093
|
+
return effect;
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Explicit operator recovery for an applied effect whose one bounded provider
|
|
1098
|
+
* resume was already exhausted. The source effect stays applied and completed
|
|
1099
|
+
* history is untouched; this only reserves one new, separately identified
|
|
1100
|
+
* provider turn, so it can never replay the source mutation.
|
|
1101
|
+
*/
|
|
1102
|
+
reserveOperatorProviderRecovery(input: { taskId: string; sourceEffectId: string; requestId: string; reason: string; authorizedBy: string; originKey: string; effectId: string; prompt: string }): WorkflowSupervisorEffect {
|
|
1103
|
+
return this.transaction((db) => {
|
|
1104
|
+
if (oldestUnappliedEffect(db, input.taskId)) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_OUTCOME_UNKNOWN');
|
|
1105
|
+
if (statement(db, "SELECT 1 FROM events WHERE task_id=? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') LIMIT 1", (s) => s.get(input.taskId))) {
|
|
1106
|
+
throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
|
|
1107
|
+
}
|
|
1108
|
+
const source = statement(db, `SELECT e.* FROM effects e WHERE e.task_id = ? AND e.effect_id = ?
|
|
1109
|
+
AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id=e.effect_id AND applied.kind='effect_applied')
|
|
1110
|
+
AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.source_effect_id=e.effect_id)
|
|
1111
|
+
LIMIT 1`, (s) => s.get(input.taskId, input.sourceEffectId)) as Record<string, unknown> | undefined;
|
|
1112
|
+
if (!source) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
|
|
1113
|
+
// This class exists precisely because the bounded automatic resume ran
|
|
1114
|
+
// out. Without that durable fact there is no authorization here.
|
|
1115
|
+
if (!providerResumeExhaustedWithin(db, input.sourceEffectId)) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_NOT_EXHAUSTED');
|
|
1116
|
+
const originKey = input.originKey;
|
|
1117
|
+
const effect = this.reserveEffectWithin(db, { taskId: input.taskId, effectId: input.effectId, kind: 'recovery', originKey, prompt: input.prompt });
|
|
1118
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
1119
|
+
input.taskId, `assistant-recovery-reserved:${originKey}`, 'assistant_recovery_reserved', input.sourceEffectId,
|
|
1120
|
+
json({
|
|
1121
|
+
recovery_effect_id: effect.effectId, requested_by: 'user', authorized_by: input.authorizedBy,
|
|
1122
|
+
request_id: input.requestId, reason: input.reason, exactly_once_resume: true, operator_authorized: true,
|
|
1123
|
+
}), now()));
|
|
1124
|
+
return effect;
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
freshConversationMigrationContext(taskId: string): { fromConversationId: string; requestId: string } | undefined {
|
|
1128
|
+
return this.read((db) => {
|
|
1129
|
+
const row = statement(db, "SELECT payload_json FROM events WHERE task_id = ? AND kind = 'conversation_migrated' ORDER BY event_id DESC LIMIT 1", (s) => s.get(taskId)) as { payload_json?: string } | undefined;
|
|
1130
|
+
const payload = parsedObject(row?.payload_json);
|
|
1131
|
+
if (payload.fresh_conversation !== true || payload.to_conversation_id !== `bootstrap:${taskId}`) return undefined;
|
|
1132
|
+
const fromConversationId = typeof payload.from_conversation_id === 'string' ? payload.from_conversation_id.trim() : '';
|
|
1133
|
+
const requestId = typeof payload.request_id === 'string' ? payload.request_id.trim() : '';
|
|
1134
|
+
if (!fromConversationId || !requestId) return undefined;
|
|
1135
|
+
return { fromConversationId, requestId };
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Explicit operator switch of an exhausted task to a *fresh* conversation in
|
|
1141
|
+
* its configured Project. This is the one path that legitimately opens a new
|
|
1142
|
+
* chat: it exists for an exact conversation that is durably unusable, never as
|
|
1143
|
+
* a reaction to provider backpressure. The whole move and its replacement
|
|
1144
|
+
* enrollment turn commit in one transaction, so the task can never be left on
|
|
1145
|
+
* a fresh conversation holding a prompt that assumes prior context.
|
|
1146
|
+
*/
|
|
1147
|
+
migrateToFreshConversation(input: {
|
|
1148
|
+
taskId: string;
|
|
1149
|
+
expectedConversationId: string;
|
|
1150
|
+
requestId: string;
|
|
1151
|
+
reason: string;
|
|
1152
|
+
authorizedBy: string;
|
|
1153
|
+
replacement: { effectId: string; prompt: string };
|
|
1154
|
+
}): { task: WorkflowSupervisorTask; migrated: boolean; retiredEffectId?: string } {
|
|
1155
|
+
return this.transaction((db) => {
|
|
1156
|
+
const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
1157
|
+
if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
1158
|
+
const task = taskFromRow(row);
|
|
1159
|
+
if (task.conversationId !== input.expectedConversationId) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_STALE');
|
|
1160
|
+
if (statement(db, "SELECT 1 FROM events WHERE task_id=? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') LIMIT 1", (s) => s.get(input.taskId))) {
|
|
1161
|
+
throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
|
|
1162
|
+
}
|
|
1163
|
+
const targetConversationId = `bootstrap:${input.taskId}`;
|
|
1164
|
+
if (task.conversationId === targetConversationId) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_ALREADY_PENDING');
|
|
1165
|
+
// Bound repeated unsuccessful moves, not the lifetime of a healthy task.
|
|
1166
|
+
// A genuine completion reopens the budget for its next 15-turn rotation.
|
|
1167
|
+
const prior = statement(db, `SELECT COUNT(*) AS total FROM events
|
|
1168
|
+
WHERE task_id = ? AND kind = 'conversation_migrated'
|
|
1169
|
+
AND event_id > COALESCE(
|
|
1170
|
+
(SELECT MAX(event_id) FROM events WHERE task_id = ? AND kind = 'assistant_completion'), 0
|
|
1171
|
+
)`, (s) => s.get(input.taskId, input.taskId)) as { total?: number } | undefined;
|
|
1172
|
+
if (Number(prior?.total ?? 0) >= WORKFLOW_SUPERVISOR_MAX_CONVERSATION_MIGRATIONS) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_BUDGET_EXHAUSTED');
|
|
1173
|
+
// A submitted mutation whose outcome is unknown must be reconciled, never
|
|
1174
|
+
// abandoned by moving the task elsewhere.
|
|
1175
|
+
const pending = oldestUnappliedEffect(db, input.taskId);
|
|
1176
|
+
if (pending) {
|
|
1177
|
+
const ledger = effectDispatchLedger(db, pending.effectId);
|
|
1178
|
+
const negativeProof = latestNotAppliedProofEventId(db, pending.effectId) > ledger.lastEventId;
|
|
1179
|
+
if (ledger.generations > 0 && !negativeProof) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_OUTCOME_UNKNOWN');
|
|
1180
|
+
}
|
|
1181
|
+
const originKey = `fresh-conversation:${input.taskId}:${input.requestId}`;
|
|
1182
|
+
const replacement = this.reserveEffectWithin(db, {
|
|
1183
|
+
taskId: input.taskId, effectId: input.replacement.effectId, kind: 'enrollment', originKey, prompt: input.replacement.prompt,
|
|
1184
|
+
});
|
|
1185
|
+
if (pending && pending.effectId !== replacement.effectId) {
|
|
1186
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
1187
|
+
input.taskId, `effect-retired-fresh-conversation:${pending.effectId}:${input.requestId}`, 'effect_outcome_unknown_superseded',
|
|
1188
|
+
pending.effectId,
|
|
1189
|
+
json({
|
|
1190
|
+
request_id: input.requestId, authorized_by: input.authorizedBy, reason: input.reason,
|
|
1191
|
+
generation: 0, replacement_effect_id: replacement.effectId, attestation: 'never_dispatched_fresh_conversation',
|
|
1192
|
+
}), now()));
|
|
1193
|
+
}
|
|
1194
|
+
statement(db, 'UPDATE tasks SET conversation_id = ?, conversation_url = ? WHERE task_id = ?', (s) => s.run(targetConversationId, 'https://chatgpt.com/', input.taskId));
|
|
1195
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
|
|
1196
|
+
input.taskId, `conversation-migrated:${input.taskId}:${input.requestId}`, 'conversation_migrated',
|
|
1197
|
+
json({
|
|
1198
|
+
from_conversation_id: task.conversationId, to_conversation_id: targetConversationId, to_conversation_url: 'https://chatgpt.com/',
|
|
1199
|
+
fresh_conversation: true, replacement_effect_id: replacement.effectId,
|
|
1200
|
+
request_id: input.requestId, authorized_by: input.authorizedBy, reason: input.reason,
|
|
1201
|
+
...(pending ? { retired_effect_id: pending.effectId } : {}),
|
|
1202
|
+
}), now()));
|
|
1203
|
+
const updated = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
1204
|
+
if (!updated) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
|
|
1205
|
+
return {
|
|
1206
|
+
task: taskFromRow(updated), migrated: true,
|
|
1207
|
+
...(pending && pending.effectId !== replacement.effectId ? { retiredEffectId: pending.effectId } : {}),
|
|
1208
|
+
};
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
migrateConversation(input: { taskId: string; expectedConversationId: string; conversationId: string; conversationUrl: string; requestId: string; reason: string; authorizedBy: string }): { task: WorkflowSupervisorTask; migrated: boolean } {
|
|
1213
|
+
return this.transaction((db) => {
|
|
1214
|
+
const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
1215
|
+
if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
1216
|
+
const task = taskFromRow(row);
|
|
1217
|
+
if (task.conversationId !== input.expectedConversationId) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_STALE');
|
|
1218
|
+
if (statement(db, "SELECT 1 FROM events WHERE task_id=? AND kind IN ('terminal_done','terminal_needs_user','terminal_stopped') LIMIT 1", (s) => s.get(input.taskId))) {
|
|
1219
|
+
throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
|
|
1220
|
+
}
|
|
1221
|
+
if (task.conversationId === input.conversationId) {
|
|
1222
|
+
if (task.conversationUrl !== input.conversationUrl) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_CONVERSATION_CONFLICT');
|
|
1223
|
+
return { task, migrated: false };
|
|
1224
|
+
}
|
|
1225
|
+
const conflict = statement(db, 'SELECT task_id FROM tasks WHERE conversation_id = ? AND task_id <> ?', (s) => s.get(input.conversationId, input.taskId)) as { task_id?: string } | undefined;
|
|
1226
|
+
if (conflict) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_CONVERSATION_CONFLICT');
|
|
1227
|
+
// Bound repeated unsuccessful moves, not the lifetime of a healthy task.
|
|
1228
|
+
// A genuine completion reopens the budget for its next 15-turn rotation.
|
|
1229
|
+
const prior = statement(db, `SELECT COUNT(*) AS total FROM events
|
|
1230
|
+
WHERE task_id = ? AND kind = 'conversation_migrated'
|
|
1231
|
+
AND event_id > COALESCE(
|
|
1232
|
+
(SELECT MAX(event_id) FROM events WHERE task_id = ? AND kind = 'assistant_completion'), 0
|
|
1233
|
+
)`, (s) => s.get(input.taskId, input.taskId)) as { total?: number } | undefined;
|
|
1234
|
+
if (Number(prior?.total ?? 0) >= WORKFLOW_SUPERVISOR_MAX_CONVERSATION_MIGRATIONS) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_BUDGET_EXHAUSTED');
|
|
1235
|
+
const pending = oldestUnappliedEffect(db, input.taskId);
|
|
1236
|
+
if (pending) {
|
|
1237
|
+
const ledger = effectDispatchLedger(db, pending.effectId);
|
|
1238
|
+
const negativeProof = latestNotAppliedProofEventId(db, pending.effectId) > ledger.lastEventId;
|
|
1239
|
+
if (ledger.generations > 0 && !negativeProof) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_OUTCOME_UNKNOWN');
|
|
1240
|
+
}
|
|
1241
|
+
statement(db, 'UPDATE tasks SET conversation_id = ?, conversation_url = ? WHERE task_id = ?', (s) => s.run(input.conversationId, input.conversationUrl, input.taskId));
|
|
1242
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
|
|
1243
|
+
input.taskId, `conversation-migrated:${input.taskId}:${input.requestId}`, 'conversation_migrated',
|
|
1244
|
+
json({
|
|
1245
|
+
from_conversation_id: task.conversationId, to_conversation_id: input.conversationId, to_conversation_url: input.conversationUrl,
|
|
1246
|
+
request_id: input.requestId, authorized_by: input.authorizedBy, reason: input.reason,
|
|
1247
|
+
...(pending ? { pending_effect_id: pending.effectId } : {}),
|
|
1248
|
+
}), now()));
|
|
1249
|
+
const updated = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
1250
|
+
if (!updated) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
|
|
1251
|
+
return { task: taskFromRow(updated), migrated: true };
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
reconcileNativePreSendFailure(effectId: string, requestId: string): boolean {
|
|
1255
|
+
// Bounded compatibility repair for the pre-20261002 native dispatcher.
|
|
1256
|
+
// These exact returned reasons occur before Send is clicked. Other unknown
|
|
1257
|
+
// observations, including transport exceptions, are never negative proof.
|
|
1258
|
+
const row = this.read((db) => {
|
|
1259
|
+
const dispatch = effectDispatchLedger(db, effectId);
|
|
1260
|
+
const owner = statement(db, "SELECT payload_json FROM events WHERE event_id=? AND kind='effect_dispatch_started'", (s) => s.get(dispatch.lastEventId)) as { payload_json?: string } | undefined;
|
|
1261
|
+
if (parsedObject(owner?.payload_json).surface !== 'macos-native') return undefined;
|
|
1262
|
+
// Other observers can report a transport error while the native sender
|
|
1263
|
+
// is still preparing. Only that generation's dispatch owner can prove
|
|
1264
|
+
// it returned before clicking Send.
|
|
1265
|
+
return statement(db, "SELECT payload_json FROM events WHERE effect_id=? AND kind='effect_unknown' AND event_id>? AND json_extract(payload_json,'$.surface')='macos-native' ORDER BY event_id LIMIT 1", (s) => s.get(effectId, dispatch.lastEventId)) as { payload_json?: string } | undefined;
|
|
1266
|
+
});
|
|
1267
|
+
const evidence = parsedObject(row?.payload_json);
|
|
1268
|
+
const beforeSend = new Set(['composer_missing', 'composer_not_empty', 'composer_selection_unavailable', 'composer_text_insertion_rejected', 'composer_text_unconfirmed', 'composer_submit_mismatch', 'send_button_missing']);
|
|
1269
|
+
if (evidence.surface !== 'macos-native' || evidence.reconciliation === true || !beforeSend.has(String(evidence.reason))) return false;
|
|
1270
|
+
this.recordEffectNotAppliedProof(effectId, `operator-pre-send:${requestId}`, { ...evidence, send_clicked: false, requested_by: 'user', request_id: requestId, attestation: 'persisted_native_pre_send_return' });
|
|
1271
|
+
return true;
|
|
1272
|
+
}
|
|
1273
|
+
private reserveEffectWithin(db: Database, input: { taskId: string; effectId: string; kind: WorkflowEffectKind; originKey: string; sourceCompletionFingerprint?: string; prompt: string }): WorkflowSupervisorEffect {
|
|
1274
|
+
statement(db, 'INSERT OR IGNORE INTO effects(effect_id,task_id,kind,origin_key,source_completion_fingerprint,prompt_text,created_at) VALUES (?,?,?,?,?,?,?)', (s) => s.run(input.effectId, input.taskId, input.kind, input.originKey, input.sourceCompletionFingerprint ?? null, input.prompt, now()));
|
|
1275
|
+
const row = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(input.originKey)) as Record<string, unknown> | undefined;
|
|
1276
|
+
if (!row) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_RESERVE_FAILED');
|
|
1277
|
+
const effect = effectFromRow(row);
|
|
1278
|
+
if (effect.effectId !== input.effectId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_ID_CONFLICT:${input.originKey}:${effect.effectId}:${input.effectId}`);
|
|
1279
|
+
if (effect.taskId !== input.taskId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT:${input.originKey}`);
|
|
1280
|
+
if (effect.kind !== input.kind) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_KIND_CONFLICT:${input.originKey}`);
|
|
1281
|
+
if ((effect.sourceCompletionFingerprint ?? '') !== (input.sourceCompletionFingerprint ?? '')) {
|
|
1282
|
+
throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_SOURCE_CONFLICT:${input.originKey}`);
|
|
1283
|
+
}
|
|
1284
|
+
if (effect.prompt !== input.prompt) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_PROMPT_CONFLICT:${input.originKey}`);
|
|
1285
|
+
return effect;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/** Attest that no Send intent was reserved and the provider UI could not prepare. */
|
|
1289
|
+
recordEffectPreparationFailure(effectId: string, reasonCode: string): void {
|
|
1290
|
+
this.transaction((db) => {
|
|
1291
|
+
const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
|
|
1292
|
+
if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
1293
|
+
const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId));
|
|
1294
|
+
const dispatch = effectDispatchLedger(db, effectId);
|
|
1295
|
+
if (applied || (dispatch.generations > 0 && latestNotAppliedProofEventId(db, effectId) <= dispatch.lastEventId)) {
|
|
1296
|
+
throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_FAILURE_OUTCOME_NOT_PROVEN');
|
|
1297
|
+
}
|
|
1298
|
+
const activeReleaseId = this.activeReleaseId()?.trim() || 'unidentified-runtime';
|
|
1299
|
+
const key = `effect-preflight-rejected:${effectId}:${activeReleaseId}`;
|
|
1300
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
1301
|
+
effect.task_id!, key, 'effect_preflight_rejected', effectId,
|
|
1302
|
+
json({ active_release_id: activeReleaseId, reason: (boundedText(reasonCode) ?? 'COMPUTER_PREPARE_REJECTED').slice(0, 256), send_clicked: false, attestation: 'computer_prepare_returned_before_supervisor_send_intent' }), now(),
|
|
1303
|
+
));
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
recordEffectDispatchStarted(effectId: string, generation: number, dispatchId: string, evidence: Record<string, unknown> = {}): boolean {
|
|
1308
|
+
if (!Number.isInteger(generation) || generation < 1 || generation > 1_000_000) throw new Error('WORKFLOW_SUPERVISOR_DISPATCH_GENERATION_INVALID');
|
|
1309
|
+
return this.transaction((db) => {
|
|
1310
|
+
const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
|
|
1311
|
+
if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
1312
|
+
const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId));
|
|
1313
|
+
if (applied) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_ALREADY_APPLIED');
|
|
1314
|
+
const refunds = statement(db, "SELECT event_id FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_budget_refunded'", (s) => s.all(effectId)) as Array<{ event_id?: number }>;
|
|
1315
|
+
if (generation > WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + refunds.length || this.preflightBlocked(db, effectId)) return false;
|
|
1316
|
+
const prior = statement(db, "SELECT event_id,payload_json FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_started' ORDER BY event_id DESC LIMIT 1", (s) => s.get(effectId)) as { event_id?: number; payload_json?: string } | undefined;
|
|
1317
|
+
const currentGeneration = prior?.event_id ? storedGeneration(prior.payload_json) : 0;
|
|
1318
|
+
const retryAuthorized = !prior?.event_id || latestNotAppliedProofEventId(db, effectId) > Number(prior.event_id);
|
|
1319
|
+
if (!retryAuthorized || generation !== currentGeneration + 1) return false;
|
|
1320
|
+
// `dispatched_at_ms` is the mechanical retry-spacing fact. It is recorded
|
|
1321
|
+
// inside the evidence payload so retry spacing stays deterministic under
|
|
1322
|
+
// an injected clock without changing durable event timestamps.
|
|
1323
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(effect.task_id, `effect-dispatch:${effectId}:${generation}`, 'effect_dispatch_started', effectId, json({ dispatchId, generation, dispatched_at_ms: this.clockMs(), ...evidence }), now()));
|
|
1324
|
+
return true;
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
recordEffectObservation(effectId: string, observationId: string, outcome: WorkflowEffectOutcome, evidence: Record<string, unknown> = {}): void {
|
|
1329
|
+
if (outcome === 'not_applied') throw new Error('WORKFLOW_SUPERVISOR_NOT_APPLIED_PROOF_REQUIRED');
|
|
1330
|
+
this.transaction((db) => {
|
|
1331
|
+
const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
|
|
1332
|
+
if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
1333
|
+
const key = `effect-observation:${effectId}:${observationId}`;
|
|
1334
|
+
const kind = `effect_${outcome}`;
|
|
1335
|
+
const existing = statement(db, 'SELECT kind,payload_json FROM events WHERE event_key = ?', (s) => s.get(key)) as { kind?: string; payload_json?: string } | undefined;
|
|
1336
|
+
const payload = json(evidence);
|
|
1337
|
+
if (existing && (existing.kind !== kind || existing.payload_json !== payload)) throw new Error('WORKFLOW_SUPERVISOR_OBSERVATION_ID_CONFLICT');
|
|
1338
|
+
// Identical unknown observations remain append-only evidence at the capped
|
|
1339
|
+
// reconciliation cadence. Permanently suppressing them freezes the last
|
|
1340
|
+
// observation timestamp and makes a transient outcome-unknown state unable
|
|
1341
|
+
// to discover late provider evidence after Runtime/browser recovery.
|
|
1342
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(effect.task_id, key, kind, effectId, payload, now()));
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
recordEffectNotAppliedProof(effectId: string, observationId: string, proof: Record<string, unknown>): void {
|
|
1347
|
+
this.transaction((db) => {
|
|
1348
|
+
const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
|
|
1349
|
+
if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
1350
|
+
const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId));
|
|
1351
|
+
if (applied) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_ALREADY_APPLIED');
|
|
1352
|
+
const key = `effect-observation:${effectId}:${observationId}`;
|
|
1353
|
+
const payload = json(proof);
|
|
1354
|
+
const existing = statement(db, 'SELECT kind,payload_json FROM events WHERE event_key = ?', (s) => s.get(key)) as { kind?: string; payload_json?: string } | undefined;
|
|
1355
|
+
if (existing && (existing.kind !== 'effect_not_applied' || existing.payload_json !== payload)) throw new Error('WORKFLOW_SUPERVISOR_OBSERVATION_ID_CONFLICT');
|
|
1356
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(effect.task_id, key, 'effect_not_applied', effectId, payload, now()));
|
|
1357
|
+
const ledger = effectDispatchLedger(db, effectId);
|
|
1358
|
+
if (proof.pre_send_rejection === true
|
|
1359
|
+
&& ledger.generations >= WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS
|
|
1360
|
+
&& ledger.budgetRefunds === 0) {
|
|
1361
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(effect.task_id, `effect-dispatch-budget-refund:${effectId}`, 'effect_dispatch_budget_refunded', effectId, json({ reason: 'pre_send_rejection_did_not_reach_provider' }), now()));
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
commitCompletion(input: WorkflowSupervisorCompletion, successor?: { effectId: string; kind: WorkflowEffectKind; prompt: string; freshEnrollmentPrompt?: string }): { completion: WorkflowSupervisorCompletion; successorEffect?: WorkflowSupervisorEffect; deduplicated: boolean } {
|
|
1367
|
+
return this.transaction((db) => {
|
|
1368
|
+
const task = statement(db, 'SELECT 1 AS ok FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId));
|
|
1369
|
+
if (!task) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
1370
|
+
const sourceEffect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(input.sourceEffectId)) as { task_id?: string } | undefined;
|
|
1371
|
+
if (sourceEffect?.task_id !== input.taskId) throw new Error('WORKFLOW_SUPERVISOR_SOURCE_EFFECT_MISMATCH');
|
|
1372
|
+
const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(input.sourceEffectId));
|
|
1373
|
+
if (!applied) throw new Error('WORKFLOW_SUPERVISOR_SOURCE_EFFECT_NOT_APPLIED');
|
|
1374
|
+
const priorForEffect = statement(db, 'SELECT completion_fingerprint FROM completions WHERE task_id = ? AND source_effect_id = ?', (s) => s.get(input.taskId, input.sourceEffectId)) as { completion_fingerprint?: string } | undefined;
|
|
1375
|
+
if (priorForEffect?.completion_fingerprint && priorForEffect.completion_fingerprint !== input.completionFingerprint) {
|
|
1376
|
+
throw new Error('WORKFLOW_SUPERVISOR_SOURCE_EFFECT_COMPLETION_CONFLICT');
|
|
1377
|
+
}
|
|
1378
|
+
const existing = statement(db, 'SELECT completion_fingerprint FROM completions WHERE completion_fingerprint = ?', (s) => s.get(input.completionFingerprint));
|
|
1379
|
+
if (!existing) statement(db, 'INSERT INTO completions VALUES (?,?,?,?,?,?,?,?)', (s) => s.run(input.completionFingerprint, input.taskId, input.sourceEffectId, input.action, input.responseSha256, input.controlBlockSha256, json(input.proposal), input.committedAt));
|
|
1380
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,completion_fingerprint,payload_json,occurred_at) VALUES (?,?,?,?,?,?,?)', (s) => s.run(input.taskId, `completion:${input.completionFingerprint}`, 'assistant_completion', input.sourceEffectId, input.completionFingerprint, json(input.proposal), input.committedAt));
|
|
1381
|
+
let successorEffect: WorkflowSupervisorEffect | undefined;
|
|
1382
|
+
if (successor) {
|
|
1383
|
+
const originKey = `completion:${input.completionFingerprint}`;
|
|
1384
|
+
const priorSuccessorRow = existing
|
|
1385
|
+
? statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(originKey)) as Record<string, unknown> | undefined
|
|
1386
|
+
: undefined;
|
|
1387
|
+
if (priorSuccessorRow) {
|
|
1388
|
+
const priorSuccessor = effectFromRow(priorSuccessorRow);
|
|
1389
|
+
if (priorSuccessor.taskId !== input.taskId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT:${originKey}`);
|
|
1390
|
+
if (priorSuccessor.kind !== successor.kind && !(successor.freshEnrollmentPrompt && priorSuccessor.kind === 'enrollment')) {
|
|
1391
|
+
throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_KIND_CONFLICT:${originKey}`);
|
|
1392
|
+
}
|
|
1393
|
+
if ((priorSuccessor.sourceCompletionFingerprint ?? '') !== input.completionFingerprint) {
|
|
1394
|
+
throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_SOURCE_CONFLICT:${originKey}`);
|
|
1395
|
+
}
|
|
1396
|
+
successorEffect = priorSuccessor;
|
|
1397
|
+
} else {
|
|
1398
|
+
// User-authorized periodic rotation is a mechanical cadence over
|
|
1399
|
+
// completed CONTINUE receipts, never sends, elapsed time or Work state.
|
|
1400
|
+
// The completion, fresh enrollment, binding change and migration
|
|
1401
|
+
// evidence commit atomically in the existing Supervisor journal.
|
|
1402
|
+
const current = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown>;
|
|
1403
|
+
const currentTask = taskFromRow(current);
|
|
1404
|
+
const lastMigration = statement(db, "SELECT MAX(event_id) AS id FROM events WHERE task_id=? AND kind='conversation_migrated'", (s) => s.get(input.taskId)) as { id?: number };
|
|
1405
|
+
const completedSinceMigration = statement(db, `SELECT COUNT(*) AS total FROM events e
|
|
1406
|
+
JOIN completions c ON c.completion_fingerprint=e.completion_fingerprint
|
|
1407
|
+
WHERE e.task_id=? AND e.kind='assistant_completion' AND e.event_id>? AND c.action='CONTINUE'`,
|
|
1408
|
+
(s) => s.get(input.taskId, lastMigration?.id ?? 0)) as { total?: number };
|
|
1409
|
+
const rotate = successor.freshEnrollmentPrompt !== undefined
|
|
1410
|
+
&& currentTask.continuationPolicy.kind === 'standalone_supervisor'
|
|
1411
|
+
&& !currentTask.conversationId.startsWith('bootstrap:')
|
|
1412
|
+
&& (completedSinceMigration.total ?? 0) >= 15;
|
|
1413
|
+
successorEffect = this.reserveEffectWithin(db, {
|
|
1414
|
+
taskId: input.taskId, effectId: successor.effectId,
|
|
1415
|
+
kind: rotate ? 'enrollment' : successor.kind, originKey,
|
|
1416
|
+
sourceCompletionFingerprint: input.completionFingerprint,
|
|
1417
|
+
prompt: rotate ? successor.freshEnrollmentPrompt! : successor.prompt,
|
|
1418
|
+
});
|
|
1419
|
+
if (rotate) {
|
|
1420
|
+
const sourceConversationId = currentTask.conversationId;
|
|
1421
|
+
const bootstrapConversationId = `bootstrap:${input.taskId}`;
|
|
1422
|
+
statement(db, 'UPDATE tasks SET conversation_id=?, conversation_url=? WHERE task_id=?',
|
|
1423
|
+
(s) => s.run(bootstrapConversationId, 'https://chatgpt.com/', input.taskId));
|
|
1424
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
|
|
1425
|
+
input.taskId, `conversation-migrated:auto:${input.completionFingerprint}`, 'conversation_migrated',
|
|
1426
|
+
json({ from_conversation_id: sourceConversationId, to_conversation_id: bootstrapConversationId,
|
|
1427
|
+
to_conversation_url: 'https://chatgpt.com/', fresh_conversation: true,
|
|
1428
|
+
replacement_effect_id: successorEffect!.effectId, request_id: `automatic-receipt:${input.completionFingerprint}`,
|
|
1429
|
+
authorized_by: 'user-configured-standalone-rotation', reason: '15_confirmed_continue_receipts' }), now()));
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return { completion: input, ...(successorEffect ? { successorEffect } : {}), deduplicated: Boolean(existing) };
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
resolveTerminal(input: { completionFingerprint: string; taskId: string; action: 'DONE' | 'NEEDS_USER'; accepted: boolean; reason: string; correction?: { effectId: string; prompt: string } }): { successorEffect?: WorkflowSupervisorEffect; deduplicated: boolean } {
|
|
1438
|
+
return this.transaction((db) => {
|
|
1439
|
+
const key = `terminal-resolution:${input.completionFingerprint}`;
|
|
1440
|
+
const existing = statement(db, 'SELECT kind,payload_json FROM events WHERE event_key = ?', (s) => s.get(key)) as { kind?: string; payload_json?: string } | undefined;
|
|
1441
|
+
if (existing) {
|
|
1442
|
+
const effect = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(`completion:${input.completionFingerprint}`));
|
|
1443
|
+
return { ...(effect ? { successorEffect: effectFromRow(effect as Record<string, unknown>) } : {}), deduplicated: true };
|
|
1444
|
+
}
|
|
1445
|
+
const kind = input.accepted ? (input.action === 'DONE' ? 'terminal_done' : 'terminal_needs_user') : 'terminal_rejected';
|
|
1446
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,completion_fingerprint,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(input.taskId, key, kind, input.completionFingerprint, json({ reason: input.reason }), now()));
|
|
1447
|
+
const successorEffect = !input.accepted && input.correction ? this.reserveEffectWithin(db, { taskId: input.taskId, effectId: input.correction.effectId, kind: 'correction', originKey: `completion:${input.completionFingerprint}`, sourceCompletionFingerprint: input.completionFingerprint, prompt: input.correction.prompt }) : undefined;
|
|
1448
|
+
return { ...(successorEffect ? { successorEffect } : {}), deduplicated: false };
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
}
|