@sema-agent/server 1.323.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-hmac.d.ts +40 -0
- package/dist/approval-hmac.js +67 -0
- package/dist/approval.d.ts +31 -0
- package/dist/approval.js +72 -4
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +40 -1
- package/dist/auth-bridge.d.ts +10 -0
- package/dist/auth-bridge.js +47 -4
- package/dist/auth-keys.d.ts +19 -0
- package/dist/auth-keys.js +13 -0
- package/dist/bake-runner/main.d.ts +5 -0
- package/dist/bake-runner/main.js +43 -4
- package/dist/bake-runner/protocol.d.ts +57 -0
- package/dist/bake-runner/protocol.js +58 -3
- package/dist/bake-runner/runner.d.ts +54 -0
- package/dist/bake-runner/runner.js +87 -5
- package/dist/bench/l8/artifact.d.ts +63 -0
- package/dist/bench/l8/artifact.js +61 -2
- package/dist/bench/l8/escape.d.ts +36 -0
- package/dist/bench/l8/escape.js +25 -0
- package/dist/bench/l8/index.d.ts +14 -0
- package/dist/bench/l8/index.js +14 -0
- package/dist/bench/l8/probes.d.ts +74 -0
- package/dist/bench/l8/probes.js +60 -0
- package/dist/bench/l8/run-probes.d.ts +43 -0
- package/dist/bench/l8/run-probes.js +45 -2
- package/dist/bench/s1/arms.d.ts +126 -0
- package/dist/bench/s1/arms.js +188 -13
- package/dist/bench/s1/live-deps.d.ts +66 -0
- package/dist/bench/s1/live-deps.js +249 -11
- package/dist/bench/s1/oracle.d.ts +49 -0
- package/dist/bench/s1/oracle.js +36 -1
- package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
- package/dist/bench/s1/repair-oracle-adapter.js +30 -0
- package/dist/bench/s1/reviewer.d.ts +38 -0
- package/dist/bench/s1/reviewer.js +34 -0
- package/dist/bench/s1/row.d.ts +77 -0
- package/dist/bench/s1/row.js +62 -0
- package/dist/bench/s1/run-firm.d.ts +47 -0
- package/dist/bench/s1/run-firm.js +70 -4
- package/dist/bench/s1/runner-ctx.d.ts +71 -0
- package/dist/bench/s1/runner-ctx.js +36 -0
- package/dist/bench/s1/tasks.d.ts +108 -0
- package/dist/bench/s1/tasks.js +115 -0
- package/dist/boot-reclaim.d.ts +19 -0
- package/dist/boot-reclaim.js +20 -0
- package/dist/brain.d.ts +25 -0
- package/dist/brain.js +61 -1
- package/dist/budget.d.ts +66 -0
- package/dist/budget.js +128 -4
- package/dist/capabilities/builtin-tools.d.ts +3 -0
- package/dist/capabilities/builtin-tools.js +3 -0
- package/dist/capabilities/center-plugins.d.ts +17 -1
- package/dist/capabilities/center-plugins.js +45 -4
- package/dist/capabilities/center-prompts.d.ts +46 -0
- package/dist/capabilities/center-prompts.js +57 -3
- package/dist/capabilities/code-review-council.d.ts +28 -0
- package/dist/capabilities/code-review-council.js +40 -7
- package/dist/capabilities/collab-wire.d.ts +2 -0
- package/dist/capabilities/collab-workflows.d.ts +7 -0
- package/dist/capabilities/collab-workflows.js +44 -1
- package/dist/capabilities/oa-tools.d.ts +6 -0
- package/dist/capabilities/oa-tools.js +10 -0
- package/dist/capabilities/prompt.d.ts +14 -0
- package/dist/capabilities/prompt.js +14 -0
- package/dist/capabilities/prompts/code-review.d.ts +14 -0
- package/dist/capabilities/prompts/code-review.js +14 -0
- package/dist/capabilities/prompts/identity.d.ts +6 -0
- package/dist/capabilities/prompts/identity.js +6 -0
- package/dist/capabilities/prompts/team.d.ts +4 -0
- package/dist/capabilities/prompts/team.js +4 -0
- package/dist/capabilities/repo-tools.d.ts +11 -0
- package/dist/capabilities/repo-tools.js +8 -0
- package/dist/capabilities/sandbox-file-send.d.ts +95 -0
- package/dist/capabilities/sandbox-file-send.js +91 -3
- package/dist/capabilities/scenarios.d.ts +130 -0
- package/dist/capabilities/scenarios.js +180 -7
- package/dist/capabilities/select-environment-tool.d.ts +7 -0
- package/dist/capabilities/select-environment-tool.js +30 -0
- package/dist/capabilities/send-user-file-tool.d.ts +33 -0
- package/dist/capabilities/send-user-file-tool.js +65 -1
- package/dist/capabilities/skills.d.ts +29 -0
- package/dist/capabilities/skills.js +13 -2
- package/dist/capabilities/team.d.ts +22 -0
- package/dist/capabilities/team.js +22 -2
- package/dist/capabilities/tool-defer.d.ts +26 -0
- package/dist/capabilities/tool-defer.js +3 -0
- package/dist/config-center/apply-effective.d.ts +65 -0
- package/dist/config-center/apply-effective.js +204 -12
- package/dist/config-center/http-client.d.ts +34 -0
- package/dist/config-center/http-client.js +68 -0
- package/dist/config-center/restart-signal.d.ts +22 -0
- package/dist/config-center/restart-signal.js +39 -0
- package/dist/config-center/skills-mcp.d.ts +20 -1
- package/dist/config-center/skills-mcp.js +42 -7
- package/dist/config-center/types.d.ts +92 -1
- package/dist/config-lkg.d.ts +12 -0
- package/dist/config-lkg.js +41 -0
- package/dist/config-provider.d.ts +78 -0
- package/dist/config-provider.js +165 -2
- package/dist/config-types.d.ts +546 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +455 -60
- package/dist/degenerate-instrument.d.ts +38 -0
- package/dist/degenerate-instrument.js +55 -3
- package/dist/elicitation.d.ts +39 -0
- package/dist/elicitation.js +87 -3
- package/dist/env-facts.d.ts +61 -0
- package/dist/env-facts.js +73 -7
- package/dist/fleet/fleet-bus.d.ts +185 -0
- package/dist/fleet/fleet-bus.js +333 -27
- package/dist/fleet/subagent-tail-bus.d.ts +11 -0
- package/dist/fleet/subagent-tail-bus.js +33 -2
- package/dist/fleet-client.d.ts +38 -0
- package/dist/fleet-client.js +44 -9
- package/dist/fleet-lease.d.ts +46 -0
- package/dist/fleet-lease.js +73 -5
- package/dist/hooks/branch-transcript.d.ts +8 -0
- package/dist/hooks/branch-transcript.js +33 -0
- package/dist/hooks/cc-stop-prompt.d.ts +42 -0
- package/dist/hooks/cc-stop-prompt.js +43 -1
- package/dist/hooks/hook-llm.d.ts +15 -0
- package/dist/hooks/hook-llm.js +47 -0
- package/dist/hooks/hook-runner.d.ts +89 -0
- package/dist/hooks/hook-runner.js +327 -18
- package/dist/http/idempotency.d.ts +31 -0
- package/dist/http/idempotency.js +34 -0
- package/dist/http/principal-gate.d.ts +17 -0
- package/dist/http/principal-gate.js +35 -3
- package/dist/http/route-ctx.d.ts +36 -0
- package/dist/http/routes/approvals-assistant.d.ts +25 -0
- package/dist/http/routes/approvals-assistant.js +253 -23
- package/dist/http/routes/attachments.js +22 -4
- package/dist/http/routes/capabilities.d.ts +7 -0
- package/dist/http/routes/capabilities.js +202 -1
- package/dist/http/routes/fleet.d.ts +6 -0
- package/dist/http/routes/fleet.js +94 -5
- package/dist/http/routes/images.d.ts +17 -0
- package/dist/http/routes/images.js +222 -14
- package/dist/http/routes/leader.d.ts +7 -0
- package/dist/http/routes/leader.js +10 -0
- package/dist/http/routes/memory-policy.d.ts +6 -0
- package/dist/http/routes/memory-policy.js +38 -6
- package/dist/http/routes/notify-wake.d.ts +10 -0
- package/dist/http/routes/notify-wake.js +19 -3
- package/dist/http/routes/observability.d.ts +6 -0
- package/dist/http/routes/observability.js +19 -2
- package/dist/http/routes/runs.d.ts +15 -0
- package/dist/http/routes/runs.js +464 -41
- package/dist/http/routes/session-sync.d.ts +15 -0
- package/dist/http/routes/session-sync.js +229 -19
- package/dist/http/routes/sessions-list.d.ts +6 -0
- package/dist/http/routes/sessions-list.js +35 -4
- package/dist/http/routes/sessions.d.ts +14 -0
- package/dist/http/routes/sessions.js +176 -31
- package/dist/http/routes/side-query.d.ts +8 -0
- package/dist/http/routes/side-query.js +26 -1
- package/dist/http/routes/tasks.d.ts +18 -0
- package/dist/http/routes/tasks.js +501 -38
- package/dist/http/routes/trace-usage.d.ts +10 -0
- package/dist/http/routes/trace-usage.js +66 -14
- package/dist/http/routes/workflows.d.ts +12 -0
- package/dist/http/routes/workflows.js +180 -19
- package/dist/http/run-meta.d.ts +11 -0
- package/dist/http/run-meta.js +6 -0
- package/dist/http/send.d.ts +19 -0
- package/dist/http/send.js +11 -0
- package/dist/http/server.d.ts +255 -0
- package/dist/http/server.js +921 -75
- package/dist/http/sse-log.d.ts +15 -0
- package/dist/http/sse-log.js +16 -0
- package/dist/http/tar.d.ts +9 -0
- package/dist/http/tar.js +21 -5
- package/dist/http/wire-gate.d.ts +9 -0
- package/dist/http/wire-gate.js +9 -0
- package/dist/http/wire-types.d.ts +187 -0
- package/dist/http/workspace-content.d.ts +8 -0
- package/dist/http/workspace-content.js +10 -0
- package/dist/images/bake-validate.d.ts +63 -0
- package/dist/images/bake-validate.js +71 -1
- package/dist/images/manifest.d.ts +19 -0
- package/dist/images/manifest.js +6 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.js +10 -5
- package/dist/key-resolver.d.ts +23 -0
- package/dist/key-resolver.js +27 -2
- package/dist/leader/diffout.d.ts +23 -0
- package/dist/leader/diffout.js +8 -1
- package/dist/leader/diffup.d.ts +32 -0
- package/dist/leader/diffup.js +43 -0
- package/dist/leader/endpoint.d.ts +20 -0
- package/dist/leader/endpoint.js +25 -2
- package/dist/leader/fanout.d.ts +60 -0
- package/dist/leader/fanout.js +29 -3
- package/dist/leader/grader-env-factory.d.ts +74 -0
- package/dist/leader/grader-env-factory.js +62 -3
- package/dist/leader/leader.d.ts +97 -0
- package/dist/leader/leader.js +151 -4
- package/dist/leader/merge.d.ts +94 -0
- package/dist/leader/merge.js +112 -9
- package/dist/leader/planner.d.ts +52 -0
- package/dist/leader/planner.js +47 -3
- package/dist/leader/repair-oracle.d.ts +61 -0
- package/dist/leader/repair-oracle.js +59 -1
- package/dist/leader/repair-wire.d.ts +78 -0
- package/dist/leader/repair-wire.js +79 -2
- package/dist/leader/wire.d.ts +79 -0
- package/dist/leader/wire.js +255 -8
- package/dist/lsp/e2b-bridge.d.ts +16 -0
- package/dist/lsp/e2b-bridge.js +32 -4
- package/dist/lsp/e2b-manager.d.ts +10 -0
- package/dist/lsp/e2b-manager.js +18 -2
- package/dist/lsp/lsp-frames.d.ts +12 -0
- package/dist/lsp/lsp-frames.js +12 -0
- package/dist/lsp/manager.d.ts +12 -0
- package/dist/lsp/manager.js +35 -2
- package/dist/lsp/types.d.ts +10 -0
- package/dist/lsp/ws-transport.d.ts +12 -0
- package/dist/lsp/ws-transport.js +53 -5
- package/dist/lsp-evict.d.ts +14 -0
- package/dist/lsp-evict.js +15 -1
- package/dist/main.js +2132 -96
- package/dist/memory-export.d.ts +1 -0
- package/dist/memory-export.js +4 -0
- package/dist/memory-scope.d.ts +49 -0
- package/dist/memory-scope.js +87 -4
- package/dist/memory-sync-client.d.ts +24 -0
- package/dist/memory-sync-client.js +41 -2
- package/dist/memory-sync.d.ts +23 -0
- package/dist/memory-sync.js +70 -1
- package/dist/model-select.d.ts +39 -0
- package/dist/model-select.js +42 -1
- package/dist/observability/cost-quota.d.ts +20 -0
- package/dist/observability/cost-quota.js +4 -0
- package/dist/observability/cost-taxonomy.d.ts +45 -0
- package/dist/observability/cost-taxonomy.js +34 -0
- package/dist/observability/logger.d.ts +1 -0
- package/dist/observability/logger.js +6 -0
- package/dist/observability/metrics.d.ts +13 -0
- package/dist/observability/metrics.js +63 -0
- package/dist/observability/otel-exporter.d.ts +9 -0
- package/dist/observability/otel-exporter.js +11 -3
- package/dist/observability/principal-context.d.ts +9 -0
- package/dist/observability/principal-context.js +9 -0
- package/dist/observability/prompt-manifest.d.ts +37 -0
- package/dist/observability/prompt-manifest.js +35 -1
- package/dist/observability/rate-limit.d.ts +10 -0
- package/dist/observability/rate-limit.js +1 -0
- package/dist/observability/tool-trace.d.ts +24 -0
- package/dist/observability/tool-trace.js +41 -1
- package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
- package/dist/orchestration/hardened-vm-runner.js +119 -4
- package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
- package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
- package/dist/orchestration/hardened-vm-worker.js +27 -0
- package/dist/orchestration/subagent-steer.d.ts +44 -0
- package/dist/orchestration/subagent-steer.js +45 -1
- package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
- package/dist/orchestration/workflow-agent-steer.js +80 -1
- package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
- package/dist/orchestration/workflow-completion-inbox.js +283 -32
- package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
- package/dist/orchestration/workflow-notify-journal.js +187 -13
- package/dist/parked-decide.d.ts +61 -0
- package/dist/parked-decide.js +58 -1
- package/dist/per-task-image.d.ts +54 -0
- package/dist/per-task-image.js +41 -0
- package/dist/plan-cache-probe.d.ts +35 -0
- package/dist/plan-cache-probe.js +18 -3
- package/dist/plugins/approval-exemption-store.d.ts +7 -0
- package/dist/plugins/approval-exemption-store.js +28 -2
- package/dist/plugins/approval-store-sql.d.ts +64 -0
- package/dist/plugins/approval-store-sql.js +32 -0
- package/dist/plugins/background-agent-store-sql.d.ts +55 -0
- package/dist/plugins/background-agent-store-sql.js +91 -4
- package/dist/plugins/background-shell-support.d.ts +98 -0
- package/dist/plugins/background-shell-support.js +117 -13
- package/dist/plugins/blob-backend.d.ts +104 -0
- package/dist/plugins/blob-backend.js +163 -6
- package/dist/plugins/breaker-state-sql.d.ts +65 -1
- package/dist/plugins/breaker-state-sql.js +46 -8
- package/dist/plugins/caching-session-store.d.ts +42 -0
- package/dist/plugins/caching-session-store.js +63 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
- package/dist/plugins/checkpoint-store-sql.js +239 -11
- package/dist/plugins/file-outcome-sink.d.ts +4 -0
- package/dist/plugins/file-outcome-sink.js +9 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
- package/dist/plugins/file-resume-anchor-store.js +43 -4
- package/dist/plugins/file-run-store.d.ts +90 -0
- package/dist/plugins/file-run-store.js +145 -13
- package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
- package/dist/plugins/file-snapshot-store-sql.js +178 -10
- package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
- package/dist/plugins/file-workflow-journal-store.js +10 -0
- package/dist/plugins/fork-routing-session-store.d.ts +47 -0
- package/dist/plugins/fork-routing-session-store.js +51 -3
- package/dist/plugins/host-platform.d.ts +69 -0
- package/dist/plugins/host-platform.js +81 -2
- package/dist/plugins/image-bake-store-sql.d.ts +136 -1
- package/dist/plugins/image-bake-store-sql.js +241 -7
- package/dist/plugins/image-index-sql.d.ts +46 -0
- package/dist/plugins/image-index-sql.js +113 -4
- package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
- package/dist/plugins/k8s-bg-scripts.js +88 -4
- package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
- package/dist/plugins/k8s-exec-protocol.js +41 -0
- package/dist/plugins/local-checkpoint-store.d.ts +40 -0
- package/dist/plugins/local-checkpoint-store.js +86 -7
- package/dist/plugins/local-session-store.d.ts +157 -0
- package/dist/plugins/local-session-store.js +289 -34
- package/dist/plugins/local-task-attachment-store.js +14 -2
- package/dist/plugins/mailbox-store-sql.d.ts +38 -0
- package/dist/plugins/mailbox-store-sql.js +39 -6
- package/dist/plugins/memory-engine-pg.d.ts +30 -0
- package/dist/plugins/memory-engine-pg.js +122 -4
- package/dist/plugins/memory-engine-tidb.d.ts +26 -0
- package/dist/plugins/memory-engine-tidb.js +115 -5
- package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
- package/dist/plugins/memory-engine-vector-util.js +5 -0
- package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
- package/dist/plugins/memory-resume-anchor-store.js +17 -0
- package/dist/plugins/memory-run-store.d.ts +63 -0
- package/dist/plugins/memory-run-store.js +73 -8
- package/dist/plugins/memory-session-policy-store.d.ts +17 -0
- package/dist/plugins/memory-session-policy-store.js +17 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
- package/dist/plugins/memory-sync-store-pg.js +45 -2
- package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
- package/dist/plugins/memory-sync-store-tidb.js +31 -1
- package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
- package/dist/plugins/outcome-ledger-sql.js +97 -3
- package/dist/plugins/pg-approval-store.d.ts +7 -0
- package/dist/plugins/pg-approval-store.js +7 -0
- package/dist/plugins/pg-breaker-state.d.ts +6 -0
- package/dist/plugins/pg-breaker-state.js +6 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
- package/dist/plugins/pg-checkpoint-store.js +8 -0
- package/dist/plugins/pg-cost-quota.d.ts +17 -0
- package/dist/plugins/pg-cost-quota.js +6 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/pg-file-snapshot-store.js +6 -0
- package/dist/plugins/pg-image-bake.d.ts +9 -0
- package/dist/plugins/pg-image-bake.js +9 -0
- package/dist/plugins/pg-image-index.d.ts +9 -0
- package/dist/plugins/pg-image-index.js +9 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
- package/dist/plugins/pg-outcome-ledger.js +9 -0
- package/dist/plugins/pg-pool.d.ts +50 -0
- package/dist/plugins/pg-pool.js +78 -0
- package/dist/plugins/pg-query.d.ts +11 -0
- package/dist/plugins/pg-rate-limiter.d.ts +23 -0
- package/dist/plugins/pg-rate-limiter.js +12 -2
- package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/pg-resume-anchor-store.js +5 -0
- package/dist/plugins/pg-run-store.d.ts +7 -0
- package/dist/plugins/pg-run-store.js +7 -0
- package/dist/plugins/pg-safe-json.d.ts +13 -0
- package/dist/plugins/pg-safe-json.js +40 -4
- package/dist/plugins/pg-session-policy-store.d.ts +5 -0
- package/dist/plugins/pg-session-policy-store.js +5 -0
- package/dist/plugins/pg-session-storage.d.ts +72 -0
- package/dist/plugins/pg-session-storage.js +152 -20
- package/dist/plugins/pg-session-store.d.ts +10 -0
- package/dist/plugins/pg-session-store.js +10 -0
- package/dist/plugins/pg-tool-result-store.d.ts +7 -0
- package/dist/plugins/pg-tool-result-store.js +7 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-journal-store.js +7 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
- package/dist/plugins/pg-workflow-run-store.js +7 -0
- package/dist/plugins/posix-shell-fs.d.ts +3 -0
- package/dist/plugins/posix-shell-fs.js +31 -1
- package/dist/plugins/remote-env-adb.d.ts +25 -0
- package/dist/plugins/remote-env-adb.js +95 -12
- package/dist/plugins/remote-env-e2b.d.ts +146 -0
- package/dist/plugins/remote-env-e2b.js +371 -40
- package/dist/plugins/remote-env-file-error.d.ts +34 -0
- package/dist/plugins/remote-env-file-error.js +37 -0
- package/dist/plugins/remote-env-host.d.ts +151 -0
- package/dist/plugins/remote-env-host.js +468 -70
- package/dist/plugins/remote-env-k8s.d.ts +135 -0
- package/dist/plugins/remote-env-k8s.js +318 -32
- package/dist/plugins/remote-env-local-docker.d.ts +65 -0
- package/dist/plugins/remote-env-local-docker.js +149 -24
- package/dist/plugins/remote-env-ssh.d.ts +35 -0
- package/dist/plugins/remote-env-ssh.js +117 -20
- package/dist/plugins/remote-scratchpad.d.ts +38 -0
- package/dist/plugins/remote-scratchpad.js +32 -2
- package/dist/plugins/remote-shell.d.ts +22 -0
- package/dist/plugins/remote-shell.js +27 -1
- package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
- package/dist/plugins/resume-anchor-store-sql.js +11 -0
- package/dist/plugins/roster-store-sql.d.ts +27 -0
- package/dist/plugins/roster-store-sql.js +54 -0
- package/dist/plugins/run-store-sql.d.ts +194 -0
- package/dist/plugins/run-store-sql.js +210 -9
- package/dist/plugins/s3-presign.d.ts +20 -0
- package/dist/plugins/s3-presign.js +49 -2
- package/dist/plugins/scheduler-support.d.ts +12 -0
- package/dist/plugins/scheduler-support.js +75 -3
- package/dist/plugins/send-file-ledger.d.ts +15 -0
- package/dist/plugins/send-file-ledger.js +53 -4
- package/dist/plugins/send-user-file.d.ts +77 -0
- package/dist/plugins/send-user-file.js +92 -4
- package/dist/plugins/session-policy-store-sql.d.ts +15 -0
- package/dist/plugins/session-policy-store-sql.js +75 -3
- package/dist/plugins/session-store.d.ts +45 -0
- package/dist/plugins/session-store.js +51 -0
- package/dist/plugins/sql-driver.d.ts +77 -0
- package/dist/plugins/sql-driver.js +4 -0
- package/dist/plugins/sql-escape.d.ts +8 -0
- package/dist/plugins/sql-escape.js +8 -0
- package/dist/plugins/sql-row-helpers.d.ts +25 -0
- package/dist/plugins/sql-row-helpers.js +25 -0
- package/dist/plugins/store-backend.d.ts +103 -0
- package/dist/plugins/store-backend.js +120 -27
- package/dist/plugins/store-contracts.d.ts +62 -0
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +60 -0
- package/dist/plugins/task-attachment-store.js +49 -3
- package/dist/plugins/task-list-store-sql.d.ts +25 -0
- package/dist/plugins/task-list-store-sql.js +21 -0
- package/dist/plugins/tidb-approval-store.d.ts +6 -0
- package/dist/plugins/tidb-approval-store.js +6 -0
- package/dist/plugins/tidb-breaker-state.d.ts +5 -0
- package/dist/plugins/tidb-breaker-state.js +5 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
- package/dist/plugins/tidb-checkpoint-store.js +7 -0
- package/dist/plugins/tidb-cost-quota.d.ts +6 -0
- package/dist/plugins/tidb-cost-quota.js +1 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
- package/dist/plugins/tidb-file-snapshot-store.js +6 -0
- package/dist/plugins/tidb-image-bake.d.ts +8 -0
- package/dist/plugins/tidb-image-bake.js +8 -0
- package/dist/plugins/tidb-image-index.d.ts +8 -0
- package/dist/plugins/tidb-image-index.js +8 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
- package/dist/plugins/tidb-outcome-ledger.js +10 -0
- package/dist/plugins/tidb-pool.d.ts +56 -0
- package/dist/plugins/tidb-pool.js +181 -1
- package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
- package/dist/plugins/tidb-rate-limiter.js +5 -2
- package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
- package/dist/plugins/tidb-resume-anchor-store.js +5 -0
- package/dist/plugins/tidb-run-store.d.ts +7 -0
- package/dist/plugins/tidb-run-store.js +7 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
- package/dist/plugins/tidb-session-policy-store.js +5 -0
- package/dist/plugins/tidb-session-storage.d.ts +43 -0
- package/dist/plugins/tidb-session-storage.js +62 -3
- package/dist/plugins/tidb-session-store.d.ts +142 -0
- package/dist/plugins/tidb-session-store.js +264 -16
- package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
- package/dist/plugins/tidb-tool-result-store.js +7 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
- package/dist/plugins/tidb-workflow-journal-store.js +7 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
- package/dist/plugins/tidb-workflow-run-store.js +8 -0
- package/dist/plugins/tool-result-store-sql.d.ts +58 -0
- package/dist/plugins/tool-result-store-sql.js +38 -2
- package/dist/plugins/web-search.d.ts +35 -0
- package/dist/plugins/web-search.js +45 -9
- package/dist/plugins/workflow-journal-limits.d.ts +10 -0
- package/dist/plugins/workflow-journal-limits.js +10 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
- package/dist/plugins/workflow-journal-store-sql.js +58 -2
- package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
- package/dist/plugins/workflow-run-store-sql.js +84 -6
- package/dist/plugins/worktree-isolation.d.ts +45 -0
- package/dist/plugins/worktree-isolation.js +105 -6
- package/dist/plugins/write-behind-counter.d.ts +56 -1
- package/dist/plugins/write-behind-counter.js +53 -14
- package/dist/principal-jwt.d.ts +24 -0
- package/dist/principal-jwt.js +60 -5
- package/dist/project-memory.d.ts +22 -0
- package/dist/project-memory.js +144 -15
- package/dist/prompts-domain-validate.d.ts +28 -0
- package/dist/prompts-domain-validate.js +42 -1
- package/dist/question.d.ts +33 -0
- package/dist/question.js +69 -2
- package/dist/resource-suspend.d.ts +21 -0
- package/dist/resource-suspend.js +16 -0
- package/dist/router/route-orchestration.d.ts +78 -0
- package/dist/router/route-orchestration.js +77 -0
- package/dist/run-local.d.ts +32 -0
- package/dist/run-local.js +172 -8
- package/dist/runs.d.ts +189 -4
- package/dist/runs.js +465 -24
- package/dist/runtime-caps-resolver.d.ts +106 -0
- package/dist/runtime-caps-resolver.js +89 -4
- package/dist/runtime-governance.d.ts +84 -0
- package/dist/runtime-governance.js +127 -1
- package/dist/sandbox-pkg-source.d.ts +23 -0
- package/dist/sandbox-pkg-source.js +37 -0
- package/dist/sealed-key.d.ts +54 -0
- package/dist/sealed-key.js +68 -3
- package/dist/security.d.ts +108 -0
- package/dist/security.js +145 -6
- package/dist/sema-registry.d.ts +21 -0
- package/dist/sema-registry.js +21 -0
- package/dist/session-leaf-bus.d.ts +31 -0
- package/dist/session-leaf-bus.js +32 -0
- package/dist/session-sync-content.d.ts +46 -0
- package/dist/session-sync-content.js +47 -1
- package/dist/session-sync-kernel.d.ts +89 -0
- package/dist/session-sync-kernel.js +48 -3
- package/dist/session-sync.d.ts +125 -0
- package/dist/session-sync.js +181 -8
- package/dist/session-titler.d.ts +26 -0
- package/dist/session-titler.js +41 -7
- package/dist/session-watch.d.ts +53 -1
- package/dist/session-watch.js +73 -9
- package/dist/sighup-idle.d.ts +30 -0
- package/dist/sighup-idle.js +12 -1
- package/dist/spec-fields.d.ts +91 -3
- package/dist/spec-fields.js +116 -4
- package/dist/task-cwd.d.ts +50 -0
- package/dist/task-cwd.js +68 -1
- package/dist/task-mcp.d.ts +47 -0
- package/dist/task-mcp.js +29 -0
- package/dist/task-settings.d.ts +175 -0
- package/dist/task-settings.js +226 -11
- package/dist/task-workflow.d.ts +67 -0
- package/dist/task-workflow.js +75 -8
- package/dist/tool-approval.d.ts +91 -0
- package/dist/tool-approval.js +215 -0
- package/dist/trace/artifacts.d.ts +9 -0
- package/dist/trace/artifacts.js +30 -5
- package/dist/trace/core-keyset-guard.d.ts +18 -0
- package/dist/trace/ledger-sink.d.ts +43 -0
- package/dist/trace/ledger-sink.js +64 -5
- package/dist/trace/project.d.ts +133 -0
- package/dist/trace/project.js +189 -2
- package/dist/trace/redact.d.ts +19 -0
- package/dist/trace/redact.js +66 -9
- package/dist/usage-analytics.d.ts +26 -0
- package/dist/usage-analytics.js +26 -3
- package/package.json +1 -1
|
@@ -16,6 +16,8 @@ export async function handleTasks(req, res, url, ctx) {
|
|
|
16
16
|
await handleTasksBody(req, res, url, ctx, miss);
|
|
17
17
|
return !miss.fell;
|
|
18
18
|
}
|
|
19
|
+
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(A9 的唯一改动:去缩进;B3 的唯一改动:
|
|
20
|
+
* 给两个匿名 `idemCache.run` 回调与三条驱动腿起名)。裸 `return;` = 「本域已应答」;走到函数尾才是「没匹配上」。 */
|
|
19
21
|
async function handleTasksBody(req, res, url, ctx, miss) {
|
|
20
22
|
const { deps } = ctx;
|
|
21
23
|
const { idemCache, inflightRuns, cancelledViaVerb, steerableRuns, counters } = ctx.registry;
|
|
@@ -24,15 +26,23 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
24
26
|
const reqState = ctx.req;
|
|
25
27
|
const source = ctx.req.source;
|
|
26
28
|
if (req.method === "POST" && (url === "/v1/tasks" || url === "/v1/tasks/stream")) {
|
|
29
|
+
// Idempotency-Key replay BEFORE the rate/quota gates (council blocker): a retry with a known key must get
|
|
30
|
+
// the SAME response, not a fresh 429 because the window filled meanwhile. No-op without a key / on a miss.
|
|
27
31
|
const rawIdem = headerStr(req.headers["idempotency-key"]);
|
|
32
|
+
// Scope by identity only when there IS a key (BL-3) — avoids reading the principal header on every
|
|
33
|
+
// submit, and keeps the no-key path independent of principal config.
|
|
34
|
+
// F-fix: scope on the VERIFIED principal (gatedPrincipal) — on a direct door a spoofable header would let a
|
|
35
|
+
// caller poison/collide another principal's idempotency cache. (BFF/gated: identical to principalFrom.)
|
|
28
36
|
const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, gatedPrincipal(req, deps.config)) : undefined;
|
|
29
37
|
const cached = idemKey ? idemCache.peek(idemKey) : undefined;
|
|
30
38
|
if (cached) {
|
|
31
39
|
if (url === "/v1/tasks/stream") {
|
|
40
|
+
// The cached entry may be IN-FLIGHT (a retry arriving during the original stream) → set up the heartbeat
|
|
41
|
+
// BEFORE awaiting so a long wait doesn't idle-timeout, then replay the terminal result.
|
|
32
42
|
sseHeaders(res);
|
|
33
43
|
const hb = setInterval(() => {
|
|
34
44
|
if (!res.writableEnded)
|
|
35
|
-
res.write(`event: heartbeat\ndata: {}\n\n`);
|
|
45
|
+
res.write(`event: heartbeat\ndata: {}\n\n`); // real frame (not an SSE comment) — a per-frame-parsing BFF drops comments, so downstream saw a zero-frame window; EventSource clients without a heartbeat listener ignore it (zero break)
|
|
36
46
|
}, 15_000);
|
|
37
47
|
try {
|
|
38
48
|
const resp = await cached;
|
|
@@ -55,32 +65,70 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
55
65
|
const prepared = await prepareSpec(req, res);
|
|
56
66
|
if (!prepared)
|
|
57
67
|
return;
|
|
68
|
+
// withPrincipal threads the caller's identity through the run so the cost tracer attributes spend
|
|
69
|
+
// (incl. council/team sub-tasks) to this principal's cumulative quota.
|
|
58
70
|
const principal = prepared.auth?.principal;
|
|
59
71
|
if (url === "/v1/tasks/stream") {
|
|
60
72
|
if (prepared.verify || prepared.cascade) {
|
|
61
73
|
sendError(res, 400, "request.field_conflict", "verify / cascade are not supported on /v1/tasks/stream (both are multi-attempt, not a single stream) — use /v1/tasks or /v1/runs");
|
|
62
74
|
return;
|
|
63
75
|
}
|
|
76
|
+
// R8 (CC parity, fixed for the AgentEvent contract — workflow re-review #2): mint the durable rewind
|
|
77
|
+
// handle (taskId) BEFORE the SSE headers so it rides as an `X-Task-Id` RESPONSE HEADER. The shell reads it even
|
|
78
|
+
// on a Ctrl+C before `done`, WITHOUT an out-of-contract `run_started` data frame (the SDK AgentEvent stream is a
|
|
79
|
+
// CLOSED oneOf; a synthetic data frame makes exhaustive consumers assertNever-throw). createRun below CLAIMS this id.
|
|
64
80
|
const earlyDurableTid = deps.runStore && prepared.spec.sessionId ? uuidv7() : undefined;
|
|
81
|
+
// [854]①b per-request opt-in 断连宽限:请求头 `x-detach-on-disconnect: true`(选头不选 body 字段——
|
|
82
|
+
// 这是本条连接的传输层语义,不该进持久化 body / 不该随 resume 重放;与 idempotency-key 同类)。
|
|
83
|
+
// 默认行为不动:交互 Esc = 断连即杀的反 token-burn 契约保留(下方 onDisconnect 的 abort 路径)。
|
|
84
|
+
// opt-in 后 client 断连不 abort,run 转后台跑完,结果按既有 durable 路径可查(X-Task-Id → GET /v1/runs/:id)。
|
|
85
|
+
// 无 durable 账本(无 runStore 或无 sessionId)时 400 fail-loud 而非静默忽略:忽略 = 断连仍杀 run,
|
|
86
|
+
// 与 caller 声明的语义相反;就算不杀,跑完的结果也无处可查(纯烧 token)—— body.model silent-drop 同类,
|
|
87
|
+
// 「不做临时方案」判 400 是真版。SSE 头未发,还能干净 400。
|
|
65
88
|
const detachOnDisconnect = headerStr(req.headers["x-detach-on-disconnect"]) === "true";
|
|
66
89
|
if (detachOnDisconnect && earlyDurableTid === undefined) {
|
|
67
90
|
sendError(res, 400, "request.precondition_unmet", "x-detach-on-disconnect requires a durable run (a run store + sessionId) — without one the detached result would be unqueryable");
|
|
68
91
|
return;
|
|
69
92
|
}
|
|
70
|
-
reqState.streamTaskId = earlyDurableTid;
|
|
93
|
+
reqState.streamTaskId = earlyDurableTid; // [854]⑥: 499 断连日志带 task 上下文(record 只在 aborted 行读它)
|
|
94
|
+
// E8 双腿对齐:detach 腿的 modelUsage 归因键(sessionId 域)——声明在此层,leak-fence clear 在本层 finally。
|
|
71
95
|
let usageKey;
|
|
96
|
+
// [854]①b/⑥:opt-in 一被受理就置 detached 标记(而非等断连时置)——record 的 res 'close' listener
|
|
97
|
+
// 注册在 onDisconnect 之前,断连时 record 先跑;在这里置位才能让 499 行真带上 detached:true。
|
|
98
|
+
// 语义不变:opt-in 受理后的任何断连都是 detach(run 不杀)。
|
|
72
99
|
reqState.streamDetached = detachOnDisconnect;
|
|
73
100
|
sseHeaders(res, earlyDurableTid ? { "x-task-id": earlyDurableTid } : undefined);
|
|
101
|
+
// G15([1833]/[1840]§五):taskId 一等信号进流首帧——浏览器 EventSource 与部分代理面读不到响应头,
|
|
102
|
+
// 消费端此前只能靠 x-task-id(cli liveClient 亲测取头)。meta 帧与头同源同条件(durable 腿才有 id,
|
|
103
|
+
// 无 durable ⇒ 头/帧同缺席,不铸空值);sessionId 同帧(server 铸新 session 时 caller 需要它绑定)。
|
|
104
|
+
// additive:未知 event 型按 SSE 语义被旧消费端忽略。
|
|
74
105
|
if (earlyDurableTid) {
|
|
75
106
|
res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", taskId: earlyDurableTid, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) })}\n\n`);
|
|
76
107
|
}
|
|
108
|
+
// Abort the underlying run (and stop the heartbeat) when the client disconnects — otherwise a dropped
|
|
109
|
+
// relay keeps burning billable tokens to completion (council). Mirrors the closed-flag pattern of the
|
|
110
|
+
// poll-based SSE endpoints, and additionally cancels the generator via spec.signal.
|
|
77
111
|
const ac = new AbortController();
|
|
112
|
+
// probe-stoppedby: a client DISCONNECT on the sync stream lane is a USER stop too —
|
|
113
|
+
// Ctrl+C on the shell drops the SSE and this abort kills the run, but the mark was only wired on the
|
|
114
|
+
// cancel-verb/resume legs, so background children reaped by THIS abort read "parent"/"system" instead
|
|
115
|
+
// of "user". Same registration discipline as runs.ts (FIRST on the signal, before core's teardown
|
|
116
|
+
// listeners). Owner = sessionId (the sync leg's core-canonical taskId — resolveSpec sets no spec.taskId,
|
|
117
|
+
// same key as the resume leg); skipSessionScoped inside the helper keeps CC Backgrounded children unmarked.
|
|
78
118
|
if (prepared.spec.sessionId)
|
|
79
119
|
markChildrenStoppedByUserOnAbort(ac.signal, prepared.spec.sessionId, prepared.auth?.principal);
|
|
80
120
|
let closed = false;
|
|
81
|
-
let detachLogged = false;
|
|
121
|
+
let detachLogged = false; // [854]①b:detach 断连 info 只打一条(req/res 两个 close listener 都可能进来)
|
|
82
122
|
const onDisconnect = () => {
|
|
123
|
+
// [854]①b:detach opt-in 的断连分支 —— 不置 closed(for-await 继续消费到 done,终态走既有
|
|
124
|
+
// setTerminal 路径落 durable 账本)、不 abort(run 不杀)。断连后的 res.write 落在已 destroy 的
|
|
125
|
+
// socket 上被 Node 丢弃;防御:挂一个 res 'error' swallow(destroy 后的 write 在部分 Node 版本
|
|
126
|
+
// 经 nextTick 发 ERR_STREAM_DESTROYED,无 listener 会变 uncaught)—— 只在 detach 已断连后挂,
|
|
127
|
+
// 正常车道的 res 错误面不被吞。task_notification 的 deliverable 谓词含 res.destroyed,断连后
|
|
128
|
+
// 自动改走 durable inbox(既有行为,正合 detach 语义)。
|
|
83
129
|
if (detachOnDisconnect) {
|
|
130
|
+
// 正常完成后的 req 'close' 不是断连(默认车道靠 abort-after-done 无害才容得下无 guard 的
|
|
131
|
+
// req listener;detach 车道要防它打出误导的 detached info)。
|
|
84
132
|
if (res.writableEnded)
|
|
85
133
|
return;
|
|
86
134
|
if (!detachLogged) {
|
|
@@ -94,22 +142,58 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
94
142
|
ac.abort();
|
|
95
143
|
};
|
|
96
144
|
req.on("close", onDisconnect);
|
|
145
|
+
// 🔴 req 'close' does NOT fire on a client disconnect once the request body has been fully consumed
|
|
146
|
+
// (prepareSpec read it before we get here; verified against node 24) — only res 'close' does. With only
|
|
147
|
+
// the req listener the disconnect-abort above never fired on this route, i.e. the dropped-relay token
|
|
148
|
+
// burn it exists to stop went right on burning. writableEnded guards the normal-completion close
|
|
149
|
+
// (end() already called → that close is not a disconnect).
|
|
97
150
|
res.on("close", () => {
|
|
98
151
|
if (!res.writableEnded)
|
|
99
152
|
onDisconnect();
|
|
100
153
|
});
|
|
154
|
+
// Heartbeat so a long no-token gap (e.g. a debate phase) doesn't trip a mesh/ingress idle timeout on
|
|
155
|
+
// the relay (center). SSE comment lines are ignored by EventSource but keep the socket alive.
|
|
101
156
|
const hb = setInterval(() => {
|
|
157
|
+
// [854]①b: `!res.destroyed` — detach 断连后 run 可能再跑很久,别每 15s 往已死 socket 写心跳
|
|
158
|
+
// (destroyed = client 已断,写了也只是被丢弃/触发 error;默认车道断连即 abort,窗口极短,不受影响)。
|
|
102
159
|
if (!res.writableEnded && !res.destroyed)
|
|
103
|
-
res.write(`event: heartbeat\ndata: {}\n\n`);
|
|
160
|
+
res.write(`event: heartbeat\ndata: {}\n\n`); // real frame (not an SSE comment) — a per-frame-parsing BFF drops comments, so downstream saw a zero-frame window; EventSource clients without a heartbeat listener ignore it (zero break)
|
|
104
161
|
}, 15_000);
|
|
162
|
+
// Wrap the whole stream in idemCache.run so the IN-FLIGHT promise is stored BEFORE work begins — a retry
|
|
163
|
+
// that arrives WHILE this stream is still running (relay timeout → re-send same key) is then caught by the
|
|
164
|
+
// peek above and replays this result instead of starting a second billable stream (council). `ranLive`
|
|
165
|
+
// tells the caller that actually produced the stream (wrote events live) apart from a concurrent caller the
|
|
166
|
+
// cache deduplicated — the latter got no live events, so it must be handed the terminal result.
|
|
105
167
|
let ranLive = false;
|
|
106
168
|
let resp;
|
|
169
|
+
// ── B3 ── 原文此处是 `idemCache.run` 的第二个实参、一个 **703 行匿名 async**。提为具名闭包:
|
|
170
|
+
// plain-stream 腿。捕获的自由变量(`ranLive`/`prepared`/`principal`/`res`/`ac`/`closed`/`hb`/
|
|
171
|
+
// `earlyDurableTid`/`usageKey`/`reqState`…)与原匿名回调完全同一批,故行为逐字。
|
|
107
172
|
const runStreamSubmitLeg = async () => {
|
|
108
173
|
ranLive = true;
|
|
109
174
|
let finalResult;
|
|
175
|
+
// [868]③: true once the post-stream settle block below recorded the run row's park/terminal state.
|
|
176
|
+
// A throw OUT of the stream (abort mid-iteration, or any plain Error before `done`) skips that whole
|
|
177
|
+
// block — the finally's backstop then settles the leaked `running` row (the durable twin of the
|
|
178
|
+
// fleet-row leak C fixed below; the fleet half got its settleFleet("failed") backstop, the run row
|
|
179
|
+
// was left to reapStale's stale-timeout with a misleading "run stalled" verdict).
|
|
110
180
|
let runRowSettled = false;
|
|
181
|
+
// Durable F4/ask on the stream path. Two things the sync/async paths do that the stream path
|
|
182
|
+
// used to skip (audit functional gap; both caught live on the gate#3 P3 cross-replica drill):
|
|
183
|
+
// 1. putCtx BEFORE the run → a suspend here (e.g. an AskUserQuestion gate, TC-5.4) is resumable
|
|
184
|
+
// on any replica; without it the operator's resume fails "resume context missing".
|
|
185
|
+
// 2. createRun a task_active claim at SUBMIT (+ heartbeat) → reapCtx's task_active guard then
|
|
186
|
+
// protects the ctx for the WHOLE first leg. The ctx's updated_at is stamped at submit and
|
|
187
|
+
// never refreshed, so a multi-minute first leg (a 5-min gradle build outlived the 120s
|
|
188
|
+
// REAP_RUN_STALE_SEC) ages the ctx past the cutoff and reapCtx deletes it MID-RUN — before
|
|
189
|
+
// the suspend's pending checkpoint can protect it. setSuspended KEEPS the claim (session
|
|
190
|
+
// stays locked submit→resume); a clean completion releases it via setTerminal.
|
|
111
191
|
let durableTaskId;
|
|
112
192
|
let durableHeartbeat;
|
|
193
|
+
// R8 (CC parity, interactive rewind on disconnect): the user-message entryId is captured INSIDE
|
|
194
|
+
// streamBody, but the rewind anchor must be writable from BOTH the done branch AND the disconnect branch
|
|
195
|
+
// (Ctrl+C before done) — so hoist it + a once-guarded put to this scope. Without this, a turn cancelled
|
|
196
|
+
// before `done` left NO anchor → the shell, even holding the early taskId, 404'd on rewind.
|
|
113
197
|
let userMsgEntryId;
|
|
114
198
|
let anchorPut = false;
|
|
115
199
|
const putRewindAnchor = () => {
|
|
@@ -120,20 +204,47 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
120
204
|
.put(prepared.spec.sessionId, durableTaskId, userMsgEntryId, principal ?? null)
|
|
121
205
|
.catch(() => deps.metrics?.inc("resume_anchor_capture_failed"));
|
|
122
206
|
};
|
|
207
|
+
// R8 (CC parity): the run-row claim + `durableTaskId` (the REWIND anchor key + the unique
|
|
208
|
+
// result.taskId re-stamp) is gated on `runStore` ALONE; the durable-suspend resume ctx (`putCtx`) on
|
|
209
|
+
// `checkpointStore`. They were bundled under ONE checkpointStore gate, over-coupling REWIND to
|
|
210
|
+
// DURABLE_APPROVAL — but E18 resume-at needs NO checkpoint (main.ts:815). Now matches the /v1/runs bg path:
|
|
211
|
+
// rewind works with just runStore (DB_BACKEND=tidb/pg), no approval. Claim-first preserved (session locked
|
|
212
|
+
// submit→resume; a concurrent submit → 409, never a silent 2nd leg, and a rejected submit never clobbers the ctx).
|
|
123
213
|
if (deps.runStore && prepared.spec.sessionId) {
|
|
124
|
-
const tid = earlyDurableTid ?? uuidv7();
|
|
214
|
+
const tid = earlyDurableTid ?? uuidv7(); // reuse the id already emitted as the X-Task-Id header (defensive mint if absent)
|
|
125
215
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
126
216
|
if (created.ok)
|
|
127
|
-
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective);
|
|
217
|
+
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective); // fire-and-forget; in-titler dedupe
|
|
128
218
|
if (!created.ok) {
|
|
129
219
|
const conflict = { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", errorCode: "conflict.session_active_run", activeTaskId: created.activeTaskId };
|
|
220
|
+
// Headers are already SSE — encode the rejection as the stream's terminal event; the 409 in
|
|
221
|
+
// the returned resp is for a deduplicated concurrent caller (and is never idem-cached).
|
|
130
222
|
res.write(`data: ${JSON.stringify({ type: "done", result: { status: "failed", errorMessage: conflict.error, activeTaskId: conflict.activeTaskId } })}\n\n`);
|
|
131
223
|
return { status: 409, body: conflict };
|
|
132
224
|
}
|
|
133
225
|
durableTaskId = tid;
|
|
134
226
|
}
|
|
227
|
+
// [900]①/[906]③ detach 车道逐事件落账:sync 腿历史上只 createRun+setTerminal(events 账本恒空,
|
|
228
|
+
// detach 后壳只能轮询终局)。opt-in 车道把 for-await 的每个事件喂给与 bg 腿(runs.ts)共享的
|
|
229
|
+
// LedgerSink(src/trace/ledger-sink.ts —— 单一 switch,落账 type/data 形状与 GET /v1/runs/:id/events
|
|
230
|
+
// 的 bg 产出完全一致),detach 后壳换道 events tail 全帧续收。非 detach 车道恒 undefined = 零改动
|
|
231
|
+
// (不落账,byte-compat)。reasoning gate 与 bg 腿同一配置源(config.traceThinking / TRACE_THINKING)。
|
|
232
|
+
// 本腿账面差异(如实;[1840]§五车后已缩两项):suggestions 不落(本腿不调 stream.suggestions)、
|
|
233
|
+
// elicitation/question/approval 帧走 live emitAsk 面不落账、stream-open inbox drain 与 observer 臂
|
|
234
|
+
// 的 task_notification 走 wire/durable-inbox 既有链(post-detach 自动 park 进 inbox,下一腿 drain
|
|
235
|
+
// 补投)——不在本账本。已对齐:model_usage(E8 三件,下方 register/turn_end delta/终局 attach)、
|
|
236
|
+
// file_link(live+durable 双写,emit 回调处)。
|
|
135
237
|
let ledgerSink;
|
|
238
|
+
// [900]①: a terminal/park event already landed in the ledger — the failure backstops below must
|
|
239
|
+
// never write `failed` over it (bg 腿 `reached` 同款纪律).
|
|
136
240
|
let ledgerTerminal = false;
|
|
241
|
+
// model_usage 双腿对齐([1840]§五,W9 同根):detach 账本腿补上 bg 腿的 E8 三件(register /
|
|
242
|
+
// turn_end delta 落账 / 终局 SUM 进 stats.modelUsage)。归因键=**sessionId**,不是 durableTaskId:
|
|
243
|
+
// core 的 taskId mint = `spec.taskId ?? sessionId`(dist 亲读 runtask.js:773),本腿 resolveSpec
|
|
244
|
+
// 不设 spec.taskId ⇒ tracer 的 brain.call e.taskId = sessionId。账本键仍是 durableTaskId(两域
|
|
245
|
+
// 分家——attach 时 getEvents 按账本键包一层)。非 detach 车道照旧不登记(下方 E8 INTENTIONAL
|
|
246
|
+
// 排除注释仍准确——那腿无账本消费者);2107 行的「账面差异」清单相应少了 model_usage 一项。
|
|
247
|
+
// (usageKey 声明在外层——leak-fence clear 在外层 finally,须覆盖 throw 路径。)
|
|
137
248
|
if (detachOnDisconnect && durableTaskId && deps.runStore) {
|
|
138
249
|
const rs = deps.runStore;
|
|
139
250
|
const tid = durableTaskId;
|
|
@@ -143,16 +254,20 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
143
254
|
deps.modelUsage?.register(usageKey);
|
|
144
255
|
}
|
|
145
256
|
if (deps.checkpointStore && prepared.spec.sessionId) {
|
|
257
|
+
// putCtx AFTER the claim (resumable). If this throws, the claim row self-heals: heartbeat hasn't
|
|
258
|
+
// started, so reapStale flips it failed + releases task_active after runStaleSec.
|
|
146
259
|
await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
|
|
147
260
|
}
|
|
148
261
|
if (durableTaskId && deps.runStore) {
|
|
149
262
|
const rs = deps.runStore;
|
|
150
263
|
const tid = durableTaskId;
|
|
151
|
-
const owner = principal ?? null;
|
|
264
|
+
const owner = principal ?? null; // run created with this owner — pass it as the single-DB null-safe heartbeat guard
|
|
152
265
|
const abortFromVerb = () => {
|
|
153
|
-
cancelledViaVerb.add(tid);
|
|
266
|
+
cancelledViaVerb.add(tid); // H2: label the settle (errorCode "cancelled"), see the set's decl
|
|
154
267
|
ac.abort();
|
|
155
268
|
};
|
|
269
|
+
// [1.207 codex H1] heartbeat ALSO polls the durable cancel flag (bg-leg parity, runs.ts) — a
|
|
270
|
+
// cancel landing on ANOTHER replica only sets the flag, and this leg previously had no consumer.
|
|
156
271
|
durableHeartbeat = setInterval(() => {
|
|
157
272
|
void rs.heartbeat(tid, owner).catch(() => undefined);
|
|
158
273
|
if (!ac.signal.aborted) {
|
|
@@ -161,14 +276,36 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
161
276
|
}
|
|
162
277
|
}, HEARTBEAT_MS);
|
|
163
278
|
durableHeartbeat.unref?.();
|
|
279
|
+
// [888]① interactive live-run cancel: register this leg's abort controller under the SAME
|
|
280
|
+
// in-flight map the bg/resume legs use — POST /v1/runs/:id/cancel's fast path (`inflightRuns
|
|
281
|
+
// .get(taskId)?.abort()`) previously only ever hit bg legs, so cancelling an interactive run
|
|
282
|
+
// flipped the durable flag that NOTHING on this leg consumed (release deferred to reapStale).
|
|
283
|
+
// Abort rides the leg's existing disconnect-kill chain (spec.signal), which settles the row.
|
|
164
284
|
inflightRuns.set(tid, ac);
|
|
285
|
+
// [1.207 codex H1] registration-window catch-up: a cancel that landed between createRun and this
|
|
286
|
+
// point (the awaited putCtx window) flipped the flag while the fast path found no entry — check
|
|
287
|
+
// once so it is honored now, not at the first heartbeat.
|
|
165
288
|
void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !ac.signal.aborted)
|
|
166
289
|
abortFromVerb(); }).catch(() => undefined);
|
|
167
290
|
}
|
|
291
|
+
// MF-Fleet: publish this run's row transitions to the fleet bus (a tracked/durable run becomes a
|
|
292
|
+
// fleet row; turn_end accrues tokens; core `task_progress` adds subagent child rows; terminal removes
|
|
293
|
+
// it). No-op for an ephemeral sync run (no durableTaskId) or when no fleetBus is wired.
|
|
294
|
+
// [1.211 codex L] fleet 装配纳入清理保护:heartbeat/inflightRuns 注册在上面已发生,而外层
|
|
295
|
+
// try/finally 从更后面才开始——fleetRunPublisher/onStart 若在这窗内 throw,计时器与 cancel 句柄
|
|
296
|
+
// 无人清理(SSE 头已发,请求以 500 帧终止但资源漏)。let+窄 try/catch 清理后 rethrow 补上这段。
|
|
168
297
|
let fleetPub;
|
|
169
298
|
try {
|
|
170
299
|
fleetPub = durableTaskId
|
|
171
|
-
|
|
300
|
+
// core 1.154 nested-subagent tree: rootTaskId = the run's CANONICAL core taskId (`spec.taskId ?? sessionId`).
|
|
301
|
+
// The sync leg does NOT set spec.taskId, so canonical == sessionId — a forwarded subagent whose parentTaskId
|
|
302
|
+
// equals it nests under the run; deeper subagents nest under their parent subagent's row.
|
|
303
|
+
// 🔒 Scope the fleet row with the SAME identity GET /v1/fleet/stream FILTERS by — `gatedPrincipal`
|
|
304
|
+
// (crypto-verified on a direct door), NOT the cost-attribution `principal` (= the spoofable header).
|
|
305
|
+
// On a direct door the two diverge: a spoofed header would tag the row under a victim's scope (cross-
|
|
306
|
+
// tenant leak) or under "default" while the verified owner's stream filters it OUT (the row vanishes
|
|
307
|
+
// from its real owner's view). Tenant-isolation parity with the stream filter (TOC review #6).
|
|
308
|
+
? fleetRunPublisher(deps.fleetBus, { runId: durableTaskId, scope: gatedPrincipal(req, deps.config) ?? "default", rootTaskId: prepared.spec.sessionId, ...fleetRunLabels(prepared.spec.objective) }) // BC-1: name = short objective preview (description = live-activity, set by the publisher onEvent tool_start)
|
|
172
309
|
: undefined;
|
|
173
310
|
}
|
|
174
311
|
catch (e) {
|
|
@@ -176,41 +313,72 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
176
313
|
clearInterval(durableHeartbeat);
|
|
177
314
|
if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
|
|
178
315
|
inflightRuns.delete(durableTaskId);
|
|
179
|
-
cancelledViaVerb.delete(durableTaskId);
|
|
316
|
+
cancelledViaVerb.delete(durableTaskId); // [1.211 codex L] 标记集同窗清理
|
|
180
317
|
}
|
|
181
318
|
throw e;
|
|
182
319
|
}
|
|
320
|
+
// C (TOC adversarial review): settle the fleet row EXACTLY once. A plain Error thrown mid-stream
|
|
321
|
+
// (not done/CheckpointError/cancel) escapes the loop before any onTerminal fires → the row leaks as a
|
|
322
|
+
// stale "running" forever. Route every onTerminal through this guard so the finally below can settle a
|
|
323
|
+
// row no terminal branch reached, without ever double-settling one that did.
|
|
183
324
|
let fleetSettled = false;
|
|
184
325
|
const settleFleet = (status, residuals) => {
|
|
185
326
|
if (fleetSettled)
|
|
186
327
|
return;
|
|
187
328
|
fleetSettled = true;
|
|
188
|
-
fleetPub?.onTerminal(status, residuals);
|
|
329
|
+
fleetPub?.onTerminal(status, residuals); // [1839]①/(a):真终态帧带残局键(publisher 侧对 parked 再挡一层)
|
|
189
330
|
};
|
|
331
|
+
// 1.207: THIS leg's live TaskStream (assigned inside streamBody) — held here so the outer finally can
|
|
332
|
+
// identity-guard its deregistration (only delete the handle WE registered; see the finally).
|
|
190
333
|
let liveStreamRef;
|
|
334
|
+
// design/122 ③: this leg's subagent-handle unregister thunks — run at leg end (finally), not on settle.
|
|
191
335
|
const subagentHandleEvictions = [];
|
|
336
|
+
// false once this leg settles — a bg subagent/bash completion after that has no live
|
|
337
|
+
// harness (idle) → the observer parks it in the durable inbox (see fwdInternals below).
|
|
192
338
|
let syncLegLive = true;
|
|
339
|
+
// option A: taskNotificationStreamKey (域:task_id:status[#seq], core [784]④a + codex F2 域二分)
|
|
340
|
+
// keys this leg already delivered (turn-open inbox drain, stream branch OR observer) — each arm
|
|
341
|
+
// skips keys another arm already carried, so a notification isn't double-written to the SSE (the
|
|
342
|
+
// turn-TAIL reap notification, which never reaches the stream, is the one the observer catches;
|
|
343
|
+
// core's pend-drain replay of a double-parked idle arrival is the one the drain-registration
|
|
344
|
+
// catches — core [784]④b). codex F1: NotifiedKeys (Map keyed on write outcome) for three-leg
|
|
345
|
+
// parity — this leg's writes are all synchronous res.write (no async failure window), so every
|
|
346
|
+
// claim registers resolved(true) and the has/skip semantics are unchanged here.
|
|
193
347
|
const syncNotifiedKeys = new NotifiedKeys();
|
|
194
348
|
try {
|
|
195
|
-
fleetPub?.onStart();
|
|
349
|
+
fleetPub?.onStart(); // [1.211 codex L] 同窗保护:subscriber throw 不再漏计时器/句柄
|
|
196
350
|
}
|
|
197
351
|
catch (e) {
|
|
198
352
|
if (durableHeartbeat)
|
|
199
353
|
clearInterval(durableHeartbeat);
|
|
200
354
|
if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
|
|
201
355
|
inflightRuns.delete(durableTaskId);
|
|
202
|
-
cancelledViaVerb.delete(durableTaskId);
|
|
356
|
+
cancelledViaVerb.delete(durableTaskId); // [1.211 codex L] 标记集同窗清理
|
|
203
357
|
}
|
|
204
|
-
settleFleet("failed");
|
|
358
|
+
settleFleet("failed"); // [1.211 codex M2] publishTask 先写行再 emit——subscriber throw 时行已在,不清即永久 running 幽灵行
|
|
205
359
|
throw e;
|
|
206
360
|
}
|
|
361
|
+
// R8 rewind handle is now the X-Task-Id RESPONSE HEADER (set with earlyDurableTid above), not an
|
|
362
|
+
// out-of-contract `run_started` data frame — the shell reads it from the response headers even on an early
|
|
363
|
+
// disconnect, and the SDK's CLOSED AgentEvent stream stays unpolluted (workflow re-review #2 caught the drift).
|
|
207
364
|
try {
|
|
208
365
|
const streamBody = async () => {
|
|
209
366
|
await withPrincipal(principal, async () => {
|
|
367
|
+
// core 1.154 nested-subagent live tree: opt into the forward sink — a subagent's task_progress
|
|
368
|
+
// lives in its ISOLATED child stream (never on THIS top stream; core's only emitter is
|
|
369
|
+
// parentToolCallId-gated), so this sink is the ONLY path a tick reaches the top-run consumers.
|
|
370
|
+
// 1.78 (review HIGH): fan it out to the LIVE SSE too (shared whitelist+redact builder, carrying
|
|
371
|
+
// the E2 identity the shell binds concurrent ticks by) — previously it fed ONLY the fleet bus,
|
|
372
|
+
// so the conversation lane never saw a real tick and the in-loop task_progress branch below is
|
|
373
|
+
// forward-compat only (dead against core ≤1.208). core swallow-guards a throwing sink.
|
|
210
374
|
const fwdInternals = {
|
|
211
375
|
onForwardEvent: (e) => {
|
|
212
376
|
fleetPub?.onForwardEvent(e);
|
|
377
|
+
// [900]① detach 车道:子代理 forward 事件(tool_start/end + task_progress)也落账 —— bg 腿
|
|
378
|
+
// 同姿势(共享 sink 的 fire-and-forget 门,child deltas 有意不逐块落,见 LedgerSink.onForwardEvent)。
|
|
213
379
|
ledgerSink?.onForwardEvent(e);
|
|
380
|
+
// S2 live tail(core 1.370 bgAgentId;bg 腿 twin 在 runs.ts):句柄分发在 res 存活判之前
|
|
381
|
+
// ——主流断了 tail 订阅者照收(tail 是独立连接)。
|
|
214
382
|
{
|
|
215
383
|
const bg = e.bgAgentId;
|
|
216
384
|
if (bg !== undefined && defaultSubagentTailBus.hasSubscribers(bg)) {
|
|
@@ -226,36 +394,73 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
226
394
|
res.write(`data: ${JSON.stringify({ type: "task_progress", ...taskProgressEventData(e) })}\n\n`);
|
|
227
395
|
}
|
|
228
396
|
else if (t === "tool_start") {
|
|
397
|
+
// C1 (core 1.219, spec.forwardSubagentEvents): a delegated child's live CONTENT events now
|
|
398
|
+
// ride the forward sink (each stamped parentToolCallId — the shell nests them under the
|
|
399
|
+
// delegation card). Echo through the SAME shared whitelist+redact builders as the top
|
|
400
|
+
// stream (§E1 — forwarded events are UNTRUSTED RAW; redaction is ours at the wire).
|
|
229
401
|
const tsv = e;
|
|
230
402
|
res.write(`data: ${JSON.stringify({ type: "tool_start", ...toolStartEventData(tsv) })}\n\n`);
|
|
231
403
|
}
|
|
232
404
|
else if (t === "tool_end") {
|
|
233
405
|
const te = e;
|
|
234
|
-
res.write(`data: ${JSON.stringify({ type: "tool_end", ...toolEndEventData(te) })}\n\n`);
|
|
406
|
+
res.write(`data: ${JSON.stringify({ type: "tool_end", ...toolEndEventData(te) })}\n\n`); // label 现由 builder 自带,外拼冗余已清
|
|
235
407
|
}
|
|
236
408
|
else if (t === "text_delta") {
|
|
409
|
+
// Child text is plain model output (same trust class as the top stream's text_delta, which
|
|
410
|
+
// rides verbatim); parentToolCallId attributes it. Whitelist the three fields explicitly.
|
|
237
411
|
const td = e;
|
|
238
412
|
res.write(`data: ${JSON.stringify({ type: "text_delta", delta: td.delta, ...(td.eventId ? { eventId: td.eventId } : {}), ...(td.parentToolCallId ? { parentToolCallId: td.parentToolCallId } : {}) })}\n\n`);
|
|
239
413
|
}
|
|
240
414
|
else if (t === "reasoning_delta") {
|
|
415
|
+
// §E1 parity with the top stream's live CoT: redactSecrets before echo.
|
|
241
416
|
const rd = e;
|
|
242
417
|
res.write(`data: ${JSON.stringify({ type: "reasoning_delta", delta: redactSecrets(rd.delta), ...(rd.eventId ? { eventId: rd.eventId } : {}), ...(rd.parentToolCallId ? { parentToolCallId: rd.parentToolCallId } : {}) })}\n\n`);
|
|
243
418
|
}
|
|
244
419
|
},
|
|
420
|
+
// background-completion observer — flip the child's fleet
|
|
421
|
+
// row (task_progress never emits a terminal tick) + park an idle-arrival in the durable
|
|
422
|
+
// inbox (workflow-type skipped: own durable chain). Rationale in runs.ts (bg-leg twin).
|
|
423
|
+
// core 1.283: `seq` = the per-stop-cycle counter (stop→wake→stop mints 1,2,…) — it MUST
|
|
424
|
+
// ride into the dedup key below (core [784]④a) and into the parked inbox entry.
|
|
425
|
+
// 白名单排查(2026-07-24,「彻查 as {} 连环坑」):手写窄类型换 core 真实类型,理由同 runs.ts twin。
|
|
245
426
|
onTaskNotification: (n) => {
|
|
246
427
|
if (n.task_type === "workflow")
|
|
247
428
|
return;
|
|
429
|
+
// S2 复审 #3:中途终态信号——task_settled 帧让订阅者不再 heartbeat 永挂(路由收帧即收流)。
|
|
248
430
|
if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
|
|
249
431
|
defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
|
|
250
432
|
}
|
|
433
|
+
// id-domain alias: flip by payload.sessionId (= the tick's uuid domain), fallback task_id.
|
|
251
434
|
const hadRow = fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false;
|
|
435
|
+
// 🔴 对抗评审 2026-07-11(HIGH,「下一 turn 0 帧」最强根因):the teardown WINDOW — after the
|
|
436
|
+
// client disconnects (the user killed the turn → ac.abort → core reaps the bg child → THIS
|
|
437
|
+
// notification fires) but BEFORE the outer finally flips `syncLegLive=false` (several awaits sit
|
|
438
|
+
// between the for-await break and the finally). In that window the old predicate neither parked
|
|
439
|
+
// (legLive still true) nor wrote (socket dead) — the notification vanished with no inbox copy and
|
|
440
|
+
// no frame. Fold the dead-socket state into the park predicate: the leg is only DELIVERABLE when
|
|
441
|
+
// it is live AND the socket can still be written. 🔴 `res.destroyed` is the CLIENT-disconnect
|
|
442
|
+
// signal (`writableEnded` only flips when WE call res.end()) — the real-HTTP retest caught the
|
|
443
|
+
// writableEnded-only predicate still writing into the dead socket.
|
|
252
444
|
const deliverable = syncLegLive && !res.writableEnded && !res.destroyed;
|
|
253
445
|
const parked = !deliverable && Boolean(deps.workflowCompletionInbox && prepared.spec.sessionId);
|
|
446
|
+
// diagnosability (rationale in runs.ts twin).
|
|
254
447
|
deps.logger?.info?.("task_notification_observed", { route: "sync-stream", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: syncLegLive, parkedDurable: parked });
|
|
255
448
|
if (parked) {
|
|
449
|
+
// 对抗评审 2026-07-11(MED):the fire-and-forget swallow hid EVERY enqueue failure (SQL blip,
|
|
450
|
+
// fence race) while the diag line above had already printed parkedDurable:true — a false park✓
|
|
451
|
+
// fingerprint. Log the failure; behavior (never fault the callback) unchanged.
|
|
256
452
|
void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(prepared.spec.sessionId, askOwner, n, Date.now(), durableTaskId)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "sync-stream", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
|
|
257
453
|
}
|
|
258
454
|
else if (deliverable) {
|
|
455
|
+
// option A (turn-TAIL killed receipt must be IMMEDIATE): a teardown-time reap
|
|
456
|
+
// notification fires while the SSE is still open but past the stream's delivery window —
|
|
457
|
+
// write the display frame directly, skipping keys the stream already carried (mid-turn
|
|
458
|
+
// dedup; the shell's dedup stays the outer belt). core [784]④a: the key folds `seq` in
|
|
459
|
+
// (shared taskNotificationStreamKey) — the old seq-less `${task_id}:${status}` shape
|
|
460
|
+
// REVERSE-swallowed a second stop-cycle's same-status frame on this arm (stop→wake→stop:
|
|
461
|
+
// the seq=2 completed frame collided with cycle 1's key and the user never saw it).
|
|
462
|
+
// codex F1: res.write is a synchronous enqueue (no async failure window — a dead socket
|
|
463
|
+
// is already folded into `deliverable` above), so the claim registers resolved(true).
|
|
259
464
|
const key = taskNotificationStreamKey(n);
|
|
260
465
|
if (syncNotifiedKeys.get(key) === undefined) {
|
|
261
466
|
syncNotifiedKeys.set(key, Promise.resolve(true));
|
|
@@ -263,6 +468,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
263
468
|
}
|
|
264
469
|
}
|
|
265
470
|
},
|
|
471
|
+
// C2 (core 1.219) + design/122 (core 1.225 ③): register each sync delegation's steer handle
|
|
472
|
+
// under this run's durable taskId (the X-Task-Id the shell holds). NOT evicted on settle —
|
|
473
|
+
// resume is only legal after settle; evicted at leg end via the collected identity-guarded
|
|
474
|
+
// thunks (outer finally), so a settled child stays steer-409/resume-able while the run lives.
|
|
266
475
|
...(deps.subagentSteerRegistry && durableTaskId
|
|
267
476
|
? {
|
|
268
477
|
onSubagentSpawn: (handle) => {
|
|
@@ -271,6 +480,15 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
271
480
|
}
|
|
272
481
|
: {}),
|
|
273
482
|
};
|
|
483
|
+
// R8 (CC parity, interactive rewind): the sync `tasks.stream` path (the REPL's lane) must ALSO
|
|
484
|
+
// leave a per-prompt rewind anchor, else an interactive-turn rewind 404s (the bg + resume legs capture
|
|
485
|
+
// it, this one didn't). Buffer the committed USER-message entryId on the HOISTED `userMsgEntryId` (so
|
|
486
|
+
// the disconnect branch can anchor too); the done branch anchors here, the disconnect branch below.
|
|
487
|
+
// core 1.207: hold the LIVE TaskStream handle + register it (keyed by durableTaskId = the
|
|
488
|
+
// X-Task-Id the shell already holds) so the replica-local verbs — POST /v1/runs/:id/detach
|
|
489
|
+
// (mid-flight ctrl+b) and, as a natural consequence, steer/compact — now reach a mid-turn
|
|
490
|
+
// INTERACTIVE run too (previously only the bg + resume legs registered). Deregistered in the
|
|
491
|
+
// outer finally (with the fleet settle), so an early throw can't leak a stale handle.
|
|
274
492
|
const liveStream = deps.runner.runTaskStream({ ...prepared.spec, signal: ac.signal }, undefined, fwdInternals);
|
|
275
493
|
liveStreamRef = liveStream;
|
|
276
494
|
if (durableTaskId)
|
|
@@ -281,19 +499,45 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
281
499
|
if (ev.type === "message_committed" && ev.role === "user")
|
|
282
500
|
userMsgEntryId = ev.entryId;
|
|
283
501
|
if (ev.type === "done") {
|
|
502
|
+
// Never stream the durable-resume capability token to the client: a
|
|
503
|
+
// durable suspend's done event carries it. Strip it from BOTH the live write and the cached
|
|
504
|
+
// body (handed to a concurrent deduplicated caller below). No-op for a normal result.
|
|
284
505
|
finalResult = stripCheckpointToken(ev.result);
|
|
506
|
+
// R8 (CC parity, interactive rewind): on the sync leg core sets result.taskId = sessionId
|
|
507
|
+
// (spec.taskId is left unset so a forwarded subagent nests under the run — server.ts above), which
|
|
508
|
+
// is NOT unique per prompt → it can't be a per-turn rewind handle (every turn would collide). Re-
|
|
509
|
+
// stamp the WIRE result.taskId to the unique durable run id (uuidv7, == the bg path's result.taskId
|
|
510
|
+
// → consistent across paths) so the shell gets a unique per-prompt handle. Internal canonical
|
|
511
|
+
// (fleet/subagent nesting) keeps sessionId — only the external done-frame value changes. Durable-run
|
|
512
|
+
// only (ephemeral has no anchor store ⇒ no rewind anyway), so the fallback keeps sessionId.
|
|
285
513
|
if (durableTaskId && finalResult)
|
|
286
514
|
finalResult = { ...finalResult, taskId: durableTaskId };
|
|
515
|
+
// [1.207 codex H2] cancel-verb abort label: core's external-signal abort yields
|
|
516
|
+
// failed WITHOUT an errorCode — project `cancelled` onto the wire+persisted form so
|
|
517
|
+
// /cancel's outcome is uniform (first-writer-wins would pin the unlabeled form). Only
|
|
518
|
+
// an abort-shaped FAILED result is labeled — a completion that raced the abort stands.
|
|
287
519
|
if (durableTaskId && cancelledViaVerb.has(durableTaskId) && finalResult?.status === "failed" && !finalResult.errorCode) {
|
|
288
520
|
finalResult = { ...finalResult, errorCode: "cancelled" };
|
|
289
521
|
}
|
|
290
522
|
finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
|
|
523
|
+
// Anchor this interactive prompt (taskId → user-message entryId) keyed by the SAME unique id the
|
|
524
|
+
// shell now holds (done.result.taskId == durableTaskId). entryId came straight from
|
|
525
|
+
// `message_committed{role:"user"}` (no getLeafId). Best-effort + once-guarded — a capture failure
|
|
526
|
+
// only makes THIS turn non-rewindable, never faults the run; the disconnect branch shares the guard.
|
|
291
527
|
putRewindAnchor();
|
|
292
|
-
settleFleet(finalResult?.status ?? "completed", fleetRunResiduals(finalResult));
|
|
528
|
+
settleFleet(finalResult?.status ?? "completed", fleetRunResiduals(finalResult)); // [1839]①/(a):终态残局键
|
|
529
|
+
// [900]① detach 车道:done 也落账(flush 攒批 + suspended/needs_review/done 投影,bg 词汇;
|
|
530
|
+
// finalResult 已 stripCheckpointToken + taskId 重印 + cancel 标注 —— 账本形 == wire 形)。
|
|
531
|
+
// 放在 res.write 之前:壳收到 wire done 后立刻换道 events tail 也必见终局帧。既有
|
|
532
|
+
// finalize/settleFleet/res.write/setTerminal 顺序不动(落账是旁路)。
|
|
293
533
|
if (ledgerSink && finalResult) {
|
|
534
|
+
// E8 双腿对齐:终局 flush 最后一拍 delta + SUM 全账本 model_usage 进 stats.modelUsage
|
|
535
|
+
// (bg 腿 withModelUsage 同形)。⚠️ 键两域分家:drain 键=usageKey(sessionId,core 归因),
|
|
536
|
+
// 账本读键=durableTaskId——attachModelUsage 的 taskId 双用途在此不成立,getEvents 包一层。
|
|
294
537
|
if (usageKey && deps.modelUsage && deps.runStore && durableTaskId) {
|
|
295
538
|
const rs2 = deps.runStore;
|
|
296
539
|
const tid2 = durableTaskId;
|
|
540
|
+
// attach 失败(SUM 读瞬断)不许炸掉 onDone——终局帧比 modelUsage 汇总重要,回落原 result。
|
|
297
541
|
const fr = finalResult;
|
|
298
542
|
finalResult = await attachModelUsage(fr, { append: ledgerSink.append, getEvents: (_id, after) => rs2.getEvents(tid2, after), modelUsage: deps.modelUsage, taskId: usageKey }).catch(() => fr);
|
|
299
543
|
}
|
|
@@ -303,54 +547,116 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
303
547
|
res.write(`data: ${JSON.stringify({ ...ev, result: finalResult })}\n\n`);
|
|
304
548
|
continue;
|
|
305
549
|
}
|
|
550
|
+
// [900]① detach 车道逐事件落账(await 保序 —— 与 bg 腿同代价姿势;message_committed/未知型
|
|
551
|
+
// sink 内部 no-op)。非 detach 车道 ledgerSink 恒 undefined = 零改动。
|
|
306
552
|
if (ledgerSink) {
|
|
307
553
|
await ledgerSink.onEvent(ev);
|
|
554
|
+
// E8 双腿对齐:turn 边界 drain per-model delta 落账(bg 腿 flushModelUsage 同位同形)。
|
|
308
555
|
if (ev.type === "turn_end" && usageKey)
|
|
309
556
|
await appendModelUsageDelta(ledgerSink.append, deps.modelUsage, usageKey);
|
|
310
557
|
}
|
|
311
|
-
fleetPub?.onEvent(ev);
|
|
558
|
+
fleetPub?.onEvent(ev); // MF-Fleet: turn_end → tokens; task_progress → subagent child row
|
|
559
|
+
// S4 P0-a: count brain-liveness phases on the sync leg too. The bg/resume legs count at
|
|
560
|
+
// their append sites.
|
|
312
561
|
if (ev.type === "status")
|
|
313
562
|
deps.metrics?.inc("brain_retry_total", { phase: String(ev.phase) });
|
|
563
|
+
// E1 (shell-host contract): tool_end.output AND (core 1.203) tool_end.structured are
|
|
564
|
+
// UNTRUSTED RAW from core (file/shell/API bodies; a structured card carries `originalFile`) —
|
|
565
|
+
// redact at the live wire boundary before echo (core only SIZE-bounds; redaction is ours, same
|
|
566
|
+
// contract as the durable path). 🔒 EXPLICIT WHITELIST, not `{...ev}` (the 1.72 task_progress
|
|
567
|
+
// lesson, re-proven by `structured`: a spread forwards a future core field RAW — 1.203 landing
|
|
568
|
+
// meant `structured` rode this leg unredacted until this build). Known-safe fields verbatim:
|
|
569
|
+
// ids/name/label/isError/truncated + E2 identity. NB the old guard also skipped redaction when
|
|
570
|
+
// `output` was undefined (→ raw catch-all) — the whitelist path now covers every tool_end.
|
|
314
571
|
if (ev.type === "status") {
|
|
572
|
+
// TaskEvent 三腿审查(steering_injected 跟进批):`status`(BrainStatus.detail,UNTRUSTED —
|
|
573
|
+
// 可能回显 provider 的 rate-limit/retry 报文)此前落裸 catch-all 未脱敏,与 durable 两腿
|
|
574
|
+
// (ledger-sink.onEvent + resume switch)共用的 brainStatusEventData 脱敏不一致——同类
|
|
575
|
+
// 「留在 catch-all」教训(1.72 task_progress),现挪显式分支,三腿同门同形。
|
|
315
576
|
res.write(`data: ${JSON.stringify({ type: "status", ...brainStatusEventData(ev) })}\n\n`);
|
|
316
577
|
}
|
|
317
578
|
else if (ev.type === "tool_end") {
|
|
318
579
|
const te = ev;
|
|
319
|
-
res.write(`data: ${JSON.stringify({ type: "tool_end", ...toolEndEventData(te) })}\n\n`);
|
|
580
|
+
res.write(`data: ${JSON.stringify({ type: "tool_end", ...toolEndEventData(te) })}\n\n`); // label 现由 builder 自带,外拼冗余已清
|
|
320
581
|
}
|
|
321
582
|
else if (ev.type === "tool_start") {
|
|
583
|
+
// 🔒 E1 parity (TOC adversarial review): redact tool_start.args on the LIVE wire too — the model's
|
|
584
|
+
// tool-call inputs can echo a secret it read (e.g. a value from a redacted tool_end fed into the
|
|
585
|
+
// next call). Whitelist for the same reason as tool_end (shared durable builder = one shape).
|
|
322
586
|
const tsv = ev;
|
|
323
587
|
res.write(`data: ${JSON.stringify({ type: "tool_start", ...toolStartEventData(tsv) })}\n\n`);
|
|
324
588
|
}
|
|
325
589
|
else if (ev.type === "task_notification") {
|
|
590
|
+
// core 1.202 (design/115 P2): a background task (workflow) finished while THIS harness was live —
|
|
591
|
+
// core already injected the <task-notification> XML into the model in-process; this is the display
|
|
592
|
+
// projection for the shell. Whitelist + redact via the shared builder (summary/result are UNTRUSTED
|
|
593
|
+
// free text). The shell dedups against the inbox-drain `workflow_complete` lane by run id.
|
|
594
|
+
// core [784]④b(跨层双泊车): a notification that arrived while the previous leg was idle is
|
|
595
|
+
// parked TWICE — core pends it in-Runner (pendingSessionNotifications) AND our sync observer
|
|
596
|
+
// parks it in the durable inbox. On this turn-open, our inbox drain (below the streamBody def)
|
|
597
|
+
// already wrote the frame and REGISTERED its key; core's pend-drain replays the same payload as
|
|
598
|
+
// a stream `task_notification` event (runtask.js queue.push) — skip it here instead of writing
|
|
599
|
+
// the wire twice. Same check also folds the mid-turn observer-then-stream echo. A frame whose
|
|
600
|
+
// key is NOT yet on this stream writes exactly as before (the first copy is never eaten).
|
|
601
|
+
// codex F1/F2: same await-the-claim shape as the bg/resume twins (on THIS leg every claim
|
|
602
|
+
// is resolved(true) — sync writes — so the await is immediate); task_type rides the key.
|
|
326
603
|
const tnv = ev.notification;
|
|
327
604
|
const tnvKey = tnv?.task_id ? taskNotificationStreamKey({ task_type: tnv.task_type, task_id: tnv.task_id, status: String(tnv.status), seq: tnv.seq }) : undefined;
|
|
328
605
|
const tnvPrior = tnvKey !== undefined ? syncNotifiedKeys.get(tnvKey) : undefined;
|
|
329
606
|
if (tnvPrior === undefined || !(await tnvPrior)) {
|
|
330
607
|
if (tnvKey !== undefined)
|
|
331
|
-
syncNotifiedKeys.set(tnvKey, Promise.resolve(true));
|
|
608
|
+
syncNotifiedKeys.set(tnvKey, Promise.resolve(true)); // stream delivered this key
|
|
332
609
|
res.write(`data: ${JSON.stringify({ type: "task_notification", ...taskNotificationEventData(ev) })}\n\n`);
|
|
333
610
|
}
|
|
334
611
|
}
|
|
335
612
|
else if (ev.type === "reasoning_delta" && typeof ev.delta === "string") {
|
|
613
|
+
// 🔒 E1 parity (TOC adversarial review): redact the CoT on the LIVE wire too. The agent's
|
|
614
|
+
// reasoning can echo a secret it read (core [R57]: CoT may carry reasoned-about secrets). The
|
|
615
|
+
// DURABLE path redacts it (runs.ts → redactSecrets, gated by persistThinking/TRACE_THINKING);
|
|
616
|
+
// the live mirror MUST match. Keep STREAMING the (redacted) thinking so live CoT still renders.
|
|
617
|
+
// 🔒 EXPLICIT WHITELIST, not `{...ev}` (TaskEvent 三腿审查跟进批, 1.72 task_progress lesson
|
|
618
|
+
// re-proven here: a spread forwards an unknown/future field RAW; this arm was the one
|
|
619
|
+
// sibling branch that still spread — the FORWARDED twin below [t === "reasoning_delta"]
|
|
620
|
+
// already used the narrow form, this now matches it byte-for-byte).
|
|
336
621
|
res.write(`data: ${JSON.stringify({ type: "reasoning_delta", delta: redactSecrets(ev.delta), ...(ev.eventId ? { eventId: ev.eventId } : {}), ...(ev.parentToolCallId ? { parentToolCallId: ev.parentToolCallId } : {}) })}\n\n`);
|
|
337
622
|
}
|
|
338
623
|
else if (ev.type === "task_progress") {
|
|
624
|
+
// 🔒 E1 live-wire parity: explicit whitelist via the SHARED builder (UNTRUSTED
|
|
625
|
+
// `name` redacted; 1.78: E2 identity rides — the shell binds concurrent subagent ticks by
|
|
626
|
+
// eventId/parentToolCallId) — parity with the durable runs.ts + resume legs.
|
|
339
627
|
res.write(`data: ${JSON.stringify({ type: "task_progress", ...taskProgressEventData(ev) })}\n\n`);
|
|
340
628
|
}
|
|
341
629
|
else if (ev.type === "compacted") {
|
|
630
|
+
// core 1.219 (attachWorkingFiles default ON): `compacted` now carries `attachedFiles` — route it
|
|
631
|
+
// through the SHARED whitelist builder (paths redactSecrets'd) instead of the raw catch-all, so
|
|
632
|
+
// the live wire matches the durable legs' shape exactly (one builder, no drift).
|
|
342
633
|
res.write(`data: ${JSON.stringify({ type: "compacted", ...compactedEventData(ev) })}\n\n`);
|
|
343
634
|
}
|
|
344
635
|
else if (ev.type === "diagnostics") {
|
|
636
|
+
// core 1.220 (design/121): the shell's render source for CC's diagnostics row.
|
|
637
|
+
// Shared whitelist builder (§E1: language-server output UNTRUSTED RAW → redactSecrets).
|
|
345
638
|
res.write(`data: ${JSON.stringify({ type: "diagnostics", ...diagnosticsEventData(ev) })}\n\n`);
|
|
346
639
|
}
|
|
347
640
|
else if (ev.type === "steering_injected") {
|
|
641
|
+
// [1550]/[1552]: core's mid-run system-reminder injection — shared whitelist builder
|
|
642
|
+
// (§E1: preview is UNTRUSTED free text → redactSecrets). Previously fell through to the
|
|
643
|
+
// raw catch-all below (unredacted `{...ev}`) — moved to an explicit builder for the same
|
|
644
|
+
// reason every other content type left the catch-all (the 1.72 task_progress lesson).
|
|
348
645
|
res.write(`data: ${JSON.stringify({ type: "steering_injected", ...steeringInjectedEventData(ev) })}\n\n`);
|
|
349
646
|
}
|
|
350
647
|
else if (ev.type === "workspace_changed") {
|
|
648
|
+
// [1559]二 core 裁定:bash cd/EnterWorktree 移动任务逻辑 cwd 后一帧,给 shell UI 的路径
|
|
649
|
+
// 提示行。cwd 与已展示的工具 args 同信任级 → 共享白名单构造器(redactSecrets + bound)。
|
|
351
650
|
res.write(`data: ${JSON.stringify({ type: "workspace_changed", ...workspaceChangedEventData(ev) })}\n\n`);
|
|
352
651
|
}
|
|
353
652
|
else if (ev.type === "text_delta" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
|
|
653
|
+
// 裸 catch-all 白名单化收官(probe-wire-1 表征钉的根治半场):最后四个已知臂离开
|
|
654
|
+
// `JSON.stringify(ev)`。**零字节变化**白名单——已知键(含 E2 四 identity 键:live 现状
|
|
655
|
+
// 带全四键,消费端可能已依赖,与 durable builder 的「刻意排除」是两腿两策)逐一挑,
|
|
656
|
+
// 只挡 core 未来新键的裸穿透(structured 在 1.203 就真穿透过本腿)。text_delta 的 delta
|
|
657
|
+
// 不做内容变换:逐 token 片段上 redactSecrets 对跨 chunk 的秘密无效还破流式——durable
|
|
658
|
+
// 腿攒批后 redact 是那半的正解,两腿口径注在 ledger-sink.flush。catch-all(下方 else)
|
|
659
|
+
// 从此只剩**真未知臂**:L1 臂集门(core-keyset-guard ⑤)在 core 加臂时 tsc 红逼显式处置。
|
|
354
660
|
const e = ev;
|
|
355
661
|
const ident = {
|
|
356
662
|
...(e.eventId !== undefined ? { eventId: e.eventId } : {}),
|
|
@@ -373,35 +679,78 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
373
679
|
}
|
|
374
680
|
});
|
|
375
681
|
};
|
|
682
|
+
// E23 + §4④: wrap the synchronous stream in the per-run elicitation AND AskUserQuestion contexts (nested
|
|
683
|
+
// — each uses its own ALS). emit = a named SSE frame on THIS open connection; taskId keys each throttle
|
|
684
|
+
// (the durable run id, else a synthetic internal id); owner gates the respond (the VERIFIED principal —
|
|
685
|
+
// NOT the cost-attribution `principal`, which is the spoofable header and null on a direct door → would
|
|
686
|
+
// park owner=null and let any tenant answer); ac.signal cancels a parked ask on client disconnect. Each
|
|
687
|
+
// wrap is a no-op when its coordinator is off; the two share the identical id/owner/emit/signal.
|
|
376
688
|
const askTaskId = durableTaskId ?? uuidv7();
|
|
377
689
|
const askOwner = gatedPrincipal(req, deps.config) ?? null;
|
|
378
690
|
const emitAsk = (frame) => {
|
|
379
691
|
if (!res.writableEnded)
|
|
380
692
|
res.write(`event: ${frame.type}\ndata: ${JSON.stringify(frame)}\n\n`);
|
|
381
693
|
};
|
|
694
|
+
// P1 ①②: this new turn is a stream-open for the session — push any async-workflow completions that
|
|
695
|
+
// finished since (frames ride the SAME connection, before the run's own events; the shell dedups by
|
|
696
|
+
// runId). `askOwner` = the VERIFIED principal → only this caller's own completions are emitted.
|
|
697
|
+
// 🔒 THROW on a dead/ended stream instead of emitAsk's silent no-op (1.80 review: a disconnect
|
|
698
|
+
// between request-send and this drain made the two-phase ack a FALSE success — the entry was
|
|
699
|
+
// removed with no frame ever written; throwing keeps it pending for the next leg). Only THIS
|
|
700
|
+
// drain wrapper throws — the shared emitAsk (question/elicit frames) keeps its no-op semantics.
|
|
701
|
+
// 🔴 drain 案: write these as ANONYMOUS `data:` frames, NOT emitAsk's NAMED
|
|
702
|
+
// `event: <type>` form — the run's own live-leg task_notification frames on this SAME connection
|
|
703
|
+
// are anonymous (L1392/L1474), so a shell parsing the default message channel silently never saw
|
|
704
|
+
// a drained frame (the Monitor "park✓ drain✗" fingerprint). question/elicit stay on emitAsk
|
|
705
|
+
// (their own established named-event contract).
|
|
382
706
|
await emitPendingWorkflowCompletions(deps.workflowCompletionInbox, prepared.spec.sessionId, askOwner, (frame) => {
|
|
383
707
|
if (closed || res.writableEnded || res.destroyed)
|
|
384
708
|
throw new Error("stream ended before the completion frame was written");
|
|
709
|
+
// core [784]④b: register the drained frame's key on THIS stream — the same idle-arrival was
|
|
710
|
+
// ALSO parked core-side (pendingSessionNotifications), and its pend-drain replay lands on this
|
|
711
|
+
// very stream as a task_notification event; the loop's key check folds that second wire copy.
|
|
712
|
+
// codex F1: resolved(true) — the res.write below is synchronous (the dead-stream guard above
|
|
713
|
+
// already threw). codex F2: task_type (restored from the entry extras) rides into the key —
|
|
714
|
+
// an external notify's caller-chosen task_id must not pre-claim an internal task's key.
|
|
385
715
|
const f = frame;
|
|
386
716
|
if (f.type === "task_notification" && f.task_id)
|
|
387
717
|
syncNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), Promise.resolve(true));
|
|
388
718
|
res.write(`data: ${JSON.stringify(frame)}\n\n`);
|
|
389
|
-
},
|
|
719
|
+
},
|
|
720
|
+
// emit-target diagnostics — this drain rides the caller's OWN live SSE response.
|
|
721
|
+
{ route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
|
|
390
722
|
const withElicit = deps.elicitation
|
|
391
723
|
? () => deps.elicitation.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, streamBody)
|
|
392
724
|
: streamBody;
|
|
393
725
|
const withQuestion = () => deps.question
|
|
394
726
|
? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, withElicit)
|
|
395
727
|
: withElicit();
|
|
728
|
+
// [816]/[820]②: the live tool-approval context — SAME id/owner/emit/signal as question/elicit
|
|
729
|
+
// (nested ALS, each its own store). sessionId keys the "allow all edits this session" state.
|
|
730
|
+
// [1535] 正名:本 sync 腿仍是唯一 runWithContext 包裹,但不再是子代唯一到达路——下方
|
|
731
|
+
// spec.onAsk 的 bound 闭包经 core 继承链给了 bg/嵌套子代直达宿主流的路;真正无宿主流的
|
|
732
|
+
// 残余腿(durable-submit/headless resume)才落 "unavailable" → durable park / core deny。
|
|
733
|
+
// 🔒 approval 帧不用共享 emitAsk 的静默 no-op 语义:res 已结束时 no-op = 卡「假投递成功」,
|
|
734
|
+
// coordinator 等满 TTL 落 deny —— G1 "unavailable"→park 腿在真 wiring 里就不可达(cross-review
|
|
735
|
+
// 抓获)。这里 THROW,coordinator 的 emit-catch 按「无活人可达」返 "unavailable"(durable 部署
|
|
736
|
+
// park;非 durable 由 core fail-closed deny,行为不劣于旧 no-op)。仅 approval 帧收紧;question/
|
|
737
|
+
// elicit 的共享 emitAsk 保持 no-op(它们各有 TTL/abort 语义,另议)。
|
|
396
738
|
const emitApproval = (frame) => {
|
|
397
739
|
if (res.writableEnded || res.destroyed)
|
|
398
740
|
throw new Error("live stream ended — approval card undeliverable");
|
|
399
741
|
emitAsk(frame);
|
|
400
742
|
};
|
|
743
|
+
// [1535] server 半场([1539] 定谳断点①的修):同一 ctx 双装——ALS 包裹(宿主自身 ask 的
|
|
744
|
+
// 既有路)+ **spec.onAsk = boundAsk(ctx)**(core 1.294 继承链 parentConstraints 冻结
|
|
745
|
+
// `spec.onAsk ?? deps.onAsk` 逐 ancestor 传给每个委派子代:bg 子代 lane 无 ALS ctx 时不再
|
|
746
|
+
// 「unavailable/零帧 fail-closed deny」,权限卡带 sourceTaskId 浮到宿主本流)。suspendAsk
|
|
747
|
+
// 退位条件的「onAsk 在场」真值不变(deps.onAsk 恒 wire),durable 部署行为零漂移。
|
|
401
748
|
const approvalCtx = deps.toolApproval
|
|
402
749
|
? { taskId: askTaskId, owner: askOwner, emit: emitApproval, abortSignal: ac.signal, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) }
|
|
403
750
|
: undefined;
|
|
404
751
|
if (approvalCtx && deps.toolApproval) {
|
|
752
|
+
// [1546] HIGH-1:闭包只携身份——emit 经 broker 取「当前」活跃流(宿主重连的新 SSE 在
|
|
753
|
+
// runWithContext 注册时覆盖指针),internalsSnapshot 冻结的闭包不再携死 response/死 signal。
|
|
405
754
|
prepared.spec.onAsk = deps.toolApproval.boundAsk({
|
|
406
755
|
owner: askOwner,
|
|
407
756
|
taskId: askTaskId,
|
|
@@ -409,6 +758,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
409
758
|
});
|
|
410
759
|
}
|
|
411
760
|
const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
|
|
761
|
+
// SendUserFile:sync 腿 file_link 走 live out-of-band 帧面(emitAsk)。detach 车道([1840]§五
|
|
762
|
+
// file_link 修):**同落账**——断连后 live 面写在已死 socket 上被丢,壳换道 events tail 必须
|
|
763
|
+
// 能拿到文件卡(bg 腿本就 durable append,双腿对齐);live+durable 双写与 text/tool 帧的换道
|
|
764
|
+
// 语义同栏(壳按换道协议处理重放)。非 detach 车道 ledgerSink 恒 undefined = live-only 字节不变。
|
|
412
765
|
await (deps.sendUserFile
|
|
413
766
|
? deps.sendUserFile.runWithContext({
|
|
414
767
|
taskId: askTaskId,
|
|
@@ -416,33 +769,50 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
416
769
|
void (await emitAsk(f));
|
|
417
770
|
if (ledgerSink) {
|
|
418
771
|
const { type, ...rest } = f;
|
|
419
|
-
await ledgerSink.append(type, rest).catch(() => undefined);
|
|
772
|
+
await ledgerSink.append(type, rest).catch(() => undefined); // 账本瞬断不 fault 工具面(live 帧已出)
|
|
420
773
|
}
|
|
421
774
|
},
|
|
422
775
|
}, withApproval)
|
|
423
776
|
: withApproval());
|
|
777
|
+
// Drive the parked run row to its terminal/suspended state (releases or keeps task_active).
|
|
424
778
|
if (durableTaskId && deps.runStore) {
|
|
779
|
+
// R8 decouple defense-in-depth (review LOW): only KEEP the lock (setSuspended/needs_review) when
|
|
780
|
+
// checkpointStore exists — without it there's no resume ctx, so a suspend (unreachable per durableEnabled,
|
|
781
|
+
// but fail-safe matters) FALLS THROUGH to setTerminal which RELEASES the claim. Mirrors the sync path.
|
|
425
782
|
if (finalResult?.status === "suspended" && deps.checkpointStore)
|
|
426
783
|
await deps.runStore.setSuspended(durableTaskId);
|
|
427
784
|
else if (finalResult?.status === "needs_review" && deps.checkpointStore)
|
|
428
|
-
await deps.runStore.setNeedsReview(durableTaskId);
|
|
785
|
+
await deps.runStore.setNeedsReview(durableTaskId); // D-B: park a review pause, keep the lock (resumable via /plan_review)
|
|
429
786
|
else if (finalResult)
|
|
430
787
|
await deps.runStore.setTerminal(durableTaskId, finalResult.status, finalResult, finalResult.errorMessage ?? null);
|
|
431
788
|
else {
|
|
789
|
+
// No done event observed — the client disconnected (closed → break) and ac.abort() killed the
|
|
790
|
+
// run. Race guard: the task may have SUSPENDED right before the abort (checkpoint already
|
|
791
|
+
// persisted, its done event unread) — a pending checkpoint outlives the abort, so park the row
|
|
792
|
+
// suspended (session stays locked for the operator's /decide). Otherwise record an honest
|
|
793
|
+
// failure: never "completed" for a run whose terminal state was never seen.
|
|
794
|
+
// R8: anchor the rewind handle even on disconnect — if the user-message entryId was committed
|
|
795
|
+
// before the Ctrl+C, the shell (holding the early run_started taskId) can still rewind to this turn.
|
|
432
796
|
putRewindAnchor();
|
|
433
797
|
const pending = await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId);
|
|
434
798
|
if (pending) {
|
|
799
|
+
// [1.211 codex M1] detach 账本补 park 终局帧(done 未读到但 checkpoint 已在——账本没有
|
|
800
|
+
// suspended 帧则 events tail 无终局,壳等空;gate 内容未读到 done 无从知,落 gate:null)。
|
|
435
801
|
if (ledgerSink && !ledgerTerminal) {
|
|
436
802
|
ledgerTerminal = true;
|
|
437
803
|
await ledgerSink.flush().catch(() => undefined);
|
|
438
804
|
await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
|
|
439
805
|
}
|
|
440
806
|
await deps.runStore.setSuspended(durableTaskId);
|
|
441
|
-
settleFleet("suspended");
|
|
807
|
+
settleFleet("suspended"); // parked → stays in fleet as "waiting"
|
|
442
808
|
}
|
|
443
809
|
else {
|
|
810
|
+
// codex L1 对齐:同一类用户中止(disconnect abort)不再因 iterator 正常 return / throw 而给
|
|
811
|
+
// 两种终局形——与 throw 路兜底、resume-abort settle 同为 errorCode "cancelled" 的取消终局。
|
|
444
812
|
const dcErr = "stream client disconnected before completion (run aborted)";
|
|
445
813
|
const dc = { taskId: durableTaskId, sessionId: prepared.spec.sessionId ?? "", status: "failed", errorCode: "cancelled", errorMessage: dcErr, stats: { turns: 0, tokens: 0 } };
|
|
814
|
+
// [900]① detach 车道:账本也要有终局帧(bg 腿 cancelled 终局同形;detach 下本分支仅在
|
|
815
|
+
// generator 无 done 早收/cancel-verb 干净收尾时可达)。best-effort:落账失败不改行既有 settle。
|
|
446
816
|
if (ledgerSink && !ledgerTerminal) {
|
|
447
817
|
ledgerTerminal = true;
|
|
448
818
|
await ledgerSink.flush().catch(() => undefined);
|
|
@@ -452,46 +822,66 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
452
822
|
settleFleet("failed");
|
|
453
823
|
}
|
|
454
824
|
}
|
|
455
|
-
runRowSettled = true;
|
|
825
|
+
runRowSettled = true; // [868]③: every branch above recorded the row — the finally backstop stands down
|
|
456
826
|
}
|
|
457
827
|
}
|
|
458
828
|
finally {
|
|
459
|
-
syncLegLive = false;
|
|
829
|
+
syncLegLive = false; // notifications from here on take the durable-inbox path
|
|
460
830
|
if (durableHeartbeat)
|
|
461
831
|
clearInterval(durableHeartbeat);
|
|
832
|
+
// 1.207: drop the live verb handle — IDENTITY-GUARDED (review LOW): a suspend→fast-resume can
|
|
833
|
+
// re-register the SAME taskId from the resume leg before this finally runs; an unconditional
|
|
834
|
+
// delete would clobber the fresh handle (verbs would 409 "another replica" until re-registration).
|
|
462
835
|
if (durableTaskId && steerableRuns.get(durableTaskId) === liveStreamRef)
|
|
463
836
|
steerableRuns.delete(durableTaskId);
|
|
837
|
+
// [888]① same identity guard for the cancel handle (a fast-resume registers its own controller).
|
|
464
838
|
if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
|
|
465
839
|
inflightRuns.delete(durableTaskId);
|
|
466
|
-
cancelledViaVerb.delete(durableTaskId);
|
|
840
|
+
cancelledViaVerb.delete(durableTaskId); // H2 label lifecycle rides the registration
|
|
467
841
|
}
|
|
468
842
|
for (const evict of subagentHandleEvictions)
|
|
469
|
-
evict();
|
|
843
|
+
evict(); // design/122 ③: leg-end handle eviction (identity-guarded)
|
|
844
|
+
// [868]③: the DURABLE twin of leak C — a throw out of the stream (abort mid-iteration / plain
|
|
845
|
+
// Error) also skipped the run-row settle block, leaving a `running` zombie that only reapStale
|
|
846
|
+
// would flip (after runStaleSec, as a misleading "run stalled (instance lost?)"). Settle it here:
|
|
847
|
+
// re-read the row (a concurrent path may have settled it — then this is a no-op), honour a
|
|
848
|
+
// suspend that raced the abort (pending checkpoint ⇒ park, the disconnect branch's exact guard),
|
|
849
|
+
// else fail it — errorCode "cancelled" when the leg was aborted (user kill / disconnect), bare
|
|
850
|
+
// failed for a genuine mid-stream error. Best-effort: the backstop must never mask the real throw.
|
|
851
|
+
// 🔴 runs BEFORE the leak-C fleet backstop below (codex M3): the run-row verdict here (park vs
|
|
852
|
+
// fail) must drive the fleet verdict — the old order settled the fleet row "failed" first and a
|
|
853
|
+
// subsequently PARKED run row contradicted it ("waiting" run under a failed fleet row).
|
|
470
854
|
if (!runRowSettled && durableTaskId && deps.runStore) {
|
|
471
855
|
try {
|
|
472
856
|
const row = await deps.runStore.getRun(durableTaskId);
|
|
473
857
|
if (row?.status === "running") {
|
|
474
858
|
const pending = prepared.spec.sessionId ? await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId) : undefined;
|
|
475
859
|
if (pending) {
|
|
860
|
+
// [1.211 codex M1] detach 账本补 park 终局帧(finally 兜底路同款,gate 未知落 null)。
|
|
476
861
|
if (ledgerSink && !ledgerTerminal) {
|
|
477
862
|
ledgerTerminal = true;
|
|
478
863
|
await ledgerSink.flush().catch(() => undefined);
|
|
479
864
|
await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
|
|
480
865
|
}
|
|
481
866
|
await deps.runStore.setSuspended(durableTaskId);
|
|
482
|
-
settleFleet("suspended");
|
|
867
|
+
settleFleet("suspended"); // parked → the fleet row stays honest ("waiting", disconnect-branch parity)
|
|
483
868
|
}
|
|
484
869
|
else if (finalResult) {
|
|
870
|
+
// [1.211 codex H1] done 已观测(账本/fleet 可能已按它落)但行仍 running = 首次 setTerminal
|
|
871
|
+
// 抖动——兜底必须重试「原终局」,绝不写 failed(否则账本 done+fleet completed+行 failed
|
|
872
|
+
// 三面矛盾;bg 腿 `reached` 重试同款纪律)。账本侧若 onDone 也没落成,这里补一发。
|
|
485
873
|
if (ledgerSink && !ledgerTerminal) {
|
|
486
874
|
ledgerTerminal = true;
|
|
487
875
|
await ledgerSink.onDone(finalResult).catch(() => undefined);
|
|
488
876
|
}
|
|
489
877
|
await deps.runStore.setTerminal(durableTaskId, finalResult.status, finalResult, finalResult.errorMessage ?? null);
|
|
490
|
-
settleFleet(finalResult.status, fleetRunResiduals(finalResult));
|
|
878
|
+
settleFleet(finalResult.status, fleetRunResiduals(finalResult)); // once-guard 下通常 no-op;[1839]①/(a) 残局键同带
|
|
491
879
|
}
|
|
492
880
|
else if (ac.signal.aborted) {
|
|
493
881
|
const err = "stream aborted before completion (run cancelled)";
|
|
494
882
|
const c = { taskId: durableTaskId, sessionId: prepared.spec.sessionId ?? "", status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
|
|
883
|
+
// [900]① detach 车道:throw 路兜底同样补账本终局帧(bg 腿 catch 路 append("failed") 同形;
|
|
884
|
+
// ledgerTerminal 防对已落 done/park 的账本覆写 failed —— bg 腿 `reached` 同款纪律)。
|
|
495
885
|
if (ledgerSink && !ledgerTerminal) {
|
|
496
886
|
ledgerTerminal = true;
|
|
497
887
|
await ledgerSink.flush().catch(() => undefined);
|
|
@@ -511,17 +901,28 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
511
901
|
}
|
|
512
902
|
}
|
|
513
903
|
catch {
|
|
904
|
+
/* best-effort — reapStale remains the last-resort net; the original throw stays the story */
|
|
514
905
|
}
|
|
515
906
|
}
|
|
907
|
+
// C: a plain Error thrown mid-stream skipped every onTerminal branch above — settle the leaked
|
|
908
|
+
// "running" row to "failed" before the throw propagates (no-op when any branch — incl. the [868]③
|
|
909
|
+
// park above — already settled; settleFleet is once-guarded).
|
|
516
910
|
settleFleet("failed");
|
|
517
911
|
}
|
|
518
912
|
return { status: 200, body: finalResult };
|
|
519
913
|
};
|
|
914
|
+
// B3 复审(codex HIGH):无 runStore 的部署 durableTaskId 恒缺 → 这条活流从不进 steerableRuns,
|
|
915
|
+
// inflight() 全程读 0,SIGHUP 空闲窗(以及 SIGTERM drain)会把它当空闲斩掉。整腿显式计数,
|
|
916
|
+
// 与 steerableRuns 的短暂双计无害(两者同在终局递减,>0 语义只看非零)。
|
|
520
917
|
counters.uncountedBillableInflight++;
|
|
521
918
|
try {
|
|
522
|
-
resp = await idemCache.run(idemKey, runStreamSubmitLeg,
|
|
919
|
+
resp = await idemCache.run(idemKey, runStreamSubmitLeg,
|
|
920
|
+
// Cache only a clean completion; a failed/aborted stream is evicted so a retry re-runs.
|
|
921
|
+
(r) => r.body?.status === "completed");
|
|
523
922
|
}
|
|
524
923
|
catch (e) {
|
|
924
|
+
// Log unconditionally (don't silently drop a runner error that races with the client close — council);
|
|
925
|
+
// re-throw only while the client is still connected so the outer handler can surface it.
|
|
525
926
|
deps.logger?.error("stream_error", { closed, err: e instanceof Error ? e.message : String(e) });
|
|
526
927
|
if (!closed)
|
|
527
928
|
throw e;
|
|
@@ -529,40 +930,67 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
529
930
|
finally {
|
|
530
931
|
counters.uncountedBillableInflight--;
|
|
531
932
|
clearInterval(hb);
|
|
933
|
+
// E8 leak fence(双腿对齐的对偶):detach 腿注册过的归因键在 leg 收尾必 clear——终局 attach 已
|
|
934
|
+
// drain 过时这是幂等 no-op;errored/aborted leg 则丢掉未 drain 的累积(bg 腿 finally 同款)。
|
|
532
935
|
if (usageKey)
|
|
533
936
|
deps.modelUsage?.clear(usageKey);
|
|
534
937
|
}
|
|
938
|
+
// A concurrent caller the cache deduplicated (ranLive=false) received no live events — hand it the terminal.
|
|
535
939
|
if (!ranLive && resp && !res.writableEnded) {
|
|
536
940
|
res.write(`data: ${JSON.stringify({ type: "done", result: resp.body, replay: true })}\n\n`);
|
|
537
941
|
}
|
|
538
942
|
res.end();
|
|
539
943
|
}
|
|
540
944
|
else {
|
|
945
|
+
// Idempotency-Key dedup (center blocker: exactly-once billing) — concurrent + retried same-key submits
|
|
946
|
+
// share one run; the whole run+record is wrapped so metrics/cost are counted exactly once on a replay.
|
|
947
|
+
// ── B3 ── 同上:原文此处是同步腿的 **128 行匿名 async**,提为具名闭包。
|
|
541
948
|
const runSyncSubmitLeg = async () => {
|
|
949
|
+
// Durable F4/ask on the sync path — same two things as the stream path (audit functional gap):
|
|
950
|
+
// 1. createRun a task_active claim at SUBMIT (+ heartbeat) so reapCtx's task_active guard protects
|
|
951
|
+
// the ctx for the WHOLE first leg (its updated_at is stamped at submit and never refreshed; a
|
|
952
|
+
// multi-minute leg would otherwise age it past REAP_RUN_STALE_SEC and get it reaped MID-RUN).
|
|
953
|
+
// setSuspended on suspend keeps the claim (session locked submit→resume); setTerminal releases it.
|
|
954
|
+
// 2. putCtx AFTER the claim → a suspend here is resumable on any replica. Claim-first is load-bearing
|
|
955
|
+
// (parity with /v1/runs): a lost claim is a 409, never a silent second leg on a locked session —
|
|
956
|
+
// and a rejected submit must not clobber the active run's resume ctx (putCtx is sessionId-keyed).
|
|
542
957
|
let durableTaskId;
|
|
543
958
|
let durableHeartbeat;
|
|
959
|
+
// R8 (CC parity): the run-row claim + `durableTaskId` (the REWIND anchor key + the unique result.taskId
|
|
960
|
+
// re-stamp) is gated on `runStore` ALONE; the durable-suspend resume ctx (`putCtx`) on `checkpointStore`. These
|
|
961
|
+
// were bundled under ONE checkpointStore gate, which over-coupled REWIND to DURABLE_APPROVAL — but E18 resume-at
|
|
962
|
+
// needs NO checkpoint (main.ts:815). Now matches the already-decoupled /v1/runs bg path: rewind works with just
|
|
963
|
+
// runStore (DB_BACKEND=tidb/pg), no approval. Claim-first preserved (a lost claim → 409, never a silent 2nd leg).
|
|
544
964
|
if (deps.runStore && prepared.spec.sessionId) {
|
|
545
965
|
const tid = uuidv7();
|
|
546
966
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
547
967
|
if (!created.ok)
|
|
548
968
|
return { status: 409, body: { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", errorCode: "conflict.session_active_run", activeTaskId: created.activeTaskId } };
|
|
549
|
-
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective);
|
|
969
|
+
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective); // fire-and-forget session auto-title
|
|
550
970
|
durableTaskId = tid;
|
|
551
971
|
}
|
|
552
972
|
if (deps.checkpointStore && prepared.spec.sessionId) {
|
|
973
|
+
// putCtx AFTER the claim (resumable). If this throws, the claim row self-heals: no heartbeat yet → reapStale flips it failed + releases.
|
|
553
974
|
await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
|
|
554
975
|
}
|
|
976
|
+
// [888]① interactive live-run cancel, non-stream twin: this leg previously ran with NO abort seam at
|
|
977
|
+
// all (bare runTask, no spec.signal) — a cancel flipped the durable flag nothing consumed. Wire a
|
|
978
|
+
// controller through spec.signal (same seam as the stream leg's disconnect-kill) + register it in the
|
|
979
|
+
// in-flight map so the cancel fast path lands. Children reaped by this abort are a USER stop (parity
|
|
980
|
+
// with the stream leg's disconnect marker).
|
|
555
981
|
const syncCancelCtrl = new AbortController();
|
|
556
982
|
if (prepared.spec.sessionId)
|
|
557
983
|
markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, prepared.spec.sessionId, prepared.auth?.principal);
|
|
558
984
|
if (durableTaskId && deps.runStore) {
|
|
559
985
|
const rs = deps.runStore;
|
|
560
986
|
const tid = durableTaskId;
|
|
561
|
-
const owner = principal ?? null;
|
|
987
|
+
const owner = principal ?? null; // run created with this owner — pass it as the single-DB null-safe heartbeat guard
|
|
562
988
|
const abortFromVerb = () => {
|
|
563
|
-
cancelledViaVerb.add(tid);
|
|
989
|
+
cancelledViaVerb.add(tid); // H2: label the settle (errorCode "cancelled")
|
|
564
990
|
syncCancelCtrl.abort();
|
|
565
991
|
};
|
|
992
|
+
// [1.207 codex H1] heartbeat ALSO polls the durable cancel flag (bg-leg parity) — cross-replica
|
|
993
|
+
// cancel only sets the flag; this leg previously had no consumer.
|
|
566
994
|
durableHeartbeat = setInterval(() => {
|
|
567
995
|
void rs.heartbeat(tid, owner).catch(() => undefined);
|
|
568
996
|
if (!syncCancelCtrl.signal.aborted) {
|
|
@@ -572,6 +1000,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
572
1000
|
}, HEARTBEAT_MS);
|
|
573
1001
|
durableHeartbeat.unref?.();
|
|
574
1002
|
inflightRuns.set(tid, syncCancelCtrl);
|
|
1003
|
+
// [1.207 codex H1] registration-window catch-up (flag flipped during the awaited putCtx window).
|
|
575
1004
|
void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !syncCancelCtrl.signal.aborted)
|
|
576
1005
|
abortFromVerb(); }).catch(() => undefined);
|
|
577
1006
|
}
|
|
@@ -579,10 +1008,21 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
579
1008
|
inflightRuns.set(durableTaskId, syncCancelCtrl);
|
|
580
1009
|
}
|
|
581
1010
|
const specWithSignal = { ...prepared.spec, signal: syncCancelCtrl.signal };
|
|
1011
|
+
// Developer mode (1.44): verify = adversarial verify→fix gate. Cascade (1.45): cheap→strong ladder,
|
|
1012
|
+
// escalate on a failed gate (default = didn't complete). Both opt-in, mutually exclusive, multi-attempt.
|
|
582
1013
|
let result;
|
|
583
|
-
let cancelVerbLabel = false;
|
|
584
|
-
|
|
1014
|
+
let cancelVerbLabel = false; // [1.207 codex H2] set in the finally below while the label entry is alive
|
|
1015
|
+
// E8 (shell-host contract): the SYNCHRONOUS /v1/tasks path is INTENTIONALLY excluded from the per-task `modelUsage`
|
|
1016
|
+
// echo — it does NOT register() with the ModelUsageTracker, so the register-gate makes its brain.call usage a
|
|
1017
|
+
// no-op (NOT a leak; the register-gate prevents the prior unbounded growth). modelUsage is the
|
|
1018
|
+
// async durable /v1/runs feature (cap `modelUsage` is gated on the run store + tracker); a sync caller gets
|
|
1019
|
+
// its result synchronously and reads aggregate cost from stats. (Wiring sync = register + a withModelUsage
|
|
1020
|
+
// attach pass; deferred as low-value for the synchronous path.)
|
|
1021
|
+
counters.uncountedBillableInflight++; // B-1: the sync await is real model work — drain must see it
|
|
585
1022
|
try {
|
|
1023
|
+
// ── B3 ── 原文此处是一处三层三元,把三条模型驱动腿挤在五行里。三腿具名化后判别顺序
|
|
1024
|
+
// (verify → cascade → plain)、短路语义、传参**逐字不变**;`runWithVerification` 仍是整对象
|
|
1025
|
+
// 直传(快审 F1:cost 顶不在重建中丢失),`runCascade` 仍从操作员梯子 + 本任务预算现算配置。
|
|
586
1026
|
const verifyLeg = (v) => runWithVerification(deps.runner, specWithSignal, v);
|
|
587
1027
|
const cascadeLeg = () => runCascade(deps.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd));
|
|
588
1028
|
const plainLeg = () => deps.runner.runTask(specWithSignal);
|
|
@@ -593,36 +1033,59 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
593
1033
|
if (durableHeartbeat)
|
|
594
1034
|
clearInterval(durableHeartbeat);
|
|
595
1035
|
if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl) {
|
|
596
|
-
inflightRuns.delete(durableTaskId);
|
|
597
|
-
cancelVerbLabel = cancelledViaVerb.has(durableTaskId);
|
|
1036
|
+
inflightRuns.delete(durableTaskId); // [888]① identity-guarded
|
|
1037
|
+
cancelVerbLabel = cancelledViaVerb.has(durableTaskId); // H2: capture before dropping (projection below)
|
|
598
1038
|
cancelledViaVerb.delete(durableTaskId);
|
|
599
1039
|
}
|
|
600
1040
|
}
|
|
1041
|
+
// [1.207 codex H2] cancel-verb abort label (non-stream twin of the SSE done-frame projection):
|
|
1042
|
+
// core's external-signal abort yields failed WITHOUT an errorCode — label it so /cancel's outcome is
|
|
1043
|
+
// uniform across legs. Only an abort-shaped FAILED result; a completion that raced the abort stands.
|
|
601
1044
|
if (cancelVerbLabel && result.status === "failed" && !result.errorCode) {
|
|
602
1045
|
result = { ...result, errorCode: "cancelled" };
|
|
603
1046
|
}
|
|
1047
|
+
// E18: a resume-at caller error (a stale/non-message anchor target) comes back as a failed result carrying a
|
|
1048
|
+
// resume_at.* code — a caller mistake that BILLED NOTHING. Handle it BEFORE the suspend/terminal/finalize
|
|
1049
|
+
// logic below: release the submit-time claim (unlock the session) but do NOT finalizeTaskResult (it must not
|
|
1050
|
+
// count as a billed failure / plan-cache miss), and return a 4xx (caller fault) instead of a 200-with-failed
|
|
1051
|
+
// body. NOT idem-cached (the predicate below caches only success-range responses) — a fixed anchor can retry.
|
|
604
1052
|
const resumeAtStatus = resumeAtHttpStatus(result);
|
|
605
1053
|
if (resumeAtStatus) {
|
|
606
1054
|
if (durableTaskId && deps.runStore)
|
|
607
1055
|
await deps.runStore.setTerminal(durableTaskId, "failed", stripCheckpointToken(result), result.errorMessage ?? null);
|
|
608
1056
|
return { status: resumeAtStatus, body: { errorCode: result.errorCode, error: result.errorMessage ?? "resume-at failed" } };
|
|
609
1057
|
}
|
|
1058
|
+
// Durable suspend on the sync path: PARK it as an async run + return a pollable taskId, NEVER the
|
|
1059
|
+
// capability token. The submitter switches to GET /v1/runs/:id. Cost is recorded
|
|
1060
|
+
// by the RESUMED run (mirrors runInBackground, which also parks-without-record on suspend) — not here,
|
|
1061
|
+
// so the pre-suspend tokens aren't double-counted on resume.
|
|
610
1062
|
if (result.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
|
|
1063
|
+
// The submit-time row — guaranteed when runStore is present (a lost claim was 409'd before running).
|
|
611
1064
|
if (durableTaskId)
|
|
612
1065
|
await deps.runStore.setSuspended(durableTaskId);
|
|
613
1066
|
return { status: 200, body: { taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" } };
|
|
614
1067
|
}
|
|
1068
|
+
// design/80 D-B: a review pause (plan_review / dry_run_review) on the sync path — PARK it like a suspend
|
|
1069
|
+
// (keep task_active) + return a pollable taskId so the /v1/assistant/tasks/:id/plan_review resume can
|
|
1070
|
+
// claim it. NEVER the capability token (parity with the suspended branch above).
|
|
615
1071
|
if (result.status === "needs_review" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
|
|
616
1072
|
if (durableTaskId)
|
|
617
1073
|
await deps.runStore.setNeedsReview(durableTaskId);
|
|
618
1074
|
return { status: 200, body: { taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" } };
|
|
619
1075
|
}
|
|
1076
|
+
// Not suspended → release the submit-time claim so the session unlocks + the ctx becomes reapable.
|
|
620
1077
|
if (durableTaskId && deps.runStore)
|
|
621
1078
|
await deps.runStore.setTerminal(durableTaskId, result.status, stripCheckpointToken(result), result.errorMessage ?? null);
|
|
1079
|
+
// A completed OR failed result both BILLED → cache it so a retry replays the outcome (no double-bill);
|
|
1080
|
+
// a thrown error / transient conflict is evicted by the cache so a genuine retry re-runs. (The async
|
|
1081
|
+
// /v1/runs caches only its 202 receipt — a different object, queried separately — hence the asymmetry.)
|
|
622
1082
|
finalizeTaskResult(result, principal, prepared.spec.objective, prepared.spec.sessionId);
|
|
1083
|
+
// A verify/cascade NESTED suspend is mapped by core to status:"failed" while still carrying the
|
|
1084
|
+
// capability token (the genuine top-level suspend is parked above and never reaches here). Strip it so
|
|
1085
|
+
// the resume credential never goes in the response body OR the idempotency cache.
|
|
623
1086
|
return { status: 200, body: stripCheckpointToken(result) };
|
|
624
1087
|
};
|
|
625
|
-
const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400);
|
|
1088
|
+
const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400); // cache only success-range outcomes (a completed/failed BILLED result, replay to avoid double-bill); never a session-active 409 or a resume-at 4xx (billed nothing — a retry must re-check)
|
|
626
1089
|
sendJson(res, resp.status, resp.body);
|
|
627
1090
|
}
|
|
628
1091
|
return;
|