@moretea-labs/forge 1.8.1 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -2
- package/CHANGELOG.md +20 -0
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
- package/adapters/chatgpt/provider-delivery.ts +1 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
- package/adapters/computer/chatgpt-extension-target.ts +295 -50
- package/adapters/computer/chrome-extension/background.js +227 -71
- package/adapters/computer/chrome-extension/content.js +181 -157
- package/adapters/computer/chrome-extension/manifest.json +3 -1
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +39 -9
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -6
- package/adapters/mcp/instructions.ts +2 -2
- package/adapters/mcp/multi-repository.ts +7 -28
- package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
- package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
- package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
- package/adapters/mcp/runtime-gateway/router.ts +3 -5
- package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
- package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
- package/adapters/mcp/server.ts +24 -3
- package/adapters/mcp/setup.ts +4 -10
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
- package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
- package/adapters/mcp/toolset-names.ts +11 -42
- package/adapters/mcp/toolset.ts +16 -90
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +18 -6
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +54 -14
- package/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +1 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +114 -30
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +5 -5
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +14 -2
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -54
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +24 -12
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +1 -12
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
- package/packages/kernel/scheduler/application/eligibility.ts +4 -1
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
- package/packages/kernel/work/application/work-service.ts +1 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +9 -2
- package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
- package/scripts/assemble-template.ts +11 -72
- package/scripts/check-mcp-compatibility.ts +55 -74
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +305 -28
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +3 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +0 -1
- package/scripts/run-governed-gate.ts +2 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +7 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
- package/scripts/sync-generated-authority.mjs +0 -1
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/agent-jobs/integration.ts +2 -2
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- package/src/cli/commands/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +2 -14
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/setup-execution.ts +41 -0
- package/src/cli/controller/completion-orchestrator.ts +1 -1
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +2 -2
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +97 -76
- package/src/cli/controller/operational-plan.ts +1 -1
- package/src/cli/controller/systemd-user.ts +4 -10
- package/src/cli/editing/edit-session.ts +17 -5
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +1 -1
- package/src/cli/local-bridge/console-view-models.ts +1 -1
- package/src/cli/local-bridge/facade-api.ts +0 -2
- package/src/cli/repositories/command-classifier.ts +0 -9
- package/src/cli/repositories/command-scope.ts +2 -2
- package/src/cli/repositories/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +2 -1
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +39 -3
- package/src/cli/repository/inspector.ts +185 -343
- package/src/effects/process-runner.ts +17 -1
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +7 -0
- package/src/runtime/context/semantic-navigation.ts +1 -1
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
- package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
- package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
- package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
- package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
- package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
- package/src/runtime/control-plane/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
- package/src/runtime/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
- package/src/runtime/execution/process-runtime/runtime.ts +20 -16
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +1 -2
- package/src/runtime/maintenance/cleanup.ts +41 -5
- package/src/runtime/plugins/action-application.ts +12 -1
- package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
- package/src/runtime/plugins/browser-adapter.ts +381 -313
- package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
- package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
- package/src/runtime/plugins/browser-registration.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +26 -35
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
- package/src/runtime/plugins/computer-registration.ts +76 -9
- package/src/runtime/plugins/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +166 -1
- package/src/runtime/plugins/google-shared.ts +10 -5
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +45 -40
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/local-system-adapter.ts +1 -99
- package/src/runtime/plugins/store.ts +80 -7
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/recovery/actions.ts +1 -1
- package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/release/release-coordinator.ts +20 -4
- package/src/runtime/root/controller-progression-composition.ts +67 -5
- package/src/runtime/root/package-connector-service.ts +123 -11
- package/src/runtime/root/package-runtime-release.ts +6 -18
- package/src/runtime/root/package-runtime-service.ts +7 -13
- package/src/runtime/root/release-execution-canary.ts +18 -5
- package/src/runtime/root/release-manifest.ts +0 -34
- package/src/runtime/root/release-materialize.ts +0 -78
- package/src/runtime/root/runtime.ts +1 -2
- package/src/runtime/root/scheduled-controller-composition.ts +2 -1
- package/src/runtime/root/service.ts +90 -11
- package/src/runtime/root/types.ts +0 -6
- package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
- package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
- package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/json-files.ts +8 -3
- package/src/runtime/standalone-recovery/core.ts +189 -174
- package/src/runtime/standalone-recovery/entry.ts +62 -30
- package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
- package/src/runtime/workflow/schedules/engine.ts +36 -14
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
- package/supervisor/client.ts +31 -14
- package/supervisor/control-plane.ts +210 -115
- package/supervisor/forge-validators.ts +88 -21
- package/supervisor/native-browser-adapter.ts +393 -28
- package/supervisor/native-messaging/host.ts +21 -2
- package/supervisor/protocol.ts +74 -26
- package/supervisor/server.ts +16 -8
- package/supervisor/store.ts +476 -57
- package/supervisor/types.ts +19 -13
- package/SKILL.md +0 -427
- package/adapters/computer/chatgpt-target-router.ts +0 -103
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- package/scripts/check-skill-version.ts +0 -5
- package/scripts/ensure-codegraph.sh +0 -16
- package/scripts/run-skill-hook.ts +0 -244
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- package/src/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
package/supervisor/store.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { mkdirSync } from 'node:fs';
|
|
4
4
|
import { resolveWorkflowSupervisorForgeHome, workflowSupervisorDatabasePathValue, workflowSupervisorRootPath } from './paths';
|
|
5
|
-
import
|
|
5
|
+
import { validateSupervisorObjective } from './protocol';
|
|
6
|
+
import type { WorkflowEffectKind, WorkflowEffectOutcome, WorkflowSupervisorCompletion, WorkflowSupervisorContinuationProof, WorkflowSupervisorDiscoverySnapshot, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTaskStall, WorkflowSupervisorTerminalState, WorkflowSupervisorPreflightComposerObservation } from './types';
|
|
6
7
|
export type { WorkflowSupervisorTaskStall } from './types';
|
|
7
8
|
|
|
8
9
|
interface Statement { get(...params: unknown[]): unknown; all(...params: unknown[]): unknown[]; run(...params: unknown[]): unknown; finalize?(): void }
|
|
@@ -53,6 +54,10 @@ function json(value: unknown): string {
|
|
|
53
54
|
return JSON.stringify(value);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
export function workflowSupervisorCompletionContractSha256(contract: Record<string, unknown>): string {
|
|
58
|
+
return createHash('sha256').update(json(contract)).digest('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
export { resolveWorkflowSupervisorForgeHome } from './paths';
|
|
57
62
|
export function workflowSupervisorRoot(forgeHome?: string): string {
|
|
58
63
|
const root = workflowSupervisorRootPath(forgeHome);
|
|
@@ -91,6 +96,10 @@ function openDatabase(forgeHome?: string): Database {
|
|
|
91
96
|
kind TEXT NOT NULL, effect_id TEXT, completion_fingerprint TEXT, payload_json TEXT NOT NULL, occurred_at TEXT NOT NULL
|
|
92
97
|
);
|
|
93
98
|
CREATE INDEX IF NOT EXISTS events_task_order ON events(task_id, event_id);
|
|
99
|
+
-- Terminal probes run for every task on the polling/reconciliation paths.
|
|
100
|
+
-- Index only terminal facts so an open task does not rescan its whole log.
|
|
101
|
+
CREATE INDEX IF NOT EXISTS events_terminal_task_order ON events(task_id, event_id)
|
|
102
|
+
WHERE kind IN ('terminal_done','terminal_needs_user','terminal_stopped');
|
|
94
103
|
CREATE INDEX IF NOT EXISTS events_effect_kind_order ON events(effect_id, kind, event_id);
|
|
95
104
|
CREATE TABLE IF NOT EXISTS discovered_conversations (
|
|
96
105
|
source TEXT NOT NULL, conversation_id TEXT NOT NULL, canonical_url TEXT NOT NULL, title TEXT,
|
|
@@ -143,6 +152,7 @@ function completionFromRow(row: Record<string, unknown>): WorkflowSupervisorComp
|
|
|
143
152
|
};
|
|
144
153
|
}
|
|
145
154
|
function parsedObject(value: unknown): Record<string, unknown> {
|
|
155
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>;
|
|
146
156
|
try { const parsed = JSON.parse(String(value)); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {}; }
|
|
147
157
|
catch { return {}; }
|
|
148
158
|
}
|
|
@@ -151,7 +161,12 @@ function storedGeneration(value: unknown): number {
|
|
|
151
161
|
return Number.isInteger(generation) && generation > 0 ? generation : 1;
|
|
152
162
|
}
|
|
153
163
|
function latestNotAppliedProofEventId(db: Database, effectId: string): number {
|
|
154
|
-
|
|
164
|
+
// Delayed negative evidence from an older generation must not authorize
|
|
165
|
+
// another Send while the latest generation is outcome-unknown. Apply this
|
|
166
|
+
// read-side fence to journals written by earlier releases as well.
|
|
167
|
+
const dispatch = 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;
|
|
168
|
+
if (!dispatch?.event_id) return 0;
|
|
169
|
+
const notApplied = statement(db, "SELECT event_id FROM events WHERE effect_id = ? AND kind = 'effect_not_applied' AND event_id > ? AND json_extract(payload_json,'$.dispatch_generation') = ? ORDER BY event_id DESC LIMIT 1", (s) => s.get(effectId, dispatch.event_id, storedGeneration(dispatch.payload_json))) as { event_id?: number } | undefined;
|
|
155
170
|
return Number(notApplied?.event_id ?? 0);
|
|
156
171
|
}
|
|
157
172
|
|
|
@@ -192,6 +207,15 @@ function oldestUnappliedEffect(db: Database, taskId: string): WorkflowSupervisor
|
|
|
192
207
|
SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied'
|
|
193
208
|
) AND NOT EXISTS (
|
|
194
209
|
SELECT 1 FROM events superseded WHERE superseded.effect_id = e.effect_id AND superseded.kind = 'effect_outcome_unknown_superseded'
|
|
210
|
+
) AND NOT (
|
|
211
|
+
e.kind = 'recovery' AND NOT EXISTS (
|
|
212
|
+
SELECT 1 FROM events started
|
|
213
|
+
WHERE started.effect_id = e.effect_id AND started.kind = 'effect_dispatch_started'
|
|
214
|
+
) AND EXISTS (
|
|
215
|
+
SELECT 1 FROM completions completed
|
|
216
|
+
WHERE completed.task_id = e.task_id
|
|
217
|
+
AND e.origin_key = 'provider-recovery:' || completed.source_effect_id
|
|
218
|
+
)
|
|
195
219
|
) ORDER BY e.created_at, e.effect_id LIMIT 1`, (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
196
220
|
return row ? effectFromRow(row) : undefined;
|
|
197
221
|
}
|
|
@@ -312,6 +336,12 @@ function unknownObservationDelayMs(count: number): number {
|
|
|
312
336
|
);
|
|
313
337
|
}
|
|
314
338
|
|
|
339
|
+
function unknownObservationDue(db: Database, effectId: string, afterEventId: number, nowMs: number): boolean {
|
|
340
|
+
const observation = effectUnknownObservationLedger(db, effectId, afterEventId);
|
|
341
|
+
return observation.sameFingerprintCount === 0
|
|
342
|
+
|| nowMs - observation.lastOccurredAtMs >= unknownObservationDelayMs(observation.sameFingerprintCount);
|
|
343
|
+
}
|
|
344
|
+
|
|
315
345
|
function effectDispatchLedger(db: Database, effectId: string): EffectDispatchLedger {
|
|
316
346
|
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
347
|
const last = rows[rows.length - 1];
|
|
@@ -330,9 +360,9 @@ function effectDispatchLedger(db: Database, effectId: string): EffectDispatchLed
|
|
|
330
360
|
};
|
|
331
361
|
}
|
|
332
362
|
/** 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'));
|
|
363
|
+
function effectPreflightFailureReleaseIds(db: Database, effectId: string): Array<{ eventId: number; releaseId: string }> {
|
|
364
|
+
const rows = statement(db, "SELECT event_id,payload_json FROM events WHERE effect_id = ? AND kind = 'effect_preflight_rejected' ORDER BY event_id", (s) => s.all(effectId)) as Array<{ event_id?: number; payload_json?: string }>;
|
|
365
|
+
return rows.map((row) => ({ eventId: Number(row.event_id ?? 0), releaseId: String(parsedObject(row.payload_json).active_release_id ?? 'unidentified-runtime') }));
|
|
336
366
|
}
|
|
337
367
|
|
|
338
368
|
export class WorkflowSupervisorStore {
|
|
@@ -350,8 +380,18 @@ export class WorkflowSupervisorStore {
|
|
|
350
380
|
|
|
351
381
|
private preflightBlocked(db: Database, effectId: string): boolean {
|
|
352
382
|
const tried = effectPreflightFailureReleaseIds(db, effectId);
|
|
353
|
-
|
|
354
|
-
|
|
383
|
+
const releaseId = this.activeReleaseId()?.trim() || 'unidentified-runtime';
|
|
384
|
+
const lastFailure = tried.filter((entry) => entry.releaseId === releaseId).at(-1);
|
|
385
|
+
// Three failed releases exhaust automatic re-preparation, not an
|
|
386
|
+
// operator's single explicit post-repair proof. The same durable event
|
|
387
|
+
// ledger still enforces one rearm at most and re-blocks after its failure.
|
|
388
|
+
const rearm = statement(db, "SELECT event_id FROM events WHERE effect_id = ? AND kind = 'effect_preflight_operator_rearmed' ORDER BY event_id DESC LIMIT 1", (s) => s.get(effectId)) as { event_id?: number } | undefined;
|
|
389
|
+
const rearmedEventId = Number(rearm?.event_id ?? 0);
|
|
390
|
+
if (tried.length >= WORKFLOW_SUPERVISOR_MAX_PREFLIGHT_RELEASE_ATTEMPTS) {
|
|
391
|
+
return rearmedEventId <= (tried.at(-1)?.eventId ?? 0);
|
|
392
|
+
}
|
|
393
|
+
if (!lastFailure) return false;
|
|
394
|
+
return rearmedEventId <= lastFailure.eventId;
|
|
355
395
|
}
|
|
356
396
|
|
|
357
397
|
close(): void {
|
|
@@ -423,6 +463,7 @@ export class WorkflowSupervisorStore {
|
|
|
423
463
|
}
|
|
424
464
|
|
|
425
465
|
registerTask(input: WorkflowSupervisorTaskInput): WorkflowSupervisorTask {
|
|
466
|
+
validateSupervisorObjective(input.objective);
|
|
426
467
|
return this.transaction((db) => {
|
|
427
468
|
const createdAt = now();
|
|
428
469
|
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));
|
|
@@ -468,6 +509,14 @@ export class WorkflowSupervisorStore {
|
|
|
468
509
|
&& task.conversationId === input.conversationId
|
|
469
510
|
&& task.conversationUrl === input.conversationUrl;
|
|
470
511
|
if (bootstrapUpgrade) {
|
|
512
|
+
const existingWorkId = boundedText(task.completionContract.work_id);
|
|
513
|
+
const existingRequirementId = boundedText(task.completionContract.requirement_id);
|
|
514
|
+
const incomingWorkId = boundedText(input.completionContract.work_id);
|
|
515
|
+
const incomingRequirementId = boundedText(input.completionContract.requirement_id);
|
|
516
|
+
if ((existingWorkId && incomingWorkId !== existingWorkId)
|
|
517
|
+
|| (existingRequirementId && incomingRequirementId !== existingRequirementId)) {
|
|
518
|
+
throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_CONFLICT');
|
|
519
|
+
}
|
|
471
520
|
statement(db, `UPDATE tasks
|
|
472
521
|
SET objective = ?, completion_contract_json = ?, continuation_policy_json = ?, user_blocker_policy_json = ?
|
|
473
522
|
WHERE task_id = ?`, (s) => s.run(
|
|
@@ -525,32 +574,62 @@ export class WorkflowSupervisorStore {
|
|
|
525
574
|
requestId: string;
|
|
526
575
|
authorizedBy: string;
|
|
527
576
|
refreshEffect?: { effectId: string; expectedPrompt: string; prompt: string };
|
|
528
|
-
|
|
577
|
+
semanticBinding?: { workId?: string; requirementId?: string; expectedCompletionContractSha256: string };
|
|
578
|
+
}): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; completionContractSha256: string; semanticBindingChanged: boolean; refreshedEffect?: WorkflowSupervisorEffect } {
|
|
529
579
|
return this.transaction((db) => {
|
|
530
|
-
const objective = input.objective
|
|
531
|
-
if (!objective || objective.length > 8_000) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_INVALID');
|
|
580
|
+
const objective = validateSupervisorObjective(input.objective);
|
|
532
581
|
if (!/^[0-9a-f]{64}$/i.test(input.expectedObjectiveSha256)) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_EXPECTED_SHA_INVALID');
|
|
533
582
|
if (!input.requestId.trim()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_REQUIRED');
|
|
534
583
|
const targetSha = objectiveSha256(objective);
|
|
584
|
+
const semanticBinding = input.semanticBinding;
|
|
585
|
+
const workId = boundedText(semanticBinding?.workId);
|
|
586
|
+
const requirementId = boundedText(semanticBinding?.requirementId);
|
|
587
|
+
if (semanticBinding) {
|
|
588
|
+
if (Boolean(workId) === Boolean(requirementId)) throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_TARGET_INVALID');
|
|
589
|
+
if (!/^[0-9a-f]{64}$/i.test(semanticBinding.expectedCompletionContractSha256)) {
|
|
590
|
+
throw new Error('WORKFLOW_SUPERVISOR_COMPLETION_CONTRACT_REVISION_EXPECTED_SHA_INVALID');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const semanticBindingKind = workId ? 'work' : requirementId ? 'requirement' : undefined;
|
|
594
|
+
const semanticBindingId = workId ?? requirementId;
|
|
535
595
|
const eventKey = `task-objective-revise:${input.taskId}:${input.requestId}`;
|
|
596
|
+
const bindingEventKey = `task-semantic-binding-revise:${input.taskId}:${input.requestId}`;
|
|
536
597
|
const existingEvent = statement(db, 'SELECT payload_json FROM events WHERE event_key = ?', (s) => s.get(eventKey)) as { payload_json?: string } | undefined;
|
|
598
|
+
const existingBindingEvent = statement(db, 'SELECT payload_json FROM events WHERE event_key = ?', (s) => s.get(bindingEventKey)) as { payload_json?: string } | undefined;
|
|
537
599
|
if (existingEvent) {
|
|
538
600
|
const payload = parsedObject(existingEvent.payload_json);
|
|
539
|
-
if (boundedText(payload.objective_sha256) !== targetSha
|
|
601
|
+
if (boundedText(payload.objective_sha256) !== targetSha
|
|
602
|
+
|| boundedText(payload.semantic_binding_id) !== semanticBindingId
|
|
603
|
+
|| boundedText(payload.semantic_binding_kind) !== semanticBindingKind
|
|
604
|
+
|| Boolean(existingBindingEvent) !== Boolean(semanticBinding)) {
|
|
605
|
+
throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_CONFLICT');
|
|
606
|
+
}
|
|
607
|
+
if (existingBindingEvent) {
|
|
608
|
+
const bindingPayload = parsedObject(existingBindingEvent.payload_json);
|
|
609
|
+
if (boundedText(bindingPayload.completion_contract_sha256) !== boundedText(payload.completion_contract_sha256)
|
|
610
|
+
|| boundedText(bindingPayload.semantic_binding_id) !== semanticBindingId
|
|
611
|
+
|| boundedText(bindingPayload.semantic_binding_kind) !== semanticBindingKind) {
|
|
612
|
+
throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_REVISION_REQUEST_CONFLICT');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
540
615
|
const taskRow = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
541
616
|
if (!taskRow) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
617
|
+
const deduplicatedTask = taskFromRow(taskRow);
|
|
542
618
|
const refreshedEffectId = boundedText(payload.refreshed_effect_id);
|
|
543
619
|
const refreshedRow = refreshedEffectId
|
|
544
620
|
? statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(refreshedEffectId)) as Record<string, unknown> | undefined
|
|
545
621
|
: undefined;
|
|
546
622
|
return {
|
|
547
|
-
task:
|
|
623
|
+
task: deduplicatedTask,
|
|
548
624
|
changed: payload.changed === true,
|
|
549
625
|
deduplicated: true,
|
|
550
626
|
objectiveSha256: targetSha,
|
|
627
|
+
completionContractSha256: workflowSupervisorCompletionContractSha256(deduplicatedTask.completionContract),
|
|
628
|
+
semanticBindingChanged: payload.semantic_binding_changed === true,
|
|
551
629
|
...(refreshedRow ? { refreshedEffect: effectFromRow(refreshedRow) } : {}),
|
|
552
630
|
};
|
|
553
631
|
}
|
|
632
|
+
if (existingBindingEvent) throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_REVISION_REQUEST_CONFLICT');
|
|
554
633
|
const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
555
634
|
if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
556
635
|
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;
|
|
@@ -559,6 +638,31 @@ export class WorkflowSupervisorStore {
|
|
|
559
638
|
const currentSha = objectiveSha256(task.objective);
|
|
560
639
|
if (currentSha !== input.expectedObjectiveSha256.toLowerCase()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_CONFLICT');
|
|
561
640
|
|
|
641
|
+
const currentContractSha = workflowSupervisorCompletionContractSha256(task.completionContract);
|
|
642
|
+
let nextCompletionContract = task.completionContract;
|
|
643
|
+
let completionContractSha = currentContractSha;
|
|
644
|
+
let semanticBindingChanged = false;
|
|
645
|
+
if (semanticBinding) {
|
|
646
|
+
if (task.completionContract.kind !== 'model_semantic_completion') {
|
|
647
|
+
throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_REVISION_KIND_UNSUPPORTED');
|
|
648
|
+
}
|
|
649
|
+
const currentWorkId = boundedText(task.completionContract.work_id);
|
|
650
|
+
const currentRequirementId = boundedText(task.completionContract.requirement_id);
|
|
651
|
+
if ((currentWorkId || currentRequirementId)
|
|
652
|
+
&& (currentWorkId !== workId || currentRequirementId !== requirementId)) {
|
|
653
|
+
throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_CONFLICT');
|
|
654
|
+
}
|
|
655
|
+
if (currentContractSha !== semanticBinding.expectedCompletionContractSha256.toLowerCase()) {
|
|
656
|
+
throw new Error('WORKFLOW_SUPERVISOR_COMPLETION_CONTRACT_REVISION_CONFLICT');
|
|
657
|
+
}
|
|
658
|
+
nextCompletionContract = {
|
|
659
|
+
...task.completionContract,
|
|
660
|
+
...(workId ? { work_id: workId } : { requirement_id: requirementId }),
|
|
661
|
+
};
|
|
662
|
+
completionContractSha = workflowSupervisorCompletionContractSha256(nextCompletionContract);
|
|
663
|
+
semanticBindingChanged = completionContractSha !== currentContractSha;
|
|
664
|
+
}
|
|
665
|
+
|
|
562
666
|
let refreshedEffect: WorkflowSupervisorEffect | undefined;
|
|
563
667
|
if (input.refreshEffect) {
|
|
564
668
|
const effectRow = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(input.refreshEffect!.effectId)) as Record<string, unknown> | undefined;
|
|
@@ -579,6 +683,26 @@ export class WorkflowSupervisorStore {
|
|
|
579
683
|
|
|
580
684
|
const changed = task.objective !== objective;
|
|
581
685
|
if (changed) statement(db, 'UPDATE tasks SET objective = ? WHERE task_id = ?', (s) => s.run(objective, input.taskId));
|
|
686
|
+
if (semanticBindingChanged) {
|
|
687
|
+
statement(db, 'UPDATE tasks SET completion_contract_json = ? WHERE task_id = ?', (s) => s.run(json(nextCompletionContract), input.taskId));
|
|
688
|
+
}
|
|
689
|
+
if (semanticBinding) {
|
|
690
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
|
|
691
|
+
input.taskId,
|
|
692
|
+
bindingEventKey,
|
|
693
|
+
'task_semantic_binding_revised',
|
|
694
|
+
json({
|
|
695
|
+
request_id: input.requestId,
|
|
696
|
+
authorized_by: input.authorizedBy,
|
|
697
|
+
semantic_binding_kind: semanticBindingKind,
|
|
698
|
+
semantic_binding_id: semanticBindingId,
|
|
699
|
+
previous_completion_contract_sha256: currentContractSha,
|
|
700
|
+
completion_contract_sha256: completionContractSha,
|
|
701
|
+
changed: semanticBindingChanged,
|
|
702
|
+
}),
|
|
703
|
+
now(),
|
|
704
|
+
));
|
|
705
|
+
}
|
|
582
706
|
statement(db, 'INSERT INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
|
|
583
707
|
input.taskId,
|
|
584
708
|
eventKey,
|
|
@@ -589,16 +713,120 @@ export class WorkflowSupervisorStore {
|
|
|
589
713
|
previous_objective_sha256: currentSha,
|
|
590
714
|
objective_sha256: targetSha,
|
|
591
715
|
changed,
|
|
716
|
+
completion_contract_sha256: completionContractSha,
|
|
717
|
+
...(semanticBinding ? {
|
|
718
|
+
semantic_binding_kind: semanticBindingKind,
|
|
719
|
+
semantic_binding_id: semanticBindingId,
|
|
720
|
+
semantic_binding_changed: semanticBindingChanged,
|
|
721
|
+
} : {}),
|
|
592
722
|
...(refreshedEffect ? { refreshed_effect_id: refreshedEffect.effectId } : {}),
|
|
593
723
|
}),
|
|
594
724
|
now(),
|
|
595
725
|
));
|
|
596
726
|
const updatedTask = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
597
727
|
if (!updatedTask) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
|
|
598
|
-
return {
|
|
728
|
+
return {
|
|
729
|
+
task: taskFromRow(updatedTask), changed, deduplicated: false, objectiveSha256: targetSha,
|
|
730
|
+
completionContractSha256: completionContractSha, semanticBindingChanged,
|
|
731
|
+
...(refreshedEffect ? { refreshedEffect } : {}),
|
|
732
|
+
};
|
|
599
733
|
});
|
|
600
734
|
}
|
|
601
735
|
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; }); }
|
|
736
|
+
bindScheduledWake(input: { taskId: string; repoId: string; workId: string; scheduleId: string }): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean } {
|
|
737
|
+
const scheduleId = boundedText(input.scheduleId);
|
|
738
|
+
if (!scheduleId || scheduleId.length > 512) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_ID_INVALID');
|
|
739
|
+
return this.transaction((db) => {
|
|
740
|
+
const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
741
|
+
if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
742
|
+
const task = taskFromRow(row);
|
|
743
|
+
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))) {
|
|
744
|
+
throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
|
|
745
|
+
}
|
|
746
|
+
if (task.completionContract.repo_id !== input.repoId || task.completionContract.work_id !== input.workId) {
|
|
747
|
+
throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_WORK_IDENTITY_MISMATCH');
|
|
748
|
+
}
|
|
749
|
+
if (task.continuationPolicy.kind !== 'forge_goal_outer_turn') throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_OUTER_TURN_REQUIRED');
|
|
750
|
+
const existingScheduleId = boundedText(task.continuationPolicy.scheduled_wake_schedule_id);
|
|
751
|
+
if (existingScheduleId) {
|
|
752
|
+
if (existingScheduleId !== scheduleId) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_BINDING_CONFLICT');
|
|
753
|
+
return { task, changed: false, deduplicated: true };
|
|
754
|
+
}
|
|
755
|
+
const continuationPolicy = { ...task.continuationPolicy, scheduled_wake_schedule_id: scheduleId };
|
|
756
|
+
statement(db, 'UPDATE tasks SET continuation_policy_json = ? WHERE task_id = ?', (s) => s.run(json(continuationPolicy), input.taskId));
|
|
757
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,payload_json,occurred_at) VALUES (?,?,?,?,?)', (s) => s.run(
|
|
758
|
+
input.taskId, `scheduled-wake-bound:${input.taskId}:${scheduleId}`, 'scheduled_wake_bound',
|
|
759
|
+
json({ repo_id: input.repoId, work_id: input.workId, schedule_id: scheduleId, attestation: 'exact_work_schedule_binding' }), now()));
|
|
760
|
+
const updated = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
761
|
+
if (!updated) throw new Error('WORKFLOW_SUPERVISOR_TASK_PERSIST_FAILED');
|
|
762
|
+
return { task: taskFromRow(updated), changed: true, deduplicated: false };
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
refreshUndispatchedEffectPrompt(input: { taskId: string; effectId: string; expectedPrompt: string; prompt: string }): boolean {
|
|
766
|
+
if (!input.prompt.trim()) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_PROMPT_REQUIRED');
|
|
767
|
+
return this.transaction((db) => {
|
|
768
|
+
const row = statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(input.effectId)) as Record<string, unknown> | undefined;
|
|
769
|
+
if (!row || String(row.task_id) !== input.taskId) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_TASK_MISMATCH');
|
|
770
|
+
if (String(row.prompt_text) !== input.expectedPrompt) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_PROMPT_CHANGED');
|
|
771
|
+
const dispatched = statement(db, "SELECT 1 FROM events WHERE effect_id = ? AND kind = 'effect_dispatch_started' LIMIT 1", (s) => s.get(input.effectId));
|
|
772
|
+
const applied = statement(db, "SELECT 1 FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(input.effectId));
|
|
773
|
+
if (dispatched || applied) return false;
|
|
774
|
+
statement(db, 'UPDATE effects SET prompt_text = ? WHERE effect_id = ? AND prompt_text = ?', (s) => s.run(input.prompt, input.effectId, input.expectedPrompt));
|
|
775
|
+
return true;
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
reserveScheduledWake(input: { taskId: string; scheduleId: string; occurrenceId: string; effectId: string; prompt: string }): { effect: WorkflowSupervisorEffect; deduplicated: boolean } {
|
|
779
|
+
const scheduleId = boundedText(input.scheduleId);
|
|
780
|
+
const occurrenceId = boundedText(input.occurrenceId);
|
|
781
|
+
if (!scheduleId || scheduleId.length > 512) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_ID_INVALID');
|
|
782
|
+
if (!occurrenceId || occurrenceId.length > 512) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_OCCURRENCE_ID_INVALID');
|
|
783
|
+
return this.transaction((db) => {
|
|
784
|
+
const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
785
|
+
if (!row) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN');
|
|
786
|
+
const task = taskFromRow(row);
|
|
787
|
+
if (task.continuationPolicy.kind !== 'forge_goal_outer_turn'
|
|
788
|
+
|| task.continuationPolicy.scheduled_wake_schedule_id !== scheduleId) {
|
|
789
|
+
throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_BINDING_MISMATCH');
|
|
790
|
+
}
|
|
791
|
+
const originKey = `scheduled-wake:${scheduleId}:${occurrenceId}`;
|
|
792
|
+
const priorRow = statement(db, 'SELECT * FROM effects WHERE origin_key = ?', (s) => s.get(originKey)) as Record<string, unknown> | undefined;
|
|
793
|
+
if (priorRow) {
|
|
794
|
+
const prior = effectFromRow(priorRow);
|
|
795
|
+
if (prior.taskId !== input.taskId || prior.effectId !== input.effectId || prior.kind !== 'continuation' || !prior.sourceCompletionFingerprint) {
|
|
796
|
+
throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_IDENTITY_CONFLICT');
|
|
797
|
+
}
|
|
798
|
+
return { effect: prior, deduplicated: true };
|
|
799
|
+
}
|
|
800
|
+
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))) {
|
|
801
|
+
throw new Error('WORKFLOW_SUPERVISOR_TASK_TERMINAL');
|
|
802
|
+
}
|
|
803
|
+
if (oldestUnappliedEffect(db, input.taskId)) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_CAUSAL_OBLIGATION_PENDING');
|
|
804
|
+
const awaitingReceipt = statement(db, `SELECT 1 AS ok FROM effects e
|
|
805
|
+
WHERE e.task_id = ?
|
|
806
|
+
AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied')
|
|
807
|
+
AND NOT EXISTS (SELECT 1 FROM completions c WHERE c.task_id = e.task_id AND c.source_effect_id = e.effect_id)
|
|
808
|
+
LIMIT 1`, (s) => s.get(input.taskId));
|
|
809
|
+
if (awaitingReceipt) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_CAUSAL_OBLIGATION_PENDING');
|
|
810
|
+
const latestRow = statement(db, "SELECT c.* FROM completions c WHERE c.task_id = ? ORDER BY COALESCE((SELECT MAX(completion_event.event_id) FROM events completion_event WHERE completion_event.task_id = c.task_id AND completion_event.completion_fingerprint = c.completion_fingerprint AND completion_event.kind = 'assistant_completion'), c.rowid) DESC LIMIT 1", (s) => s.get(input.taskId)) as Record<string, unknown> | undefined;
|
|
811
|
+
if (!latestRow) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_NOT_WAITING');
|
|
812
|
+
const completion = completionFromRow(latestRow);
|
|
813
|
+
if (completion.action !== 'WAIT') throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_NOT_WAITING');
|
|
814
|
+
const consumed = statement(db, 'SELECT * FROM effects WHERE task_id = ? AND source_completion_fingerprint = ?', (s) => s.get(input.taskId, completion.completionFingerprint)) as Record<string, unknown> | undefined;
|
|
815
|
+
if (consumed) throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_ALREADY_CONSUMED');
|
|
816
|
+
const effect = this.reserveEffectWithin(db, {
|
|
817
|
+
taskId: input.taskId,
|
|
818
|
+
effectId: input.effectId,
|
|
819
|
+
kind: 'continuation',
|
|
820
|
+
originKey,
|
|
821
|
+
sourceCompletionFingerprint: completion.completionFingerprint,
|
|
822
|
+
prompt: input.prompt,
|
|
823
|
+
});
|
|
824
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,completion_fingerprint,payload_json,occurred_at) VALUES (?,?,?,?,?,?,?)', (s) => s.run(
|
|
825
|
+
input.taskId, `scheduled-wake-committed:${scheduleId}:${occurrenceId}`, 'scheduled_wake_reserved', effect.effectId,
|
|
826
|
+
completion.completionFingerprint, json({ schedule_id: scheduleId, occurrence_id: occurrenceId, source_completion_fingerprint: completion.completionFingerprint }), now()));
|
|
827
|
+
return { effect, deduplicated: false };
|
|
828
|
+
});
|
|
829
|
+
}
|
|
602
830
|
bindBootstrapConversation(taskId: string, conversationId: string, conversationUrl: string): WorkflowSupervisorTask {
|
|
603
831
|
return this.transaction((db) => {
|
|
604
832
|
const row = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
@@ -674,12 +902,12 @@ export class WorkflowSupervisorStore {
|
|
|
674
902
|
WHERE c.action = 'CONTINUE'
|
|
675
903
|
AND NOT EXISTS (SELECT 1 FROM effects successor WHERE successor.origin_key = 'completion:' || c.completion_fingerprint)
|
|
676
904
|
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.
|
|
905
|
+
ORDER BY COALESCE((SELECT MAX(completion_event.event_id) FROM events completion_event WHERE completion_event.task_id = c.task_id AND completion_event.completion_fingerprint = c.completion_fingerprint AND completion_event.kind = 'assistant_completion'), c.rowid) DESC LIMIT ?`, (s) => s.all(boundedLimit))
|
|
678
906
|
.map((row) => completionFromRow(row as Record<string, unknown>)));
|
|
679
907
|
}
|
|
680
908
|
getCompletionByResponseSha256(taskId: string, responseSha256: string): WorkflowSupervisorCompletion | undefined {
|
|
681
909
|
return this.read((db) => {
|
|
682
|
-
const rows = statement(db, 'SELECT * FROM completions WHERE task_id = ? AND response_sha256 = ? ORDER BY committed_at,
|
|
910
|
+
const rows = statement(db, 'SELECT * FROM completions WHERE task_id = ? AND response_sha256 = ? ORDER BY committed_at, rowid LIMIT 2', (s) => s.all(taskId, responseSha256)) as Record<string, unknown>[];
|
|
683
911
|
if (rows.length > 1) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_COMPLETION_AMBIGUOUS');
|
|
684
912
|
return rows[0] ? completionFromRow(rows[0]) : undefined;
|
|
685
913
|
});
|
|
@@ -687,12 +915,13 @@ export class WorkflowSupervisorStore {
|
|
|
687
915
|
/** The only replay candidate when no applied effect is awaiting a receipt. */
|
|
688
916
|
getLatestCompletion(taskId: string): WorkflowSupervisorCompletion | undefined {
|
|
689
917
|
return this.read((db) => {
|
|
690
|
-
const row = statement(db,
|
|
918
|
+
const row = statement(db, "SELECT c.* FROM completions c WHERE c.task_id = ? ORDER BY COALESCE((SELECT MAX(completion_event.event_id) FROM events completion_event WHERE completion_event.task_id = c.task_id AND completion_event.completion_fingerprint = c.completion_fingerprint AND completion_event.kind = 'assistant_completion'), c.rowid) DESC LIMIT 1", (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
691
919
|
return row ? completionFromRow(row) : undefined;
|
|
692
920
|
});
|
|
693
921
|
}
|
|
694
922
|
continuationProof(input: { repoId?: string; activeReleaseId: string; notBefore: string }): WorkflowSupervisorContinuationProof | undefined {
|
|
695
|
-
|
|
923
|
+
const notBeforeMs = Date.parse(input.notBefore);
|
|
924
|
+
if (!Number.isFinite(notBeforeMs)) throw new Error('WORKFLOW_SUPERVISOR_PROOF_BOUNDARY_INVALID');
|
|
696
925
|
const activeReleaseId = boundedText(input.activeReleaseId);
|
|
697
926
|
if (!activeReleaseId) throw new Error('WORKFLOW_SUPERVISOR_PROOF_RELEASE_REQUIRED');
|
|
698
927
|
return this.read((db) => {
|
|
@@ -701,46 +930,108 @@ export class WorkflowSupervisorStore {
|
|
|
701
930
|
const task = taskFromRow(taskRow);
|
|
702
931
|
const taskRepoId = boundedText(task.completionContract.repo_id) ?? boundedText(task.continuationPolicy.repo_id);
|
|
703
932
|
if (input.repoId?.trim() && taskRepoId !== input.repoId.trim()) continue;
|
|
704
|
-
const completionRows = statement(db,
|
|
933
|
+
const completionRows = statement(db, "SELECT c.* FROM completions c WHERE c.task_id = ? AND c.committed_at >= ? ORDER BY COALESCE((SELECT MAX(completion_event.event_id) FROM events completion_event WHERE completion_event.task_id = c.task_id AND completion_event.completion_fingerprint = c.completion_fingerprint AND completion_event.kind = 'assistant_completion'), c.rowid) ASC", (s) => s.all(task.taskId, input.notBefore)) as Record<string, unknown>[];
|
|
705
934
|
const completions = completionRows.map(completionFromRow);
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
for (let index = 0; index +
|
|
935
|
+
// The bounded release canary task must prove C -> C -> accepted D.
|
|
936
|
+
// Long-running production tasks may remain open; any disposable task can supply this proof.
|
|
937
|
+
for (let index = 0; index + 2 < completions.length; index += 1) {
|
|
938
|
+
if (index + 3 !== completions.length) continue;
|
|
709
939
|
const first = completions[index]!;
|
|
710
940
|
const second = completions[index + 1]!;
|
|
711
|
-
|
|
712
|
-
|
|
941
|
+
const third = completions[index + 2]!;
|
|
942
|
+
if (first.action !== 'CONTINUE' || second.action !== 'CONTINUE' || third.action !== 'DONE') continue;
|
|
943
|
+
const rows = [first, second, third].map((completion) => statement(db, 'SELECT * FROM effects WHERE effect_id = ?', (s) => s.get(completion.sourceEffectId)) as Record<string, unknown> | undefined);
|
|
713
944
|
if (rows.some((row) => !row)) continue;
|
|
714
|
-
const [firstEffect, secondEffect] = rows.map((row) => effectFromRow(row!)) as [WorkflowSupervisorEffect, WorkflowSupervisorEffect];
|
|
715
|
-
if (
|
|
945
|
+
const [firstEffect, secondEffect, thirdEffect] = rows.map((row) => effectFromRow(row!)) as [WorkflowSupervisorEffect, WorkflowSupervisorEffect, WorkflowSupervisorEffect];
|
|
946
|
+
if (
|
|
947
|
+
[firstEffect, secondEffect, thirdEffect].some((effect) => effect.taskId !== task.taskId)
|
|
948
|
+
|| secondEffect.sourceCompletionFingerprint !== first.completionFingerprint
|
|
949
|
+
|| thirdEffect.sourceCompletionFingerprint !== second.completionFingerprint
|
|
950
|
+
) continue;
|
|
716
951
|
const runtimeInstanceIds: string[] = [];
|
|
717
952
|
let valid = true;
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
953
|
+
let thirdCompletionEventId = 0;
|
|
954
|
+
for (const [completionIndex, completion, effect] of [
|
|
955
|
+
[0, first, firstEffect],
|
|
956
|
+
[1, second, secondEffect],
|
|
957
|
+
[2, third, thirdEffect],
|
|
958
|
+
] as const) {
|
|
959
|
+
const dispatchRows = 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(effect.effectId)) as Array<{ event_id?: number; payload_json?: string; occurred_at?: string }>;
|
|
960
|
+
if (
|
|
961
|
+
dispatchRows.length !== 1
|
|
962
|
+
|| !Number.isFinite(Date.parse(String(dispatchRows[0]?.occurred_at ?? '')))
|
|
963
|
+
|| Date.parse(String(dispatchRows[0]?.occurred_at ?? '')) < notBeforeMs
|
|
964
|
+
) { valid = false; break; }
|
|
965
|
+
const dispatch = dispatchRows[0]!;
|
|
966
|
+
const evidence = parsedObject(dispatch.payload_json);
|
|
722
967
|
if (boundedText(evidence.active_release_id) !== activeReleaseId) { valid = false; break; }
|
|
723
968
|
const runtimeInstanceId = boundedText(evidence.runtime_instance_id);
|
|
724
969
|
if (!runtimeInstanceId) { valid = false; break; }
|
|
725
970
|
runtimeInstanceIds.push(runtimeInstanceId);
|
|
726
|
-
const
|
|
971
|
+
const appliedRows = statement(db, "SELECT event_id,occurred_at FROM events WHERE effect_id = ? AND kind = 'effect_applied' ORDER BY event_id", (s) => s.all(effect.effectId)) as Array<{ event_id?: number; occurred_at?: string }>;
|
|
972
|
+
const applied = appliedRows.find((row) => {
|
|
973
|
+
const occurredAtMs = Date.parse(String(row.occurred_at ?? ''));
|
|
974
|
+
return Number(row.event_id ?? 0) > Number(dispatch.event_id ?? 0)
|
|
975
|
+
&& Number.isFinite(occurredAtMs)
|
|
976
|
+
&& occurredAtMs >= notBeforeMs;
|
|
977
|
+
});
|
|
727
978
|
if (!applied) { valid = false; break; }
|
|
979
|
+
const completionEvents = statement(db, "SELECT event_id,occurred_at FROM events WHERE task_id = ? AND completion_fingerprint = ? AND kind = 'assistant_completion' ORDER BY event_id", (s) => s.all(task.taskId, completion.completionFingerprint)) as Array<{ event_id?: number; occurred_at?: string }>;
|
|
980
|
+
if (
|
|
981
|
+
completionEvents.length !== 1
|
|
982
|
+
|| !Number.isFinite(Date.parse(String(completionEvents[0]?.occurred_at ?? '')))
|
|
983
|
+
|| Date.parse(String(completionEvents[0]?.occurred_at ?? '')) < notBeforeMs
|
|
984
|
+
|| Number(completionEvents[0]?.event_id ?? 0) <= Number(applied.event_id ?? 0)
|
|
985
|
+
) { valid = false; break; }
|
|
986
|
+
if (completionIndex === 2) thirdCompletionEventId = Number(completionEvents[0]?.event_id ?? 0);
|
|
728
987
|
}
|
|
729
|
-
const minimumRuntimeInstances =
|
|
988
|
+
const minimumRuntimeInstances = 2;
|
|
730
989
|
if (!valid || new Set(runtimeInstanceIds).size < minimumRuntimeInstances) continue;
|
|
990
|
+
const terminalRows = statement(db, "SELECT event_id,occurred_at FROM events WHERE task_id = ? AND completion_fingerprint = ? AND kind = 'terminal_done' ORDER BY event_id", (s) => s.all(task.taskId, third.completionFingerprint)) as Array<{ event_id?: number; occurred_at?: string }>;
|
|
991
|
+
const latestTerminal = statement(db, "SELECT kind,completion_fingerprint 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(task.taskId)) as { kind?: string; completion_fingerprint?: string } | undefined;
|
|
992
|
+
if (
|
|
993
|
+
terminalRows.length !== 1
|
|
994
|
+
|| !Number.isFinite(Date.parse(String(terminalRows[0]?.occurred_at ?? '')))
|
|
995
|
+
|| Date.parse(String(terminalRows[0]?.occurred_at ?? '')) < notBeforeMs
|
|
996
|
+
|| Number(terminalRows[0]?.event_id ?? 0) <= thirdCompletionEventId
|
|
997
|
+
|| latestTerminal?.kind !== 'terminal_done'
|
|
998
|
+
|| latestTerminal.completion_fingerprint !== third.completionFingerprint
|
|
999
|
+
) continue;
|
|
731
1000
|
return {
|
|
732
1001
|
taskId: task.taskId, conversationId: task.conversationId, activeReleaseId,
|
|
733
|
-
actions: ['CONTINUE', 'CONTINUE'],
|
|
734
|
-
completionFingerprints: [first.completionFingerprint, second.completionFingerprint],
|
|
735
|
-
sourceEffectIds: [first.sourceEffectId, second.sourceEffectId],
|
|
1002
|
+
actions: ['CONTINUE', 'CONTINUE', 'DONE'],
|
|
1003
|
+
completionFingerprints: [first.completionFingerprint, second.completionFingerprint, third.completionFingerprint],
|
|
1004
|
+
sourceEffectIds: [first.sourceEffectId, second.sourceEffectId, third.sourceEffectId],
|
|
736
1005
|
runtimeInstanceIds: [...new Set(runtimeInstanceIds)],
|
|
737
|
-
firstCommittedAt: first.committedAt, lastCommittedAt:
|
|
1006
|
+
firstCommittedAt: first.committedAt, lastCommittedAt: third.committedAt,
|
|
738
1007
|
};
|
|
739
1008
|
}
|
|
740
1009
|
}
|
|
741
1010
|
return undefined;
|
|
742
1011
|
});
|
|
743
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Exact persisted pre-Send editor evidence. ProseMirror may introduce a
|
|
1015
|
+
* mid-body line break; only the observed prior exact digest grants recovery.
|
|
1016
|
+
*/
|
|
1017
|
+
preSendComposerObservation(effectId: string): WorkflowSupervisorPreflightComposerObservation | undefined {
|
|
1018
|
+
return this.read((db) => {
|
|
1019
|
+
const sql = "SELECT e.payload_json FROM events e JOIN effects fx ON fx.effect_id = e.effect_id "
|
|
1020
|
+
+ "WHERE e.effect_id = ? AND e.kind = 'effect_preflight_rejected' "
|
|
1021
|
+
+ "AND e.event_id < (SELECT MIN(t.event_id) FROM events t WHERE t.task_id = fx.task_id AND t.kind = 'terminal_stopped') "
|
|
1022
|
+
+ "ORDER BY e.event_id DESC LIMIT 1";
|
|
1023
|
+
const row = statement(db, sql, (query) => query.get(effectId)) as { payload_json?: string } | undefined;
|
|
1024
|
+
const data = parsedObject(row?.payload_json);
|
|
1025
|
+
const editor = parsedObject(data.composer_observation);
|
|
1026
|
+
if (data.send_clicked !== false || data.attestation !== 'computer_prepare_returned_before_supervisor_send_intent'
|
|
1027
|
+
|| editor.classification !== 'current_effect' || editor.selectorVersion !== 'visible-single-v2'
|
|
1028
|
+
|| !Number.isInteger(editor.chars) || Number(editor.chars) < 1
|
|
1029
|
+
|| typeof editor.fingerprint !== 'string' || !/^[a-f0-9]{24}$/.test(editor.fingerprint)) return undefined;
|
|
1030
|
+
return { chars: Number(editor.chars), fingerprint: editor.fingerprint,
|
|
1031
|
+
classification: 'current_effect', selectorVersion: 'visible-single-v2' };
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
744
1035
|
latestEffectDispatch(effectId: string): { eventId: number; generation: number; evidence: Record<string, unknown> } | undefined {
|
|
745
1036
|
return this.read((db) => {
|
|
746
1037
|
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;
|
|
@@ -781,11 +1072,7 @@ export class WorkflowSupervisorStore {
|
|
|
781
1072
|
}
|
|
782
1073
|
const retryAuthorized = latestNotAppliedProofEventId(db, effect.effectId) > ledger.lastEventId;
|
|
783
1074
|
if (!retryAuthorized) {
|
|
784
|
-
|
|
785
|
-
if (unknownLedger.sameFingerprintCount > 0
|
|
786
|
-
&& nowMs - unknownLedger.lastOccurredAtMs < unknownObservationDelayMs(unknownLedger.sameFingerprintCount)) {
|
|
787
|
-
return undefined;
|
|
788
|
-
}
|
|
1075
|
+
if (!unknownObservationDue(db, effect.effectId, ledger.lastEventId, nowMs)) return undefined;
|
|
789
1076
|
return { effect, mode: 'reconcile', generation: ledger.lastGeneration };
|
|
790
1077
|
}
|
|
791
1078
|
if (ledger.generations >= WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + ledger.budgetRefunds) return undefined;
|
|
@@ -826,7 +1113,19 @@ export class WorkflowSupervisorStore {
|
|
|
826
1113
|
const pending = this.currentUnappliedEffect(taskId);
|
|
827
1114
|
if (pending) {
|
|
828
1115
|
const ledger = this.read((db) => ({ ...effectDispatchLedger(db, pending.effectId), proof: latestNotAppliedProofEventId(db, pending.effectId), preflightBlocked: this.preflightBlocked(db, pending.effectId) }));
|
|
829
|
-
if (ledger.preflightBlocked)
|
|
1116
|
+
if (ledger.preflightBlocked) {
|
|
1117
|
+
const failure = this.read((db) => statement(db,
|
|
1118
|
+
"SELECT payload_json FROM events WHERE effect_id = ? AND kind = 'effect_preflight_rejected' ORDER BY event_id DESC LIMIT 1",
|
|
1119
|
+
(s) => s.get(pending.effectId)) as { payload_json?: string } | undefined);
|
|
1120
|
+
const record = parsedObject(failure?.payload_json);
|
|
1121
|
+
const reasonCode = boundedText(record.reason)?.slice(0, 256);
|
|
1122
|
+
const diagnostic = record.composer_observation;
|
|
1123
|
+
const preflightComposerObservation = diagnostic && typeof diagnostic === 'object' && !Array.isArray(diagnostic)
|
|
1124
|
+
? diagnostic as WorkflowSupervisorPreflightComposerObservation : undefined;
|
|
1125
|
+
return { state: 'spaced', effectId: pending.effectId, reason: 'pre_send_infrastructure_blocked',
|
|
1126
|
+
...(reasonCode ? { preflightReasonCode: reasonCode } : {}),
|
|
1127
|
+
...(preflightComposerObservation ? { preflightComposerObservation } : {}) };
|
|
1128
|
+
}
|
|
830
1129
|
const ceiling = WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS + ledger.budgetRefunds;
|
|
831
1130
|
const retryAuthorized = ledger.generations === 0 || ledger.proof > ledger.lastEventId;
|
|
832
1131
|
// An exhausted retry ceiling is the operator-facing state: no amount of
|
|
@@ -845,13 +1144,92 @@ export class WorkflowSupervisorStore {
|
|
|
845
1144
|
if (this.hasAppliedEffectAwaitingCompletion(taskId)) {
|
|
846
1145
|
return { state: 'spaced', ...(leaf ? { effectId: leaf.effectId } : {}), reason: 'awaiting_completion_receipt' };
|
|
847
1146
|
}
|
|
1147
|
+
const waiting = this.read((db) => {
|
|
1148
|
+
const taskRow = statement(db, 'SELECT * FROM tasks WHERE task_id = ?', (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
1149
|
+
if (!taskRow) return undefined;
|
|
1150
|
+
const task = taskFromRow(taskRow);
|
|
1151
|
+
const scheduleId = boundedText(task.continuationPolicy.scheduled_wake_schedule_id);
|
|
1152
|
+
if (!scheduleId) return undefined;
|
|
1153
|
+
const latest = statement(db, "SELECT c.completion_fingerprint, c.action FROM completions c WHERE c.task_id = ? ORDER BY COALESCE((SELECT MAX(completion_event.event_id) FROM events completion_event WHERE completion_event.task_id = c.task_id AND completion_event.completion_fingerprint = c.completion_fingerprint AND completion_event.kind = 'assistant_completion'), c.rowid) DESC LIMIT 1", (s) => s.get(taskId)) as { completion_fingerprint?: string; action?: string } | undefined;
|
|
1154
|
+
return latest?.action === 'WAIT' && latest.completion_fingerprint
|
|
1155
|
+
? { state: 'waiting' as const, scheduleId, completionFingerprint: latest.completion_fingerprint }
|
|
1156
|
+
: undefined;
|
|
1157
|
+
});
|
|
1158
|
+
if (waiting) return waiting;
|
|
848
1159
|
return { state: 'inert' };
|
|
849
1160
|
}
|
|
850
1161
|
/**
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
1162
|
+
* Consecutive genuinely failed task-local attempts since the last durable
|
|
1163
|
+
* provider progress or conversation move. Counts across failure codes and
|
|
1164
|
+
* Runtime restarts; an unknown submission observation is NOT a failed send.
|
|
1165
|
+
* This is a projection of the existing Supervisor journal, not another
|
|
1166
|
+
* mutable retry or delivery authority.
|
|
1167
|
+
*/
|
|
1168
|
+
automaticConversationFailureEvidence(taskId: string): { count: number; latestEventId: number; lastFailureAtMs: number } {
|
|
1169
|
+
return this.read((db) => {
|
|
1170
|
+
const row = statement(db, `SELECT COUNT(*) AS total, COALESCE(MAX(event_id),0) AS latest_id, MAX(occurred_at) AS last_at
|
|
1171
|
+
FROM events WHERE task_id = ? AND kind IN (
|
|
1172
|
+
'effect_preflight_rejected', 'assistant_provider_failed',
|
|
1173
|
+
'assistant_recovery_exhausted', 'browser_delivery_attempt_failed'
|
|
1174
|
+
) AND event_id > COALESCE((
|
|
1175
|
+
SELECT MAX(event_id) FROM events WHERE task_id = ?
|
|
1176
|
+
AND kind IN ('effect_applied','assistant_completion','conversation_migrated')
|
|
1177
|
+
),0)`, (s) => s.get(taskId, taskId)) as { total?: number; latest_id?: number; last_at?: string };
|
|
1178
|
+
return { count: Number(row?.total ?? 0), latestEventId: Number(row?.latest_id ?? 0),
|
|
1179
|
+
lastFailureAtMs: Date.parse(row?.last_at ?? '') || 0 };
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/** Journal one attempted task-local Computer operation, never a guessed Send. */
|
|
1184
|
+
recordBrowserDeliveryAttemptFailure(input: { taskId: string; conversationId: string; code: string; effectId?: string }): void {
|
|
1185
|
+
this.transaction((db) => {
|
|
1186
|
+
const task = statement(db, 'SELECT conversation_id FROM tasks WHERE task_id = ?', (s) => s.get(input.taskId)) as { conversation_id?: string } | undefined;
|
|
1187
|
+
// An old tab or pre-migration provider result cannot charge the new chat.
|
|
1188
|
+
if (!task || task.conversation_id !== input.conversationId) return;
|
|
1189
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
1190
|
+
input.taskId, `browser-delivery-failed:${input.taskId}:${randomUUID()}`,
|
|
1191
|
+
'browser_delivery_attempt_failed', input.effectId ?? null,
|
|
1192
|
+
json({ conversation_id: input.conversationId, code: input.code.slice(0, 128), phase: 'task_local_pre_mutation' }), now(),
|
|
1193
|
+
));
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/** One operator-approved repair of a proven pre-send failure, never a provider retry refund. */
|
|
1198
|
+
authorizeOperatorPreflightRetry(input: { effectId: string; requestId: string; reason: string; authorizedBy: string }): { deduplicated: boolean } {
|
|
1199
|
+
return this.transaction((db) => {
|
|
1200
|
+
const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(input.effectId)) as { task_id?: string } | undefined;
|
|
1201
|
+
if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
1202
|
+
if (statement(db, "SELECT 1 FROM events WHERE effect_id = ? AND kind IN ('effect_applied','effect_outcome_unknown_superseded') LIMIT 1", (s) => s.get(input.effectId))) {
|
|
1203
|
+
throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_EFFECT_TERMINAL');
|
|
1204
|
+
}
|
|
1205
|
+
if (effectDispatchLedger(db, input.effectId).generations !== 0) throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_RECOVERY_SEND_ALREADY_RECORDED');
|
|
1206
|
+
const key = `effect-preflight-operator-rearmed:${input.effectId}:${input.requestId}`;
|
|
1207
|
+
if (statement(db, 'SELECT 1 FROM events WHERE event_key = ?', (s) => s.get(key))) return { deduplicated: true };
|
|
1208
|
+
const failed = effectPreflightFailureReleaseIds(db, input.effectId);
|
|
1209
|
+
const releaseId = this.activeReleaseId()?.trim() || 'unidentified-runtime';
|
|
1210
|
+
// An operator may authorize ONE bounded no-Send re-observation after a
|
|
1211
|
+
// verified repair, including on a successor Runtime after the automatic
|
|
1212
|
+
// three-release cap. Never erase the prior failures or refund Send budget.
|
|
1213
|
+
if (statement(db, "SELECT 1 FROM events WHERE effect_id = ? AND kind = 'effect_preflight_operator_rearmed' LIMIT 1", (s) => s.get(input.effectId))) {
|
|
1214
|
+
throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_OPERATOR_REARM_EXHAUSTED');
|
|
1215
|
+
}
|
|
1216
|
+
if (!this.preflightBlocked(db, input.effectId)
|
|
1217
|
+
|| (failed.length < WORKFLOW_SUPERVISOR_MAX_PREFLIGHT_RELEASE_ATTEMPTS
|
|
1218
|
+
&& !failed.some((entry) => entry.releaseId === releaseId))) {
|
|
1219
|
+
throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_RECOVERY_NOT_BLOCKED');
|
|
1220
|
+
}
|
|
1221
|
+
statement(db, 'INSERT INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
1222
|
+
effect.task_id!, key, 'effect_preflight_operator_rearmed', input.effectId,
|
|
1223
|
+
json({ request_id: input.requestId, authorized_by: input.authorizedBy, reason: boundedText(input.reason)?.slice(0, 512), active_release_id: releaseId, attestation: 'operator_authorized_after_proven_no_send' }), now(),
|
|
1224
|
+
));
|
|
1225
|
+
return { deduplicated: false };
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Explicit operator authority to continue a mechanically proven un-applied
|
|
1231
|
+
* send after its retry ceiling, without replaying an outcome-unknown mutation.
|
|
1232
|
+
* This refund path is distinct from zero-generation UI preparation repair.
|
|
855
1233
|
*/
|
|
856
1234
|
authorizeOperatorRetry(input: { effectId: string; requestId: string; reason: string; authorizedBy: string }): { refunds: number; generations: number; deduplicated: boolean } {
|
|
857
1235
|
return this.transaction((db) => {
|
|
@@ -946,6 +1324,12 @@ export class WorkflowSupervisorStore {
|
|
|
946
1324
|
});
|
|
947
1325
|
}
|
|
948
1326
|
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)))); }
|
|
1327
|
+
bootstrapAppliedReconcileDue(effectId: string): boolean {
|
|
1328
|
+
return this.read((db) => {
|
|
1329
|
+
const dispatch = effectDispatchLedger(db, effectId);
|
|
1330
|
+
return dispatch.generations > 0 && unknownObservationDue(db, effectId, dispatch.lastEventId, this.clockMs());
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
949
1333
|
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
1334
|
latestAppliedEffectWithoutCompletion(taskId: string): WorkflowSupervisorEffect | undefined {
|
|
951
1335
|
return this.read((db) => {
|
|
@@ -966,7 +1350,10 @@ export class WorkflowSupervisorStore {
|
|
|
966
1350
|
WHERE e.task_id = ?
|
|
967
1351
|
AND EXISTS (SELECT 1 FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied')
|
|
968
1352
|
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
|
|
1353
|
+
AND NOT EXISTS (SELECT 1 FROM effects child
|
|
1354
|
+
WHERE child.origin_key = 'provider-recovery:' || e.effect_id
|
|
1355
|
+
AND EXISTS (SELECT 1 FROM events started
|
|
1356
|
+
WHERE started.effect_id = child.effect_id AND started.kind = 'effect_dispatch_started'))
|
|
970
1357
|
ORDER BY (SELECT MAX(event_id) FROM events applied WHERE applied.effect_id = e.effect_id AND applied.kind = 'effect_applied') DESC
|
|
971
1358
|
LIMIT 1`, (s) => s.get(taskId)) as Record<string, unknown> | undefined;
|
|
972
1359
|
return row ? effectFromRow(row) : undefined;
|
|
@@ -1262,12 +1649,16 @@ export class WorkflowSupervisorStore {
|
|
|
1262
1649
|
// Other observers can report a transport error while the native sender
|
|
1263
1650
|
// is still preparing. Only that generation's dispatch owner can prove
|
|
1264
1651
|
// it returned before clicking Send.
|
|
1265
|
-
|
|
1652
|
+
const unknown = 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;
|
|
1653
|
+
return unknown ? { ...unknown, dispatch_generation: dispatch.lastGeneration } : undefined;
|
|
1266
1654
|
});
|
|
1267
1655
|
const evidence = parsedObject(row?.payload_json);
|
|
1268
1656
|
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
1657
|
if (evidence.surface !== 'macos-native' || evidence.reconciliation === true || !beforeSend.has(String(evidence.reason))) return false;
|
|
1270
|
-
this.recordEffectNotAppliedProof(effectId, `operator-pre-send:${requestId}`, {
|
|
1658
|
+
this.recordEffectNotAppliedProof(effectId, `operator-pre-send:${requestId}`, {
|
|
1659
|
+
...evidence, send_clicked: false, dispatch_generation: row?.dispatch_generation, pre_send_rejection: true,
|
|
1660
|
+
requested_by: 'user', request_id: requestId, attestation: 'persisted_native_pre_send_return',
|
|
1661
|
+
});
|
|
1271
1662
|
return true;
|
|
1272
1663
|
}
|
|
1273
1664
|
private reserveEffectWithin(db: Database, input: { taskId: string; effectId: string; kind: WorkflowEffectKind; originKey: string; sourceCompletionFingerprint?: string; prompt: string }): WorkflowSupervisorEffect {
|
|
@@ -1285,8 +1676,8 @@ export class WorkflowSupervisorStore {
|
|
|
1285
1676
|
return effect;
|
|
1286
1677
|
}
|
|
1287
1678
|
|
|
1288
|
-
/**
|
|
1289
|
-
recordEffectPreparationFailure(effectId: string, reasonCode: string): void {
|
|
1679
|
+
/** Persist a bounded provider pre-send rejection; a started generation requires a durable exact no-click proof first. */
|
|
1680
|
+
recordEffectPreparationFailure(effectId: string, reasonCode: string, composerObservation?: WorkflowSupervisorPreflightComposerObservation): void {
|
|
1290
1681
|
this.transaction((db) => {
|
|
1291
1682
|
const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
|
|
1292
1683
|
if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
@@ -1296,10 +1687,13 @@ export class WorkflowSupervisorStore {
|
|
|
1296
1687
|
throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_FAILURE_OUTCOME_NOT_PROVEN');
|
|
1297
1688
|
}
|
|
1298
1689
|
const activeReleaseId = this.activeReleaseId()?.trim() || 'unidentified-runtime';
|
|
1299
|
-
|
|
1690
|
+
// After one explicit repair the next real preparation failure MUST be
|
|
1691
|
+
// recorded again; otherwise INSERT OR IGNORE could permit infinite retries.
|
|
1692
|
+
const rearm = statement(db, "SELECT COUNT(*) AS count FROM events WHERE effect_id = ? AND kind = 'effect_preflight_operator_rearmed'", (s) => s.get(effectId)) as { count?: number } | undefined;
|
|
1693
|
+
const key = `effect-preflight-rejected:${effectId}:${activeReleaseId}${Number(rearm?.count ?? 0) > 0 ? ':operator-rearmed' : ''}`;
|
|
1300
1694
|
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
1301
1695
|
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(),
|
|
1696
|
+
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', ...(composerObservation ? { composer_observation: composerObservation } : {}) }), now(),
|
|
1303
1697
|
));
|
|
1304
1698
|
});
|
|
1305
1699
|
}
|
|
@@ -1309,6 +1703,11 @@ export class WorkflowSupervisorStore {
|
|
|
1309
1703
|
return this.transaction((db) => {
|
|
1310
1704
|
const effect = statement(db, 'SELECT task_id FROM effects WHERE effect_id = ?', (s) => s.get(effectId)) as { task_id?: string } | undefined;
|
|
1311
1705
|
if (!effect?.task_id) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_UNKNOWN');
|
|
1706
|
+
// Completion and recovery Send compete in this same Store transaction.
|
|
1707
|
+
// A command obtained before a late completion cannot revive its recovery.
|
|
1708
|
+
if (statement(db, `SELECT 1 FROM effects e JOIN completions c
|
|
1709
|
+
ON c.task_id = e.task_id AND e.origin_key = 'provider-recovery:' || c.source_effect_id
|
|
1710
|
+
WHERE e.effect_id = ? AND e.kind = 'recovery' LIMIT 1`, (s) => s.get(effectId))) return false;
|
|
1312
1711
|
const applied = statement(db, "SELECT 1 AS ok FROM events WHERE effect_id = ? AND kind = 'effect_applied' LIMIT 1", (s) => s.get(effectId));
|
|
1313
1712
|
if (applied) throw new Error('WORKFLOW_SUPERVISOR_EFFECT_ALREADY_APPLIED');
|
|
1314
1713
|
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 }>;
|
|
@@ -1353,12 +1752,23 @@ export class WorkflowSupervisorStore {
|
|
|
1353
1752
|
const payload = json(proof);
|
|
1354
1753
|
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
1754
|
if (existing && (existing.kind !== 'effect_not_applied' || existing.payload_json !== payload)) throw new Error('WORKFLOW_SUPERVISOR_OBSERVATION_ID_CONFLICT');
|
|
1356
|
-
|
|
1755
|
+
if (existing) return; // An identical old receipt is idempotent, not fresh retry authority.
|
|
1357
1756
|
const ledger = effectDispatchLedger(db, effectId);
|
|
1757
|
+
const proofGeneration = Number(proof.dispatch_generation);
|
|
1758
|
+
if (!Number.isInteger(proofGeneration) || proofGeneration < 1 || proofGeneration !== ledger.lastGeneration) {
|
|
1759
|
+
throw new Error('WORKFLOW_SUPERVISOR_NOT_APPLIED_PROOF_GENERATION_STALE');
|
|
1760
|
+
}
|
|
1761
|
+
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()));
|
|
1762
|
+
const legacyRefund = statement(db, 'SELECT event_id FROM events WHERE event_key = ?', (s) => s.get(`effect-dispatch-budget-refund:${effectId}`)) as { event_id?: number } | undefined;
|
|
1763
|
+
const legacyRefundCoversGeneration = proofGeneration === WORKFLOW_SUPERVISOR_MAX_DISPATCH_GENERATIONS && Boolean(legacyRefund);
|
|
1358
1764
|
if (proof.pre_send_rejection === true
|
|
1359
|
-
&&
|
|
1360
|
-
&&
|
|
1361
|
-
|
|
1765
|
+
&& Number.isInteger(proofGeneration) && proofGeneration > 0
|
|
1766
|
+
&& proofGeneration === ledger.lastGeneration
|
|
1767
|
+
&& !legacyRefundCoversGeneration) {
|
|
1768
|
+
statement(db, 'INSERT OR IGNORE INTO events(task_id,event_key,kind,effect_id,payload_json,occurred_at) VALUES (?,?,?,?,?,?)', (s) => s.run(
|
|
1769
|
+
effect.task_id, `effect-dispatch-budget-refund:${effectId}:${proofGeneration}`, 'effect_dispatch_budget_refunded', effectId,
|
|
1770
|
+
json({ reason: 'pre_send_rejection_did_not_reach_provider', dispatch_generation: proofGeneration }), now(),
|
|
1771
|
+
));
|
|
1362
1772
|
}
|
|
1363
1773
|
});
|
|
1364
1774
|
}
|
|
@@ -1376,6 +1786,15 @@ export class WorkflowSupervisorStore {
|
|
|
1376
1786
|
throw new Error('WORKFLOW_SUPERVISOR_SOURCE_EFFECT_COMPLETION_CONFLICT');
|
|
1377
1787
|
}
|
|
1378
1788
|
const existing = statement(db, 'SELECT completion_fingerprint FROM completions WHERE completion_fingerprint = ?', (s) => s.get(input.completionFingerprint));
|
|
1789
|
+
// Once the recovery has a Send intent, its outcome may be unknown. A
|
|
1790
|
+
// late source reply must not create a parallel successor or rotate away
|
|
1791
|
+
// from that obligation. Exact historical completion replay stays inert.
|
|
1792
|
+
if (!existing && statement(db, `SELECT 1 FROM effects child JOIN events started
|
|
1793
|
+
ON started.effect_id = child.effect_id AND started.kind = 'effect_dispatch_started'
|
|
1794
|
+
WHERE child.task_id = ? AND child.origin_key = ? LIMIT 1`,
|
|
1795
|
+
(s) => s.get(input.taskId, 'provider-recovery:' + input.sourceEffectId))) {
|
|
1796
|
+
throw new Error('WORKFLOW_SUPERVISOR_SOURCE_EFFECT_RECOVERY_DISPATCHED');
|
|
1797
|
+
}
|
|
1379
1798
|
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
1799
|
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
1800
|
let successorEffect: WorkflowSupervisorEffect | undefined;
|