@sema-agent/server 1.323.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/budget-tracing.d.ts +48 -0
- package/dist/boot/budget-tracing.js +86 -0
- package/dist/boot/config-center.d.ts +62 -0
- package/dist/boot/config-center.js +995 -0
- package/dist/boot/coordinators.d.ts +33 -0
- package/dist/boot/coordinators.js +97 -0
- package/dist/boot/execution-env.d.ts +26 -0
- package/dist/boot/execution-env.js +370 -0
- package/dist/boot/leader.d.ts +27 -0
- package/dist/boot/leader.js +81 -0
- package/dist/boot/reapers.d.ts +53 -0
- package/dist/boot/reapers.js +252 -0
- package/dist/boot/resolve-spec.d.ts +70 -0
- package/dist/boot/resolve-spec.js +1072 -0
- package/dist/boot/runner-deps.d.ts +101 -0
- package/dist/boot/runner-deps.js +343 -0
- package/dist/boot/runtime-caps.d.ts +21 -0
- package/dist/boot/runtime-caps.js +62 -0
- package/dist/boot/session-faces.d.ts +57 -0
- package/dist/boot/session-faces.js +157 -0
- package/dist/boot/shutdown.d.ts +50 -0
- package/dist/boot/shutdown.js +129 -0
- package/dist/boot/stores.d.ts +32 -0
- package/dist/boot/stores.js +361 -0
- package/dist/boot/workflow-orchestration.d.ts +46 -0
- package/dist/boot/workflow-orchestration.js +150 -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 +585 -1
- package/dist/config.d.ts +78 -2
- package/dist/config.js +874 -301
- 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 +530 -2129
- 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
package/dist/http/routes/runs.js
CHANGED
|
@@ -11,26 +11,48 @@ import { streamSseLog } from "../sse-log.js";
|
|
|
11
11
|
import { normalizeRunEventType } from "../../trace/project.js";
|
|
12
12
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
13
13
|
import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
14
|
+
// server.ts 侧的 routeLabel / isBillableSubmitPath 仍要用下面这些正则,故本模块导出(方向恒为 server.ts → routes/*)。
|
|
14
15
|
export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
|
|
15
16
|
export const RUN_CANCEL_RE = /^\/v1\/runs\/([^/]+)\/cancel$/;
|
|
16
17
|
export const RUN_STEER_RE = /^\/v1\/runs\/([^/]+)\/steer$/;
|
|
17
18
|
export const RUN_DETACH_RE = /^\/v1\/runs\/([^/]+)\/detach$/;
|
|
18
19
|
export const RUN_COMPACT_RE = /^\/v1\/runs\/([^/]+)\/compact$/;
|
|
20
|
+
// E23 (shell-host contract): the shell answers an inbound MCP elicitation here. Live-only + same-replica (the pending promise
|
|
21
|
+
// is in-memory) — a respond on the wrong replica / after answer/expiry → 404, like a steer on a non-live run.
|
|
22
|
+
/** PAIR-REVIEW F-10:runs 面 404 文案统一富形——a\* 形 id(background-agent registry 句柄,`a`+hex 短形)
|
|
23
|
+
* 是「结构性不在 run 店」的常客([1347]⑤ 记档裁量:不改路由,只给引导)。识别句法形即补引导;
|
|
24
|
+
* 非 a\* 形保通用富文案(cancel 腿 [888]① 同款)。 */
|
|
25
|
+
/** B7(design/158):文案逐字不动,只把「体」换成「文案」——两个调用点改走 `sendError(…, "not_found.run", …)`,
|
|
26
|
+
* 机器码由发送点携带(errorCode 恒在),这样错误码门不必为「体来自 helper」的站点开豁免。 */
|
|
19
27
|
function runNotFoundMessage(id) {
|
|
28
|
+
// [1434]:前缀=域判别子既成契约——wa\*(workflow agent 观测行,1.356)与 a\* 同族放宽。
|
|
20
29
|
const aStar = /^(?:a|wa)[0-9a-f]{4,}$/i.test(id);
|
|
21
30
|
return aStar
|
|
22
31
|
? `run not found — "${id}" looks like an agent handle (a*/wa* domain), which is never in the run store; use the TaskOutput tool (or the workflow journal / subagents surface) to read it, and the run's durable taskId (X-Task-Id) for this endpoint`
|
|
23
32
|
: "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
|
|
24
33
|
}
|
|
25
34
|
export const ELICIT_RESPOND_RE = /^\/v1\/elicitations\/([^/]+)\/respond$/;
|
|
35
|
+
// §4④: the shell answers the agent's AskUserQuestion here. Same live-only + same-replica shape as elicit.
|
|
26
36
|
export const QUESTION_RESPOND_RE = /^\/v1\/questions\/([^/]+)\/respond$/;
|
|
27
37
|
export const TOOL_APPROVAL_RESPOND_RE = /^\/v1\/tool-approvals\/([^/]+)\/respond$/;
|
|
38
|
+
// C2 (core 1.219): steer a still-running Task SUB-AGENT of a run. `:target` = the child's
|
|
39
|
+
// parentToolCallId (spawn-time identity, unique) or its agentName (display name; 409 when ambiguous).
|
|
28
40
|
export const RUN_SUBAGENT_STEER_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/steer$/;
|
|
41
|
+
// design/122 (core 1.225): REVIVE a SETTLED sub-agent with a new prompt (CC dfe parity).
|
|
29
42
|
export const RUN_SUBAGENT_RESUME_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/resume$/;
|
|
43
|
+
// [1488]③(b): GET a BACKGROUND child's final report / current status (a… agent handle → core TaskRegistry,
|
|
44
|
+
// the TaskOutput-tool face over HTTP; background_agent ONLY — wa… workflow-agent rows are observation
|
|
45
|
+
// identities read via the workflow journal). Complements steer/resume (which cover SYNC children only).
|
|
30
46
|
export const RUN_SUBAGENT_OUTPUT_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/output$/;
|
|
31
47
|
export const RUN_SUBAGENT_STREAM_RE = /^\/v1\/runs\/([^/]+)\/subagents\/([^/]+)\/stream$/;
|
|
48
|
+
// [1499] CC TaskOutput/TaskStop 人侧对位 — the GENERIC task-handle verbs (b* bash stdout — cursor
|
|
49
|
+
// consumption depends on the handle's shape, see the handler; m* monitor batches, a* agent final report;
|
|
50
|
+
// `workflow` refused — journal face owns it). SESSION-enforced from birth (new face, zero consumers):
|
|
51
|
+
// a session-bound run requires the matching ?session=.
|
|
32
52
|
export const RUN_TASK_OUTPUT_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/output$/;
|
|
33
53
|
export const RUN_TASK_STOP_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/stop$/;
|
|
54
|
+
/** The task_run SSE — a thin wrapper over `streamSseLog` that PRESERVES the existing wire shape exactly
|
|
55
|
+
* (`data: {type, ...data}`, `event: failed` stale frame). Behavior-identical to the pre-refactor reader. */
|
|
34
56
|
async function streamRunEvents(req, res, runStore, taskId, staleMs) {
|
|
35
57
|
await streamSseLog(req, res, {
|
|
36
58
|
statusOf: async (id) => {
|
|
@@ -39,6 +61,9 @@ async function streamRunEvents(req, res, runStore, taskId, staleMs) {
|
|
|
39
61
|
},
|
|
40
62
|
getEvents: (id, after) => runStore.getEvents(id, after),
|
|
41
63
|
retainedFrom: (id) => runStore.retainedFrom(id),
|
|
64
|
+
// design/158 B2:唯一的读边界归一 —— 存量 `brain_status` 行按公开联合的名字(`status`)下发。
|
|
65
|
+
// `event:` 与 `data.type` 用**同一个**归一后的名字(此前也是同一个原始名),否则按 event 名分派的
|
|
66
|
+
// 消费端与按 data.type 分派的消费端会看到两套名字。除这一个键外全部原样透传(见 normalizeRunEventType)。
|
|
42
67
|
formatEvent: (ev) => {
|
|
43
68
|
const type = normalizeRunEventType(ev.type);
|
|
44
69
|
return { id: ev.seq, event: type, data: { type, ...(ev.data ?? {}) } };
|
|
@@ -51,23 +76,34 @@ export async function handleRuns(req, res, url, ctx) {
|
|
|
51
76
|
await handleRunsBody(req, res, url, ctx, miss);
|
|
52
77
|
return !miss.fell;
|
|
53
78
|
}
|
|
79
|
+
/** 子代理 / 任务句柄 / HITL 应答段——`handle()` 里它在 SVC-5 workflow steer **之后**,故是独立的域入口。 */
|
|
54
80
|
export async function handleRunVerbs(req, res, url, ctx) {
|
|
55
81
|
const miss = { fell: false };
|
|
56
82
|
await handleRunVerbsBody(req, res, url, ctx, miss);
|
|
57
83
|
return !miss.fell;
|
|
58
84
|
}
|
|
85
|
+
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
|
|
86
|
+
* = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
|
|
59
87
|
async function handleRunsBody(req, res, url, ctx, miss) {
|
|
60
88
|
const { deps } = ctx;
|
|
61
89
|
const { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints } = ctx.registry;
|
|
62
90
|
const { readJson, rateLimited, quotaExceeded, leaseDenied, runOwnerOk, runSessionAcceptOk } = ctx.helpers;
|
|
63
91
|
const { prepareSpec } = ctx.legs;
|
|
64
92
|
const source = ctx.req.source;
|
|
93
|
+
// Async run: create + return ids immediately, execute in the background.
|
|
65
94
|
if (req.method === "POST" && url === "/v1/runs") {
|
|
66
95
|
if (!deps.runStore) {
|
|
67
96
|
sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store (SESSION_BACKEND=tidb)");
|
|
68
97
|
return;
|
|
69
98
|
}
|
|
99
|
+
// Idempotency-Key dedup (center blocker): a retried create returns the SAME taskId instead of starting a
|
|
100
|
+
// second background run (= second bill). Concurrent same-key requests share the one in-flight create. The
|
|
101
|
+
// replay is checked BEFORE the rate/quota gates so a retry doesn't get a fresh 429 (council).
|
|
70
102
|
const rawIdem = headerStr(req.headers["idempotency-key"]);
|
|
103
|
+
// Scope by identity only when there IS a key (BL-3) — avoids reading the principal header on every
|
|
104
|
+
// submit, and keeps the no-key path independent of principal config.
|
|
105
|
+
// F-fix: scope on the VERIFIED principal (gatedPrincipal) — on a direct door a spoofable header would let a
|
|
106
|
+
// caller poison/collide another principal's idempotency cache. (BFF/gated: identical to principalFrom.)
|
|
71
107
|
const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, gatedPrincipal(req, deps.config)) : undefined;
|
|
72
108
|
const cached = idemKey ? idemCache.peek(idemKey) : undefined;
|
|
73
109
|
if (cached) {
|
|
@@ -75,12 +111,22 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
75
111
|
sendJson(res, resp.status, resp.body);
|
|
76
112
|
return;
|
|
77
113
|
}
|
|
114
|
+
// 🔴 复审 C1:NO lease gate here — it must not sit ABOVE the durable-taskId idempotency replay
|
|
115
|
+
// below, or a retry of an already-created run (dispatch-gateway failover) would get 429 instead of its
|
|
116
|
+
// existing 202 receipt, breaking the exactly-once contract. The lease admits a genuinely-NEW run only
|
|
117
|
+
// (applied just before createRun). Rate/quota stay here (cheap pre-reject; a rate-limited replay is a
|
|
118
|
+
// transient the caller retries, unlike a persistent lease exhaustion).
|
|
78
119
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
79
120
|
return;
|
|
80
121
|
const prepared = await prepareSpec(req, res);
|
|
81
122
|
if (!prepared)
|
|
82
123
|
return;
|
|
83
124
|
const runStore = deps.runStore;
|
|
125
|
+
// dispatch-gateway failover prerequisite: OPTIONAL caller-minted `body.taskId` idempotency. Unlike the
|
|
126
|
+
// Idempotency-Key header (in-memory, per-instance), this replay reads the DURABLE run store, so a gateway that
|
|
127
|
+
// re-routes a submit to ANOTHER instance after a network error cannot start a second run. uuidv7-validated
|
|
128
|
+
// (same alphabet the server mints — a caller can't inject an arbitrary PK shape); owner-gated replay mirrors
|
|
129
|
+
// the GET /v1/runs/:id gate (a foreign taskId reads as 404-shaped conflict, not another tenant's run ids).
|
|
84
130
|
const rawClientTaskId = prepared.body.taskId;
|
|
85
131
|
if (rawClientTaskId !== undefined && (typeof rawClientTaskId !== "string" || !isUuidV7(rawClientTaskId))) {
|
|
86
132
|
sendError(res, 400, "request.id_invalid", "body.taskId must be a uuidv7 string (caller-minted idempotency key)");
|
|
@@ -92,13 +138,15 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
92
138
|
if (existing) {
|
|
93
139
|
const verified = verifiedPrincipal(req, deps.config);
|
|
94
140
|
if (existing.owner !== null && existing.owner !== verified) {
|
|
95
|
-
sendError(res, 409, "conflict.run_exists", "taskId already exists");
|
|
141
|
+
sendError(res, 409, "conflict.run_exists", "taskId already exists"); // foreign owner: conflict, no replay (no cross-tenant oracle)
|
|
96
142
|
return;
|
|
97
143
|
}
|
|
98
144
|
sendJson(res, 202, { taskId: existing.taskId, sessionId: existing.sessionId, status: existing.status });
|
|
99
145
|
return;
|
|
100
146
|
}
|
|
101
147
|
}
|
|
148
|
+
// 🔴 复审 C1:lease gate for a genuinely-NEW submission (past the idempotency replay above). Keyed on the
|
|
149
|
+
// VERIFIED submitter = the principal billed for this run's spend (correct principal for the sync door).
|
|
102
150
|
if (await leaseDenied(req, res))
|
|
103
151
|
return;
|
|
104
152
|
const resp = await idemCache.run(idemKey, async () => {
|
|
@@ -106,16 +154,23 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
106
154
|
const taskId = clientTaskId ?? uuidv7();
|
|
107
155
|
const created = await runStore.createRun(taskId, sessionId, prepared.auth?.principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
108
156
|
if (created.ok)
|
|
109
|
-
deps.sessionTitler?.maybeTitle(sessionId, prepared.spec.objective);
|
|
157
|
+
deps.sessionTitler?.maybeTitle(sessionId, prepared.spec.objective); // fire-and-forget session auto-title
|
|
110
158
|
if (!created.ok) {
|
|
159
|
+
// race twin: two concurrent same-taskId submits (gateway retry racing the original) — the loser's
|
|
160
|
+
// session claim collides with ITS OWN taskId → idempotent 202 replay, not a 409 (the run exists, once).
|
|
111
161
|
if (clientTaskId && created.activeTaskId === clientTaskId) {
|
|
112
162
|
return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
|
|
113
163
|
}
|
|
114
164
|
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 } };
|
|
115
165
|
}
|
|
166
|
+
// Durable F4: persist the resume rebuild inputs (sessionId-keyed) so an operator can resume from any
|
|
167
|
+
// replica even after this worker is gone — core's checkpoint blob can't carry service scenario context.
|
|
116
168
|
if (deps.checkpointStore) {
|
|
117
169
|
await deps.checkpointStore.putCtx(sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
|
|
118
170
|
}
|
|
171
|
+
// E18 resume-at: capture each completed turn's (message eventId → leaf entryId) anchor so the shell can later
|
|
172
|
+
// rewind to that message. Closes over THIS run's sessionId/owner + the session store's getLeafId + the anchor
|
|
173
|
+
// store. undefined ⇒ no anchor store / no getLeafId (the env-only deploy) → resume-at simply isn't captured.
|
|
119
174
|
const anchorOwner = prepared.auth?.principal ?? null;
|
|
120
175
|
const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
|
|
121
176
|
const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
|
|
@@ -125,18 +180,42 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
125
180
|
await deps.resumeAnchorStore.put(sessionId, eventId, leaf, anchorOwner);
|
|
126
181
|
}
|
|
127
182
|
: undefined;
|
|
183
|
+
// R8 (CC-parity rewind): the USER-message anchor — keyed by THIS run's taskId (the handle the shell holds
|
|
184
|
+
// from the 202 response), value = the user-message `entryId` straight from `message_committed{role:"user"}`
|
|
185
|
+
// (always a valid resumeAt target). No `getLeafId` (the entryId is on the event), so it works even on a
|
|
186
|
+
// store that lacks getLeafId. This is the "rewind to the prompt" target = the code-restore parity path.
|
|
128
187
|
const captureUserMessageAnchor = deps.resumeAnchorStore
|
|
129
188
|
? async (entryId) => { await deps.resumeAnchorStore.put(sessionId, taskId, entryId, anchorOwner); }
|
|
130
189
|
: undefined;
|
|
190
|
+
// MF-Fleet: a background (202) run is a durable run → it gets a live fleet row exactly like the sync leg.
|
|
191
|
+
// runInBackground fires onStart/onEvent/onTerminal across ALL its legs (verify/cascade/plain-stream). No-op
|
|
192
|
+
// when no fleetBus is wired (the publisher is a null-object).
|
|
131
193
|
const fleetPub = fleetRunPublisher(deps.fleetBus, {
|
|
132
194
|
runId: taskId,
|
|
195
|
+
// 🔒 Scope the fleet row with the SAME identity GET /v1/fleet/stream FILTERS by — `gatedPrincipal`
|
|
196
|
+
// (crypto-verified on a direct door), NOT `prepared.auth.principal` (= the spoofable header). On a direct
|
|
197
|
+
// door the two diverge: a spoofed header tags the row under a victim's scope (cross-tenant leak) or under
|
|
198
|
+
// "default" while the verified owner's stream filters it OUT (the row vanishes). TOC review #6.
|
|
133
199
|
scope: gatedPrincipal(req, deps.config) ?? "default",
|
|
200
|
+
// core 1.154 nested-subagent tree: rootTaskId = the run's CANONICAL core taskId (`spec.taskId ?? sessionId`).
|
|
201
|
+
// The background leg sets `spec.taskId = taskId` (runs.ts), so canonical == taskId == runId — a forwarded
|
|
202
|
+
// subagent whose `parentTaskId` equals this nests under the run; deeper ones nest under their parent subagent.
|
|
134
203
|
rootTaskId: taskId,
|
|
135
|
-
...fleetRunLabels(prepared.spec.objective),
|
|
204
|
+
...fleetRunLabels(prepared.spec.objective), // BC-1: name = short objective preview (description = live-activity, set by the publisher onEvent tool_start)
|
|
136
205
|
});
|
|
137
|
-
void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation,
|
|
206
|
+
void runInBackground(deps.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, // E23: per-run elicitation context (onElicit routes inbound MCP elicitations to this run's stream)
|
|
207
|
+
gatedPrincipal(req, deps.config) ?? null, // E23: the VERIFIED principal that may answer (same source the respond gate uses — never the spoofable header)
|
|
208
|
+
captureTurnAnchor, // E18: per-turn (message eventId → leaf entryId) anchor capture
|
|
209
|
+
fleetPub, // MF-Fleet: the run-scoped fleet-row publisher (onStart/onEvent/onTerminal across all legs)
|
|
210
|
+
captureUserMessageAnchor, // R8: per-prompt (taskId → user-message entryId) anchor — CC "rewind to the prompt"
|
|
211
|
+
deps.question, // §4④: per-run AskUserQuestion context (onQuestion routes the agent's asks to this run's stream)
|
|
212
|
+
deps.workflowCompletionInbox, // P1 ①② follow-on: drain pending workflow completions into this leg's durable log at start
|
|
213
|
+
deps.subagentSteerRegistry, // C2 (core 1.219): register sync-delegation steer handles under this run's taskId
|
|
214
|
+
(m, x) => deps.logger?.info?.(m, x), // emit-target diagnostics for the bg leg's inbox drain
|
|
215
|
+
deps.sendUserFile, // SendUserFile 切片2:file_link 帧走本腿 durable append
|
|
216
|
+
deps.promptManifests);
|
|
138
217
|
return { status: 202, body: { taskId, sessionId, status: "running" } };
|
|
139
|
-
}, (r) => r.status === 202);
|
|
218
|
+
}, (r) => r.status === 202); // don't cache a transient 409 (session-active, billed nothing) — let a retry re-run (council)
|
|
140
219
|
sendJson(res, resp.status, resp.body);
|
|
141
220
|
return;
|
|
142
221
|
}
|
|
@@ -147,11 +226,18 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
147
226
|
return;
|
|
148
227
|
}
|
|
149
228
|
const taskId = runMatch[1];
|
|
229
|
+
// Enforce the principal requirement BEFORE the lookup so the 401/404 ordering matches the approvals
|
|
230
|
+
// endpoint (no 404-vs-401 existence oracle for an unauthenticated caller under requirePrincipal).
|
|
231
|
+
// 🔴 F-fix: gatedPrincipal (VERIFIED), not the spoofable header. On a direct door the legit owner authenticates
|
|
232
|
+
// by the JWT (no x-agent-principal header), so a `principalFrom` presence check would 401 a VALID caller — and a
|
|
233
|
+
// spoofed header must not satisfy this gate either. Mirrors runOwnerOk's verified check below. (BFF/gated: same.)
|
|
150
234
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
151
235
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
152
236
|
return;
|
|
153
237
|
}
|
|
154
238
|
const run = await deps.runStore.getRun(taskId);
|
|
239
|
+
// NB: `runOwnerOk` is short-circuited away when `!run`, so the unknown-run case MUST send its own
|
|
240
|
+
// response — otherwise the request never completes (hung socket, invisible to access log/metrics).
|
|
155
241
|
if (!run) {
|
|
156
242
|
sendError(res, 404, "not_found.run", runNotFoundMessage(taskId));
|
|
157
243
|
return;
|
|
@@ -159,15 +245,25 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
159
245
|
if (!runOwnerOk(req, res, run.owner))
|
|
160
246
|
return;
|
|
161
247
|
if (!runSessionAcceptOk(req, res, run, runMatch[2] ? "run.events" : "run.poll"))
|
|
162
|
-
return;
|
|
248
|
+
return; // [1499] accept-phase
|
|
163
249
|
if (runMatch[2]) {
|
|
164
250
|
await streamRunEvents(req, res, deps.runStore, taskId, deps.config.runStaleSec * 1000);
|
|
165
251
|
}
|
|
166
252
|
else {
|
|
167
253
|
const stale = run.status === "running" && Date.now() - new Date(run.updatedAt).getTime() > deps.config.runStaleSec * 1000;
|
|
254
|
+
// design/80 b (item 3): the supervisor cost surface = core's LLM costBreakdown + the service's infra axes.
|
|
255
|
+
// Only when infra pricing is configured (else the client reads stats.costBreakdown directly) AND the run is
|
|
256
|
+
// terminal with stats — reads the event log ONCE for the tool-call count + uses the run's wall-clock span as
|
|
257
|
+
// the sandbox-walltime proxy. Skipped for running/no-rate runs (no per-poll events read in the common case).
|
|
168
258
|
const infraRates = deps.config.infraCostRates;
|
|
169
259
|
const needCost = Boolean(run.result?.stats && !stale && infraRates && hasInfraPricing(infraRates));
|
|
260
|
+
// E12: surface post-completion prompt suggestions on the poll (the `await suggestions()` retrieval). They land
|
|
261
|
+
// as a `suggestions` event AFTER `done` (post-terminal, fire-and-forget) — so NO live events tail (which closes
|
|
262
|
+
// at the terminal status) can carry them; a poller reads them HERE once the pass settles. Only on a COMPLETED
|
|
263
|
+
// run (the sole status with suggestions), so a running poll never pays the events read.
|
|
170
264
|
const needSuggestions = !stale && run.status === "completed";
|
|
265
|
+
// Read the event log ONCE if either consumer needs it (skipped for running / no-rate to avoid a per-poll read).
|
|
266
|
+
// getEvents is a RunStore staple, but guard its presence so a degraded/partial store just omits these extras.
|
|
171
267
|
const events = (needCost || needSuggestions) && deps.runStore.getEvents ? await deps.runStore.getEvents(taskId, 0).catch(() => []) : undefined;
|
|
172
268
|
let supervisorCost;
|
|
173
269
|
if (needCost && events && run.result?.stats) {
|
|
@@ -178,8 +274,13 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
178
274
|
}
|
|
179
275
|
let suggestions;
|
|
180
276
|
if (needSuggestions && events) {
|
|
181
|
-
const sug = [...events].reverse().find((e) => e.type === "suggestions");
|
|
277
|
+
const sug = [...events].reverse().find((e) => e.type === "suggestions"); // last wins (a re-run could re-emit)
|
|
182
278
|
const arr = sug?.data?.suggestions;
|
|
279
|
+
// 🔴 **事件在场 ⇒ 已结算**,空数组必须原样透出(2026-07-26)。此前这里要求 `arr.length`,
|
|
280
|
+
// 于是「跑完了、确实一条都没有」与「还没跑完」都变成 `undefined` ⇒ **轮询方无从判断该不该停**。
|
|
281
|
+
// 与发射侧那半(`runs.ts` 此前 `if (suggestions.length)` 才 append)是同一个态被两处叠加抹平。
|
|
282
|
+
// ⚠️ 这属于本仓刚修过的**同一类**:消费方拿终态当停止条件,而数据在终态之后才到
|
|
283
|
+
// (`setTerminal` 在 suggestions 落之前)。判据统一成:事件在 = 已结算;事件不在 = 仍在跑。
|
|
183
284
|
if (Array.isArray(arr))
|
|
184
285
|
suggestions = arr.map((s) => String(s));
|
|
185
286
|
}
|
|
@@ -189,7 +290,11 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
189
290
|
status: stale ? "failed" : run.status,
|
|
190
291
|
result: run.result ?? undefined,
|
|
191
292
|
supervisorCost,
|
|
192
|
-
suggestions,
|
|
293
|
+
suggestions, // E12: present once the post-completion pass settled (undefined while pending / if none / off)
|
|
294
|
+
// Surface the structured failure code at the top level so async-run clients (OA / team skill) can
|
|
295
|
+
// branch (auth → stop, budget.*/limit.* → quota, network·server → retry/failover) without parsing
|
|
296
|
+
// the whole result. It already lives inside the persisted result JSON — no run_store schema change.
|
|
297
|
+
// A `_stale` infra timeout is not a core error, so it carries no errorCode.
|
|
193
298
|
errorCode: stale ? undefined : (run.result?.errorCode ?? undefined),
|
|
194
299
|
error: stale ? "run stalled (instance lost?)" : run.error ?? undefined,
|
|
195
300
|
jobId: run.jobId ?? undefined,
|
|
@@ -198,12 +303,19 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
198
303
|
}
|
|
199
304
|
return;
|
|
200
305
|
}
|
|
306
|
+
// POST /v1/runs/:id/cancel — stop a durable async run. Durable cross-replica flag (the owning instance's
|
|
307
|
+
// heartbeat tick honors it) + a same-replica AbortController fast path. Terminal = "failed"+errorCode
|
|
308
|
+
// "cancelled". Idempotent on a terminal run. [868] a SUSPENDED run is cancelled here too: settle the
|
|
309
|
+
// pending checkpoint (CAS expire) then terminalize the row — see cancelSuspended below.
|
|
201
310
|
const cancelMatch = req.method === "POST" ? RUN_CANCEL_RE.exec(url) : null;
|
|
202
311
|
if (cancelMatch) {
|
|
203
312
|
if (!deps.runStore) {
|
|
204
313
|
sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
|
|
205
314
|
return;
|
|
206
315
|
}
|
|
316
|
+
// Principal check BEFORE the lookup (parity with GET /v1/runs/:id): no 404-vs-401 existence oracle.
|
|
317
|
+
// 🔴 F-fix: gatedPrincipal (VERIFIED), not the spoofable header (parity with runOwnerOk below + the GET gate) —
|
|
318
|
+
// else a direct-door owner authenticating by JWT (no x-agent-principal header) is wrongly 401'd.
|
|
207
319
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
208
320
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
209
321
|
return;
|
|
@@ -211,13 +323,27 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
211
323
|
const taskId = cancelMatch[1];
|
|
212
324
|
const run = await deps.runStore.getRun(taskId);
|
|
213
325
|
if (!run) {
|
|
326
|
+
// [888]① 文案:这个 face 覆盖本部署 run store 里的全部 run(bg/durable + interactive sync 腿都建行、
|
|
327
|
+
// 都注册快路)——404 只剩「id 属于别的进程/别的 store」一类(如另起的 server 进程配 in-memory store)。
|
|
214
328
|
sendError(res, 404, "not_found.run", "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)");
|
|
215
329
|
return;
|
|
216
330
|
}
|
|
217
331
|
if (!runOwnerOk(req, res, run.owner))
|
|
218
|
-
return;
|
|
332
|
+
return; // owner mismatch → 404 (no existence oracle for non-owners)
|
|
219
333
|
if (!runSessionAcceptOk(req, res, run, "run.cancel"))
|
|
220
|
-
return;
|
|
334
|
+
return; // [1499] accept-phase
|
|
335
|
+
// [868] P0: cancel of a SUSPENDED run — the user's recovery handle for a locked session. Previously a flat
|
|
336
|
+
// 409 "deny the pending approval instead", which dead-ended when the deny itself failed or was swallowed
|
|
337
|
+
// (the incident: an AskUserQuestion durable park whose shell deny was lost kept the task_active claim with
|
|
338
|
+
// ZERO release paths on the file/memory lane — every later submit on the session 409'd forever). New
|
|
339
|
+
// semantics: settle the pending checkpoint FIRST via the CheckpointStore CAS `expire` (the reaper's
|
|
340
|
+
// "≈ deny" terminal — deliberately NOT resolve/resumeCheckpoint, which would drive a model continuation:
|
|
341
|
+
// cancel means the run DIES; a deny-RESUME feeds the refusal back to the model and burns tokens against
|
|
342
|
+
// the user's intent, and denying an AskUserQuestion is incoherent anyway — the reapExpired kind-split
|
|
343
|
+
// already encodes that), THEN terminalize the run row (setTerminal releases task_active = unlocks the
|
|
344
|
+
// session). Order is load-bearing: checkpoint-first means a late approve loses the CAS and is rejected
|
|
345
|
+
// honestly by /decide; terminal-first would open the window where an approve wins the CAS on a dead row
|
|
346
|
+
// and driveResume ghosts against markResuming (safe, but a lie in two ledgers).
|
|
221
347
|
const rs = deps.runStore;
|
|
222
348
|
const cancelSuspended = async () => {
|
|
223
349
|
const cs = deps.checkpointStore;
|
|
@@ -226,20 +352,34 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
226
352
|
const token = await cs.findPendingTokenBySession(run.sessionId);
|
|
227
353
|
if (token) {
|
|
228
354
|
const cp = await cs.get(token);
|
|
355
|
+
// CAS pending→expired; `false` = a concurrent decide (or the reaper) settled the gate first.
|
|
356
|
+
// Winning it means NO model leg can run off this gate: an approve that already won markResuming
|
|
357
|
+
// (row=running) has its resumeStream reject on the now-expired checkpoint BEFORE any model call and
|
|
358
|
+
// settles the row failed itself — our unconditional setTerminal below double-writes the same
|
|
359
|
+
// terminal (harmless; claim releases once). 双腿并跑 is impossible on this path (codex H1 review).
|
|
229
360
|
const won = cp ? await cs.expire(token, cp.scope) : false;
|
|
230
361
|
if (!won) {
|
|
362
|
+
// CAS lost — answer by the run's ACTUAL state, never pretend the cancel landed.
|
|
231
363
|
const now = await rs.getRun(taskId);
|
|
232
364
|
const st = now?.status;
|
|
233
365
|
if (st === "suspended" || st === "needs_review" || st === "running") {
|
|
366
|
+
// 文案中性覆盖两因(复验镜头:输家可能是 decide 也可能是 reaper 的 expire——后者无人 resuming)
|
|
234
367
|
sendError(res, 409, "conflict.approval_settled", "pending approval was settled concurrently (decided or expired) — re-check the run and retry cancel if it is still active", { taskId, status: st });
|
|
235
368
|
}
|
|
236
369
|
else {
|
|
237
|
-
sendJson(res, 202, { taskId, status: st ?? "failed", note: "run already terminal — cancel is a no-op" });
|
|
370
|
+
sendJson(res, 202, { taskId, status: st ?? "failed", note: "run already terminal — cancel is a no-op" }); // idempotent
|
|
238
371
|
}
|
|
239
372
|
return;
|
|
240
373
|
}
|
|
241
374
|
}
|
|
242
375
|
else {
|
|
376
|
+
// NO pending token. Two very different worlds share this face (codex H1):
|
|
377
|
+
// (a) the lockup fingerprint — deny lost / checkpoint expired-or-reaped, row parked forever;
|
|
378
|
+
// (b) a concurrent approve ALREADY resolved the checkpoint and its resume leg is running — an
|
|
379
|
+
// unconditional setTerminal here would kill/orphan that leg AND release the claim (双腿并跑).
|
|
380
|
+
// Disambiguate with the SAME CAS the resume path uses: markResuming flips ONLY a suspended row.
|
|
381
|
+
// Winning it = no resume leg exists nor can start (approve's own markResuming will now lose) — the
|
|
382
|
+
// row is OURS to terminalize. Losing it = answer by the run's actual state, never pretend.
|
|
243
383
|
const claimed = await rs.markResuming(taskId);
|
|
244
384
|
if (!claimed) {
|
|
245
385
|
const now = await rs.getRun(taskId);
|
|
@@ -248,7 +388,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
248
388
|
sendError(res, 409, "conflict.approval_settled", "pending approval was settled concurrently (decided or expired) — re-check the run and retry cancel if it is still active", { taskId, status: st });
|
|
249
389
|
}
|
|
250
390
|
else {
|
|
251
|
-
sendJson(res, 202, { taskId, status: st ?? "failed", note: "run already terminal — cancel is a no-op" });
|
|
391
|
+
sendJson(res, 202, { taskId, status: st ?? "failed", note: "run already terminal — cancel is a no-op" }); // idempotent
|
|
252
392
|
}
|
|
253
393
|
return;
|
|
254
394
|
}
|
|
@@ -256,15 +396,23 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
256
396
|
}
|
|
257
397
|
}
|
|
258
398
|
else {
|
|
399
|
+
// Minimal deployments without a checkpoint store: the run row is the only ledger — release it, say so.
|
|
259
400
|
note = "cancelled while suspended (no checkpoint store on this deployment — run row terminalized only)";
|
|
260
401
|
}
|
|
402
|
+
// Run-row terminal AFTER the checkpoint settle. Two ledgers, no silent tear: if this write fails after
|
|
403
|
+
// the checkpoint already expired, report exactly how far the cancel got — a retry takes the
|
|
404
|
+
// no-pending-token path above and completes the release (idempotent recovery). The row's error text
|
|
405
|
+
// = the SAME per-path truth as the response note (a stale-park release must not claim it settled a gate).
|
|
261
406
|
const err = note;
|
|
262
407
|
try {
|
|
263
408
|
const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
|
|
409
|
+
// [888] 低危残账修:撕裂窗自愈——checkpoint 已 settle 后 setTerminal 一次瞬时失败(store 抖动)曾直接
|
|
410
|
+
// 500,把释放推给 reapStale 的整个 stale 窗(no-token 路的重试读到 running 只会挂 requestCancel 旗,
|
|
411
|
+
// 无人消费)。这里就地有界重试(2 次退避)把瞬时抖动吞掉;仍然失败才 500 交 reapStale 兜底。
|
|
264
412
|
let lastErr;
|
|
265
413
|
for (let attempt = 0;; attempt++) {
|
|
266
414
|
try {
|
|
267
|
-
await rs.setTerminal(taskId, "failed", result, err);
|
|
415
|
+
await rs.setTerminal(taskId, "failed", result, err); // TaskStatus has no 'cancelled' member — failed + errorCode 'cancelled' is the codebase-wide cancel terminal (same shape as the resume-abort settle)
|
|
268
416
|
lastErr = undefined;
|
|
269
417
|
break;
|
|
270
418
|
}
|
|
@@ -279,9 +427,14 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
279
427
|
throw lastErr;
|
|
280
428
|
}
|
|
281
429
|
catch (e) {
|
|
430
|
+
// 文案与 note 同源(复验镜头:no-token/无 cs 路没有 settle 任何 gate,固定文案会失真);另注:
|
|
431
|
+
// no-token 路的重试会读到 running 走 requestCancel 旗,实际释放由 reapStale 兜底(有界延迟)。
|
|
282
432
|
sendError(res, 500, "internal.cancel_not_terminalized", `cancel progressed (${note}) but could not terminalize the run row (retried) — retry cancel; a stuck row is reaped after the stale window (${e instanceof Error ? e.message : String(e)})`, { taskId });
|
|
283
433
|
return;
|
|
284
434
|
}
|
|
435
|
+
// [1.207 codex M2] setTerminal 是 void 且 first-writer-wins 后 CAS 写打空不抛——退避窗内 reaper/
|
|
436
|
+
// 并发腿可能先终态化(如 approval.expired),我方写打空却仍答 "cancelled" 就是响应与账本不一致。
|
|
437
|
+
// 写后复读,照实际行应答(锁一样释放了;谁先写赢谁的 errorCode)。
|
|
285
438
|
const finalRow = await rs.getRun(taskId).catch(() => undefined);
|
|
286
439
|
if (finalRow && finalRow.errorCode !== "cancelled" && (finalRow.status === "failed" || finalRow.status === "completed" || finalRow.status === "blocked" || finalRow.status === "timeout")) {
|
|
287
440
|
sendJson(res, 202, { taskId, status: finalRow.status, errorCode: finalRow.errorCode ?? null, note: `session unlocked; the run was terminalized concurrently (${finalRow.errorCode ?? finalRow.status}) before this cancel's write — reporting the actual ledger state` });
|
|
@@ -290,17 +443,22 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
290
443
|
sendJson(res, 202, { taskId, status: "failed", errorCode: "cancelled", note });
|
|
291
444
|
};
|
|
292
445
|
if (run.status === "running") {
|
|
293
|
-
|
|
446
|
+
// Durable flag FIRST (only flips a still-running row), abort after: if the run suspended/terminal'd
|
|
447
|
+
// between the getRun above and here, the flag write affects 0 rows — re-read and answer by the run's
|
|
448
|
+
// ACTUAL state instead of lying "cancelling" about a run nothing will cancel.
|
|
449
|
+
const flagged = await deps.runStore.requestCancel(taskId, run.owner); // owner guard: single-DB defense-in-depth (HTTP gate already passed runOwnerOk)
|
|
294
450
|
if (!flagged) {
|
|
295
451
|
const now = await deps.runStore.getRun(taskId);
|
|
296
452
|
if (now?.status === "suspended") {
|
|
297
|
-
await cancelSuspended();
|
|
453
|
+
await cancelSuspended(); // [868] lost race INTO suspended — same recovery handle as the direct branch
|
|
298
454
|
}
|
|
299
455
|
else {
|
|
300
456
|
sendJson(res, 202, { taskId, status: now?.status ?? "failed", note: "run already terminal — cancel is a no-op" });
|
|
301
457
|
}
|
|
302
458
|
return;
|
|
303
459
|
}
|
|
460
|
+
// fast path: cancel landed on the running instance → abort now. Label FIRST ([1.207 codex H2]) so the
|
|
461
|
+
// interactive legs' settle projection sees the verb origin (bg legs already write "cancelled" themselves).
|
|
304
462
|
if (inflightRuns.has(taskId)) {
|
|
305
463
|
cancelledViaVerb.add(taskId);
|
|
306
464
|
inflightRuns.get(taskId).abort();
|
|
@@ -308,27 +466,53 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
308
466
|
sendJson(res, 202, { taskId, status: "cancelling" });
|
|
309
467
|
}
|
|
310
468
|
else if (run.status === "suspended" || run.status === "needs_review") {
|
|
469
|
+
// [868] settle the pending gate + terminalize + unlock (was a flat 409 dead-end). needs_review rides the
|
|
470
|
+
// SAME handle (codex M1): a plan-review park holds the claim identically and the old else branch lied
|
|
471
|
+
// "already terminal" about it (claim never released).
|
|
311
472
|
await cancelSuspended();
|
|
312
473
|
}
|
|
313
474
|
else {
|
|
314
|
-
sendJson(res, 202, { taskId, status: run.status, note: "run already terminal — cancel is a no-op" });
|
|
475
|
+
sendJson(res, 202, { taskId, status: run.status, note: "run already terminal — cancel is a no-op" }); // idempotent
|
|
315
476
|
}
|
|
316
477
|
return;
|
|
317
478
|
}
|
|
479
|
+
// POST /v1/runs/:id/steer — inject a mid-task steering message (CC-parity "运行中插话", design/47 + design/80
|
|
480
|
+
// D-A). Delivery is picked by the run's CURRENT state, surfaced via the status code (matching the spec):
|
|
481
|
+
// • in-flight on THIS replica (a live core TaskStream in `steerableRuns`) → `stream.steer()` queues the
|
|
482
|
+
// message into the harness, drained at the next turn boundary → 200 (applied).
|
|
483
|
+
// • durably SUSPENDED (any replica) → `setPendingSteer` parks it on the pending checkpoint, injected by core
|
|
484
|
+
// on the resume continuation → 202 (queued).
|
|
485
|
+
// • running on ANOTHER replica (not live here, status "running") → 409 steering.not_running. Cross-replica
|
|
486
|
+
// live-steer (a durable steer-signal + heartbeat poll, mirroring cancel_requested/preempt_requested) is a
|
|
487
|
+
// fast-follow — single-replica-per-worker deployment today, so the live + parked paths cover the headline
|
|
488
|
+
// use case; this branch is the honest seam, never a silent drop.
|
|
489
|
+
// • terminal (done/failed/cancelled/needs_review) → 409 steering.not_running.
|
|
490
|
+
// Content carrying a `</system-reminder>` break-out or control chars → 422 steering.invalid_content, validated
|
|
491
|
+
// ONCE up front with core's `validatePendingSteer` (the same validator setPendingSteer runs) so the 422 is
|
|
492
|
+
// consistent across both delivery paths — deliberately STRICTER than core's live path (which would sanitize an
|
|
493
|
+
// untrusted break-out at injection rather than reject it), trading that edge case for a uniform wire contract.
|
|
318
494
|
const steerMatch = req.method === "POST" ? RUN_STEER_RE.exec(url) : null;
|
|
319
495
|
if (steerMatch) {
|
|
320
496
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
321
|
-
return;
|
|
497
|
+
return; // mutating + hits TiDB / runs the model
|
|
322
498
|
if (!deps.runStore) {
|
|
323
499
|
sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
|
|
324
500
|
return;
|
|
325
501
|
}
|
|
502
|
+
// Per-tenant identity MUST come from gatedPrincipal (direct-door secure single point), NOT principalFrom:
|
|
503
|
+
// `trusted` carries OPERATOR authority (a trusted steer may ride a <system-reminder>), so deriving it from a
|
|
504
|
+
// spoofable header on a direct-door worker would be a privilege escalation (HANDOFF 铁律 + core §3 inv #1).
|
|
326
505
|
const principal = gatedPrincipal(req, deps.config);
|
|
327
506
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
328
507
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
329
508
|
return;
|
|
330
509
|
}
|
|
331
510
|
const taskId = steerMatch[1];
|
|
511
|
+
// Parse + validate the REQUEST before the run lookup: a bad body is the caller's error regardless of run
|
|
512
|
+
// state, and validating once here (not per delivery path) keeps 400/422 identical whether the run is live or
|
|
513
|
+
// suspended. `text` required non-empty; `mode` (CC `--steering-mode`) is accepted + enum-checked but a v1
|
|
514
|
+
// no-op — core's public TaskStream.steer takes only { trusted } today (harness steeringMode is build-time),
|
|
515
|
+
// so per-call mode is forward-draft pending core wiring (raised to core).
|
|
332
516
|
let body;
|
|
333
517
|
try {
|
|
334
518
|
body = (await readJson(req));
|
|
@@ -345,16 +529,23 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
345
529
|
sendError(res, 400, "request.field_invalid", "mode must be 'all' or 'one-at-a-time' when present");
|
|
346
530
|
return;
|
|
347
531
|
}
|
|
532
|
+
// E15 (shell-host contract): per-message queue priority. Accepted + enum-validated FAIL-LOUD, but ADVISORY
|
|
533
|
+
// today — core's steer is single-slot last-writer-wins with no per-message priority / addressable drop, so the
|
|
534
|
+
// faithful priority queue + DELETE /v1/runs/:id/queue/:messageId wait on a core seam (raised to core).
|
|
348
535
|
if (body.priority !== undefined && body.priority !== "now" && body.priority !== "next" && body.priority !== "later") {
|
|
349
536
|
sendError(res, 400, "request.field_invalid", "priority must be 'now', 'next', or 'later' when present");
|
|
350
537
|
return;
|
|
351
538
|
}
|
|
352
539
|
const text = body.text;
|
|
353
540
|
const priority = body.priority;
|
|
541
|
+
// Mint a server-side messageId so the shell can reference this steer (its drop handle once the core queue seam
|
|
542
|
+
// lands). Honest + stable now even while `priority` is a no-op forward-draft.
|
|
354
543
|
const messageId = uuidv7();
|
|
544
|
+
// trusted = explicit operator (NOT bare isOperator — its empty-list "true-for-all" would let any caller send a
|
|
545
|
+
// system-authority steer). A normal owner's steer reaches the model as a plain user message (trusted:false).
|
|
355
546
|
const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
356
547
|
try {
|
|
357
|
-
validatePendingSteer({ text, trusted });
|
|
548
|
+
validatePendingSteer({ text, trusted }); // throws CheckpointError(steering.invalid_content) on break-out / control chars
|
|
358
549
|
}
|
|
359
550
|
catch (e) {
|
|
360
551
|
if (e instanceof CheckpointError && e.code === "steering.invalid_content") {
|
|
@@ -368,12 +559,19 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
368
559
|
sendError(res, 404, "not_found.run", runNotFoundMessage(taskId));
|
|
369
560
|
return;
|
|
370
561
|
}
|
|
562
|
+
// Owner-gate (parity with preempt): an EXPLICIT operator may steer any tenant's run (supervisor intervention);
|
|
563
|
+
// a normal caller only its own. 404 (not 403) for a non-owner — no existence oracle.
|
|
371
564
|
if (!trusted && run.owner !== null && run.owner !== principal) {
|
|
372
565
|
sendError(res, 404, "not_found.run", "run not found");
|
|
373
566
|
return;
|
|
374
567
|
}
|
|
375
568
|
if (!runSessionAcceptOk(req, res, run, "run.steer"))
|
|
376
|
-
return;
|
|
569
|
+
return; // [1499] accept-phase
|
|
570
|
+
// Durable-park helper: land the steer on this session's PENDING checkpoint. The checkpoint table — not the run
|
|
571
|
+
// row — is the AUTHORITY: core writes the pending checkpoint during a suspend (before the service flips the run
|
|
572
|
+
// row to "suspended"), and CASes on `status='pending'`, so this lands iff the run is genuinely parked. Read the
|
|
573
|
+
// checkpoint's ACTUAL scope ("_" for a no-principal run) then its token; a missing scope/token/CAS-loss means
|
|
574
|
+
// the checkpoint resolved/expired/never-existed → "no-checkpoint". "no-store" = no checkpoint store wired.
|
|
377
575
|
const tryPark = async () => {
|
|
378
576
|
const cs = deps.checkpointStore;
|
|
379
577
|
if (!cs)
|
|
@@ -388,6 +586,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
388
586
|
};
|
|
389
587
|
const sendParked = () => sendJson(res, 202, { taskId, status: "suspended", delivery: "queued", messageId, ...(priority ? { priority } : {}), note: "steer parked on the checkpoint — injected when the run resumes" });
|
|
390
588
|
const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
|
|
589
|
+
// (a) Same-replica live path: the run is streaming here → inject now (drained at the next turn boundary).
|
|
391
590
|
const live = steerableRuns.get(taskId);
|
|
392
591
|
if (live) {
|
|
393
592
|
try {
|
|
@@ -402,7 +601,10 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
402
601
|
return;
|
|
403
602
|
}
|
|
404
603
|
if (code !== "steering.not_running")
|
|
405
|
-
throw e;
|
|
604
|
+
throw e; // unexpected → outer handler 500
|
|
605
|
+
// Raced: the stream finished between the map lookup and steer(). It may have durably SUSPENDED in the SAME
|
|
606
|
+
// tick (core already wrote the pending checkpoint) — fall through to the durable park rather than a spurious
|
|
607
|
+
// 409 (the live→suspended transition window). Only a genuinely-terminal run → not_running.
|
|
406
608
|
if (await tryPark() === "parked") {
|
|
407
609
|
sendParked();
|
|
408
610
|
return;
|
|
@@ -411,6 +613,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
411
613
|
return;
|
|
412
614
|
}
|
|
413
615
|
}
|
|
616
|
+
// (b) Durably suspended → park the steer on the pending checkpoint (drained on resume).
|
|
414
617
|
if (run.status === "suspended") {
|
|
415
618
|
const outcome = await tryPark();
|
|
416
619
|
if (outcome === "no-store") {
|
|
@@ -424,16 +627,27 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
424
627
|
sendNotRunning("run is no longer suspended (resolved or expired)");
|
|
425
628
|
return;
|
|
426
629
|
}
|
|
630
|
+
// (c) running-elsewhere (cross-replica, fast-follow seam) → not_running.
|
|
427
631
|
if (run.status === "running") {
|
|
428
632
|
sendNotRunning("run is active on another replica — cross-replica live-steer is not yet supported");
|
|
429
633
|
return;
|
|
430
634
|
}
|
|
635
|
+
// (d) design/144 wake 半场(core 1.283):TERMINAL run + steer = 不再一律 409——铸一个部署侧
|
|
636
|
+
// task_done 纯 park checkpoint(PendingAction {kind:"task_done"},deployment-minted per [770]①c),
|
|
637
|
+
// 把消息 park 上去,等 POST /v1/sessions/:id/wake({gate:"wake"})把它作为续跑首轮输入消费。
|
|
638
|
+
// 先 tryPark:session 若已有 pending checkpoint(needs_review park / 先前铸的 wake park)直接park上去,
|
|
639
|
+
// 绝不双铸(一 session 一 pending 不变式)。铸造前置=checkpoint store + session leaf(leafId 锚续跑);
|
|
640
|
+
// 缺任一 = 原 409 契约(env-only/无树面诚实拒绝)。
|
|
431
641
|
if (deps.checkpointStore && deps.sessionStorage?.getLeafId) {
|
|
642
|
+
// 复审 MED(陈旧 run 守卫):目标 run 终态但同 session 另有 ACTIVE run(claim 在)时,消息该去
|
|
643
|
+
// 活 run(live steer / suspended park),给这条旧终态 run 铸 park 只会造出基于活 leaf 的陈旧
|
|
644
|
+
// checkpoint + wake 撞 markResuming 409。诚实拒绝并指路。
|
|
432
645
|
const activeTaskId = await deps.runStore.getActiveTaskId?.(run.sessionId).catch(() => undefined);
|
|
433
646
|
if (activeTaskId !== undefined && activeTaskId !== null && activeTaskId !== taskId) {
|
|
434
647
|
sendNotRunning(`run is ${run.status} and its session has an ACTIVE run (${activeTaskId}) — steer that run instead`);
|
|
435
648
|
return;
|
|
436
649
|
}
|
|
650
|
+
// per-session 串行(复审 HIGH:并发双终态 steer 的 tryPark→put 窗口双铸两个 pending park)。
|
|
437
651
|
const prior = wakeParkMints.get(run.sessionId) ?? Promise.resolve();
|
|
438
652
|
const mintResult = { parked: false };
|
|
439
653
|
const job = prior.then(async () => {
|
|
@@ -441,11 +655,11 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
441
655
|
if (await tryPark() === "parked") {
|
|
442
656
|
mintResult.parked = true;
|
|
443
657
|
return;
|
|
444
|
-
}
|
|
658
|
+
} // 锁内重查:前一位刚铸完 → 直接 park 上去
|
|
445
659
|
const leafId = await Promise.resolve(deps.sessionStorage.getLeafId(run.sessionId)).catch(() => undefined);
|
|
446
660
|
if (leafId === undefined || leafId === null)
|
|
447
661
|
return;
|
|
448
|
-
const cpScope = run.owner ?? "_";
|
|
662
|
+
const cpScope = run.owner ?? "_"; // 匿名提交哨兵,与 main.ts putCtx 的 principal ?? "_" 同域
|
|
449
663
|
const wakeToken = mintCheckpointToken();
|
|
450
664
|
await cs.put(wakeToken, {
|
|
451
665
|
token: wakeToken,
|
|
@@ -459,6 +673,8 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
459
673
|
createdAt: Date.now(),
|
|
460
674
|
sourceTaskId: taskId,
|
|
461
675
|
});
|
|
676
|
+
// 跨副本纵深:put 后复核赢家——若另一副本抢先铸了别的 pending,expire 我方孤儿并把消息
|
|
677
|
+
// park 到赢家上(收敛到单 park;店无序时收敛到店的稳定扫描序,残窗如实记 fast-follow)。
|
|
462
678
|
const winner = await cs.findPendingTokenBySession(run.sessionId).catch(() => null);
|
|
463
679
|
if (winner !== null && winner !== wakeToken) {
|
|
464
680
|
await cs.expire(wakeToken, cpScope).catch(() => undefined);
|
|
@@ -475,6 +691,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
475
691
|
await job;
|
|
476
692
|
}
|
|
477
693
|
finally {
|
|
694
|
+
// 链尾自清:没有更晚的排队者(Map 仍指向本包装)才删,防长期泄漏;有排队者则由其链尾清。
|
|
478
695
|
if (wakeParkMints.get(run.sessionId) === wrapped)
|
|
479
696
|
wakeParkMints.delete(run.sessionId);
|
|
480
697
|
}
|
|
@@ -486,15 +703,20 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
486
703
|
sendNotRunning(`run is ${run.status} — not accepting steers`);
|
|
487
704
|
return;
|
|
488
705
|
}
|
|
706
|
+
// K-1c (shell-host §K): POST /v1/runs/:id/compact — MANUAL compaction (un-greys the shell's `/compact`). core
|
|
707
|
+
// 1.156 added `TaskStream.compact()`: it forces a compaction at the next SAFE turn boundary (never mid-turn) and
|
|
708
|
+
// emits `compacted{trigger:"manual"}` on the run's OWN stream (the result rides that stream, like steer). So this
|
|
709
|
+
// verb only applies to a run LIVE on THIS replica (`steerableRuns`); a suspended/terminal/other-replica run has no
|
|
710
|
+
// live stream + there is NO durable "pending compact" in core (unlike steer's setPendingSteer) → 409.
|
|
489
711
|
const compactMatch = req.method === "POST" ? RUN_COMPACT_RE.exec(url) : null;
|
|
490
712
|
if (compactMatch) {
|
|
491
713
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
492
|
-
return;
|
|
714
|
+
return; // mutating + runs the model (compaction summarizes)
|
|
493
715
|
if (!deps.runStore) {
|
|
494
716
|
sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
|
|
495
717
|
return;
|
|
496
718
|
}
|
|
497
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
719
|
+
const principal = gatedPrincipal(req, deps.config); // direct-door-secure identity, never the spoofable header
|
|
498
720
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
499
721
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
500
722
|
return;
|
|
@@ -505,15 +727,22 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
505
727
|
sendError(res, 404, "not_found.run", "run not found");
|
|
506
728
|
return;
|
|
507
729
|
}
|
|
730
|
+
// Owner-gate (parity with steer/preempt): an explicit operator may compact any tenant's run; a normal caller
|
|
731
|
+
// only its own. 404 (not 403) for a non-owner — no existence oracle.
|
|
508
732
|
const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
509
733
|
if (!operator && run.owner !== null && run.owner !== principal) {
|
|
510
734
|
sendError(res, 404, "not_found.run", "run not found");
|
|
511
735
|
return;
|
|
512
736
|
}
|
|
513
737
|
if (!runSessionAcceptOk(req, res, run, "run.compact"))
|
|
514
|
-
return;
|
|
738
|
+
return; // [1499] accept-phase
|
|
739
|
+
// core 1.293:compact(opts) 收 { instructions?, signal? }。instructions=壳 /compact 的定向摘要指令,
|
|
740
|
+
// 此处 additive 透传(空 body/无字段=现状);signal(取消腿)不在本 verb——取消需要独立动词+
|
|
741
|
+
// per-pending AbortController 台账,等壳侧 UX 契约定形再开(先到先记:[853] 知会件)。
|
|
742
|
+
// 校验在 live 查找之前(steer 同纪律):400 形状不依赖运行态,live/非 live 一致。
|
|
515
743
|
let compactBody;
|
|
516
744
|
try {
|
|
745
|
+
// HttpError(413 request body too large)原样透传,不降级成语义错误的 400(交叉评审 L5)
|
|
517
746
|
const parsed = await readJson(req);
|
|
518
747
|
if (parsed === null || typeof parsed !== "object") {
|
|
519
748
|
sendError(res, 400, "request.invalid_json", "body must be a JSON object when present");
|
|
@@ -529,6 +758,9 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
529
758
|
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
530
759
|
return;
|
|
531
760
|
}
|
|
761
|
+
// 上限=core COMPACTION_INSTRUCTIONS_MAX_CHARS(2048 码点,auto-compaction.js:9;超长 core 会静默
|
|
762
|
+
// 截断)——HTTP 面按同口径拒收,fail-loud 而非 202 后半截指令悄悄丢(交叉评审 M1)。码点计数与
|
|
763
|
+
// core 同形([...s] 展开),防代理对 emoji 类双单元字符的错杀/漏杀。
|
|
532
764
|
if (compactBody.instructions !== undefined && (typeof compactBody.instructions !== "string" || compactBody.instructions.length === 0 || [...compactBody.instructions].length > 2_048)) {
|
|
533
765
|
sendError(res, 400, "request.field_invalid", "instructions must be a non-empty string of at most 2048 characters (code points) when present — the engine caps compaction instructions there");
|
|
534
766
|
return;
|
|
@@ -539,10 +771,28 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
539
771
|
sendError(res, 409, "compact.not_running", run.status === "running" ? "run is active on another replica — manual compact is replica-local" : `run is ${run.status} — not accepting compaction`);
|
|
540
772
|
return;
|
|
541
773
|
}
|
|
542
|
-
|
|
774
|
+
// FIRE-AND-FORGET 202 (review): `compact()` resolves only once PROCESSED at the next SAFE turn boundary — which
|
|
775
|
+
// can be a whole in-flight turn away — so awaiting it would hold the HTTP connection (gateway-timeout risk + poor
|
|
776
|
+
// UX). Ack "accepted" immediately; the compaction runs at the boundary and a `compacted{trigger:"manual"}` event
|
|
777
|
+
// rides the run's OWN stream (which the client already consumes — this matches the shell's `compacting%` spinner
|
|
778
|
+
// waiting on that event). A NO-OP compaction (nothing to summarize) emits NO event (so the shell must time out its
|
|
779
|
+
// spinner, not block forever). Swallow the benign race where the stream finished between the lookup and the call
|
|
780
|
+
// (steering.not_running) + any compact error — best-effort; failure surfaces as the absence of the event, the user
|
|
781
|
+
// can retry. (core guarantees compact() never hangs — it resolves by task end at the latest.)
|
|
782
|
+
void live.compact(compactInstructions !== undefined ? { instructions: compactInstructions } : undefined).then(
|
|
783
|
+
// core 1.257 (MF-18 BREAKING): compact() now RESOLVES WITH a CompactOutcome instead of void
|
|
784
|
+
// — "processed" alone was silent when the attempt failed or was mooted (the 202-then-silence fingerprint).
|
|
785
|
+
// Consume the outcome: "compacted" is the only one that emits a `compacted{trigger:"manual"}` event on the
|
|
786
|
+
// stream; the other five (failed/mooted/noop/blocked/disabled) each have a `compaction.*` trace counterpart
|
|
787
|
+
// and produce NO event, so the shell's spinner must fall through. Log the discriminated outcome so a
|
|
788
|
+
// "202 → no event" incident reads its reason straight from the log (not a service-wire-vs-core guess).
|
|
789
|
+
(outcome) => {
|
|
543
790
|
const level = outcome === "failed" ? "warn" : "info";
|
|
544
791
|
deps.logger?.[level]?.("manual_compact_outcome", { taskId, outcome, note: outcome === "compacted" ? "a compacted{trigger:'manual'} event rode the run stream" : `no event (see compaction.${outcome === "mooted" ? "mooted" : outcome} trace / task terminal events)` });
|
|
545
792
|
}, (e) => {
|
|
793
|
+
// The not_running rejection was fully silent — correct for the benign settle race it was
|
|
794
|
+
// designed for, but a SYSTEMATIC not_running (e.g. a verb/handle mismatch on one lane) then has zero
|
|
795
|
+
// observability. Keep it non-warn (it IS usually benign) but leave a fingerprint.
|
|
546
796
|
if (e.code !== "steering.not_running") {
|
|
547
797
|
deps.logger?.warn?.("manual_compact_failed", { taskId, err: e instanceof Error ? e.message : String(e) });
|
|
548
798
|
}
|
|
@@ -553,15 +803,25 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
553
803
|
sendJson(res, 202, { taskId, status: "running", delivery: "accepted", note: "compaction will run at the next turn boundary; a compacted{trigger:'manual'} event rides the run stream if anything is summarized" });
|
|
554
804
|
return;
|
|
555
805
|
}
|
|
806
|
+
// core 1.207 (design/116 detach, CC mid-flight ctrl+b): POST /v1/runs/:id/detach {toolCallId} —
|
|
807
|
+
// move a RUNNING tool call to the background via `TaskStream.detach(toolCallId)`. Route (a) of the sync-leg
|
|
808
|
+
// detach verb design ((b)'s durable-leg fallback
|
|
809
|
+
// is blocked on TOC-local having no runStore). Core semantics: fire-and-forget + race-safe (a request landing
|
|
810
|
+
// before the tool reads its signal still detaches; after it finished = no-op; unknown toolCallId = no-op) —
|
|
811
|
+
// the detached Bash settles early with "moved to background; task_id=b*" + a `{type:"bash", detached:true}`
|
|
812
|
+
// structured card, and the G2b completion notification/spool take over. Only a detach-capable env honors it
|
|
813
|
+
// (`backgroundCapabilities.supportsDetach`; TOB envs ignore = fail-safe). Like compact, this verb is LIVE-only
|
|
814
|
+
// (replica-local `steerableRuns` handle — the sync leg now registers too): a suspended run has no running tool
|
|
815
|
+
// and there is no durable "pending detach" → 409.
|
|
556
816
|
const detachMatch = req.method === "POST" ? RUN_DETACH_RE.exec(url) : null;
|
|
557
817
|
if (detachMatch) {
|
|
558
818
|
if (rateLimited(req, res))
|
|
559
|
-
return;
|
|
819
|
+
return; // mutating, but runs no model (no quota gate — parity with cancel, not steer)
|
|
560
820
|
if (!deps.runStore) {
|
|
561
821
|
sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
|
|
562
822
|
return;
|
|
563
823
|
}
|
|
564
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
824
|
+
const principal = gatedPrincipal(req, deps.config); // direct-door-secure identity, never the spoofable header
|
|
565
825
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
566
826
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
567
827
|
return;
|
|
@@ -576,6 +836,10 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
576
836
|
return;
|
|
577
837
|
}
|
|
578
838
|
if (typeof body.toolCallId !== "string" || body.toolCallId.length === 0 || body.toolCallId.length > 256) {
|
|
839
|
+
// 🔒 The length cap is load-bearing (review MEDIUM): core's ToolDetachHub.request() is create-then-abort —
|
|
840
|
+
// an UNKNOWN id still allocates + retains a Map entry (keyed by the raw string) for the run's lifetime
|
|
841
|
+
// (gc only fires from a real tool's completion), so unbounded ids = per-run memory growth on a shared
|
|
842
|
+
// replica. Real core toolCallIds are short; 256 is generous.
|
|
579
843
|
sendError(res, 400, "request.body_shape", "body must be { toolCallId: string (non-empty, ≤256 chars) }");
|
|
580
844
|
return;
|
|
581
845
|
}
|
|
@@ -584,27 +848,50 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
584
848
|
sendError(res, 404, "not_found.run", "run not found");
|
|
585
849
|
return;
|
|
586
850
|
}
|
|
851
|
+
// Owner-gate (parity with steer/compact): an explicit operator may detach on any tenant's run; a normal
|
|
852
|
+
// caller only its own. 404 (not 403) for a non-owner — no existence oracle.
|
|
587
853
|
const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
588
854
|
if (!operator && run.owner !== null && run.owner !== principal) {
|
|
589
855
|
sendError(res, 404, "not_found.run", "run not found");
|
|
590
856
|
return;
|
|
591
857
|
}
|
|
592
858
|
if (!runSessionAcceptOk(req, res, run, "run.detach"))
|
|
593
|
-
return;
|
|
859
|
+
return; // [1499] accept-phase
|
|
594
860
|
const live = steerableRuns.get(taskId);
|
|
595
861
|
if (!live) {
|
|
596
862
|
sendError(res, 409, "detach.not_running", run.status === "running" ? "run is active on another replica — detach is replica-local" : `run is ${run.status} — no running tool call to detach`);
|
|
597
863
|
return;
|
|
598
864
|
}
|
|
865
|
+
// Fire-and-forget by CONTRACT (`detach(toolCallId): void`) — race-safety is core's; the outcome surfaces on
|
|
866
|
+
// the run's OWN stream (the early-settled tool_end with the `detached:true` structured card, then the b* task
|
|
867
|
+
// notification), which the client already consumes. 202 = "requested", honest about no confirmation here.
|
|
599
868
|
live.detach(body.toolCallId);
|
|
600
869
|
sendJson(res, 202, { taskId, toolCallId: body.toolCallId, delivery: "requested", note: "if the tool call is running in a detach-capable env it settles early with 'moved to background; task_id=b*'; otherwise the request is a no-op (fail-safe)" });
|
|
601
870
|
return;
|
|
602
871
|
}
|
|
603
872
|
miss.fell = true;
|
|
604
873
|
}
|
|
874
|
+
/** 同上纪律(子代理 / 任务句柄 / HITL 应答段)。 */
|
|
605
875
|
async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
606
876
|
const { deps } = ctx;
|
|
607
877
|
const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
|
|
878
|
+
// C2 (core 1.219) + design/122 (core 1.225): the Task-SUB-AGENT verbs of a run —
|
|
879
|
+
// POST /v1/runs/:runId/subagents/:target/steer — inject fenced guidance into a STILL-RUNNING child;
|
|
880
|
+
// POST /v1/runs/:runId/subagents/:target/resume — REVIVE a SETTLED child with a new prompt (CC dfe parity;
|
|
881
|
+
// always async: the revived child runs in the background on its retained session, completion announced
|
|
882
|
+
// via the deployment notify sink — requires the parent run to have set `retainSubagentSessions`).
|
|
883
|
+
// Shared plumbing (one trust gate): addressed by the PARENT run's taskId + the child's parentToolCallId (or
|
|
884
|
+
// agentName when unambiguous); replica-local handle registry (the legs' `onSubagentSpawn` sink; design/122 ③
|
|
885
|
+
// handles now live until LEG END, not child settle — resume is only legal after settle); owner-gated via the
|
|
886
|
+
// parent run row; content through `redactSteerIn`. 🔐 `handle.childSessionId` is a continuation capability
|
|
887
|
+
// — never in any response. Sync delegations only (core r1-M3: bg children have no handle).
|
|
888
|
+
// [1488]③(b): GET /v1/runs/:runId/subagents/:handle/output — the BACKGROUND child read face. The shell's
|
|
889
|
+
// fleet viewer gets bg_notification summaries only; the child's FINAL assistant body lives in the core
|
|
890
|
+
// TaskRegistry (what the TaskOutput tool reads). Auth = the runs-face read pattern (verified principal →
|
|
891
|
+
// owner-gate via the parent run row → honest 404, no existence oracle); access into the registry is the
|
|
892
|
+
// REGISTRATION tuple derived from the run row (owner=runId | sessionScoped via sessionId, scope=run
|
|
893
|
+
// owner ?? "default") — NEVER caller-supplied. Replica-local (the registry is in-process, like steer);
|
|
894
|
+
// non-blocking read: a still-running child returns its current status honestly, no long-poll (v1).
|
|
608
895
|
const subOutputMatch = req.method === "GET" ? RUN_SUBAGENT_OUTPUT_RE.exec(url) : null;
|
|
609
896
|
if (subOutputMatch) {
|
|
610
897
|
if (!deps.runStore || !deps.subagentTaskOutput) {
|
|
@@ -628,6 +915,22 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
628
915
|
sendError(res, 404, "not_found.run", "run not found");
|
|
629
916
|
return;
|
|
630
917
|
}
|
|
918
|
+
// [1493] SESSION-scoped content read. principal answers "is this your data at all" (whose); session
|
|
919
|
+
// answers "which conversation" (which). This verb returns CONVERSATION CONTENT (a child's final body), so
|
|
920
|
+
// the principal owner-gate above is necessary but NOT sufficient: under a SHARED principal (single-user
|
|
921
|
+
// multi-shell — the default local posture, and any tenant that shares an identity) it passes for every
|
|
922
|
+
// co-principal shell, so a SIBLING session could read another conversation's child (clay's dual-shell
|
|
923
|
+
// repro). The run belongs to a conversation (run.sessionId); a non-operator caller must PROVE it owns that
|
|
924
|
+
// conversation by asserting the matching `?session=`. Threat model is ACCIDENTAL cross-read within one
|
|
925
|
+
// user's own shells (no adversary — the shell honestly reports its own session), so an honest
|
|
926
|
+
// caller-asserted session is correct AND sufficient here (no unguessable capability needed). fable5 R1
|
|
927
|
+
// (deliberate boundary, do not "harden" this into a capability): yes, a co-principal caller COULD forge the
|
|
928
|
+
// target's session — but co-principal means the SAME authz identity, i.e. the same user reading their own
|
|
929
|
+
// data through another window; session is a content-ROUTING axis inside one principal, not an authz wall.
|
|
930
|
+
// Deployments with genuinely different humans MUST give them distinct principals (requirePrincipal) — the
|
|
931
|
+
// principal owner-gate above is the security boundary, and it runs first. An
|
|
932
|
+
// operator/trace caller bypasses (as it does the owner-gate). A run with no sessionId is not
|
|
933
|
+
// conversation-bound → principal gate only. Fail-CLOSED: a session-bound run with no/mismatched assertion → 404.
|
|
631
934
|
const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
|
|
632
935
|
if (!trusted && run.sessionId && callerSession !== run.sessionId) {
|
|
633
936
|
sendError(res, 404, "not_found.run", "run not found");
|
|
@@ -635,13 +938,28 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
635
938
|
}
|
|
636
939
|
const out = await deps.subagentTaskOutput(target, { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) });
|
|
637
940
|
const details = out.details;
|
|
941
|
+
// codex R1 (type gate): pollTask is the GENERIC registry face — same-tuple b*/m*/w* handles (bash
|
|
942
|
+
// stdout, monitor batches, workflow projections) would ride this verb otherwise, a broader egress
|
|
943
|
+
// than the documented agent-final-output contract. Only `background_agent` passes; everything else
|
|
944
|
+
// gets the SAME indistinguishable 404 (unknown / not-yours / wrong-kind — no oracle). wa* workflow-
|
|
945
|
+
// agent rows are OBSERVATION identities (never in the registry) — their read face is the workflow
|
|
946
|
+
// journal (GET /v1/workflows/:id/journal), and they land in the not_found arm here by construction.
|
|
638
947
|
if (details?.error === "not_found" || details?.type !== "background_agent") {
|
|
639
948
|
sendError(res, 404, "not_found.subagent", `no background agent "${target}" under this run (unknown handle, not this run's child, or already reaped — bg children live in the replica-local registry for the parent's lifetime; wa… workflow-agent rows are read via the workflow journal, not this verb)`);
|
|
640
949
|
return;
|
|
641
950
|
}
|
|
951
|
+
// Pass the registry's honest projection through: status/retrieval_status/partial flags verbatim,
|
|
952
|
+
// `content` = the TaskOutput-tool text (the child's final assistant body once terminal). UNTRUSTED
|
|
953
|
+
// model output, same posture as the runs face's `result`.
|
|
642
954
|
sendJson(res, 200, { taskId: runId, target, content: out.content, output: out.details });
|
|
643
955
|
return;
|
|
644
956
|
}
|
|
957
|
+
// S2([1520]{core} 提货,core 1.370 `bgAgentId`):GET /v1/runs/:id/subagents/:handle/stream —— per-agent
|
|
958
|
+
// **live tail**(SSE)。「replay+tail」的 tail 半场:replay/终报=上面的 output 面(durable 臂 1.250),
|
|
959
|
+
// 本面=从连接时刻起的 content 帧(text/reasoning delta、tool_start/end、task_progress——投影与 sync 主流
|
|
960
|
+
// forward 分支同 builder 同纪律,subagent-tail-bus.ts 顶注)。帧只在宿主 run 所在副本产生 ⇒ live 帧
|
|
961
|
+
// replica-local(meta 如实声明;他实例 running 的行=只 heartbeat,不虚构)。门=output 面逐字同形
|
|
962
|
+
// (principal→owner→session fail-closed;a* 形判;探测经同 seam,未知/非本 run/型错=同 404 无 oracle)。
|
|
645
963
|
const subStreamMatch = req.method === "GET" ? RUN_SUBAGENT_STREAM_RE.exec(url) : null;
|
|
646
964
|
if (subStreamMatch) {
|
|
647
965
|
if (!deps.runStore || !deps.subagentTaskOutput) {
|
|
@@ -665,12 +983,19 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
665
983
|
sendError(res, 404, "not_found.run", "run not found");
|
|
666
984
|
return;
|
|
667
985
|
}
|
|
986
|
+
// [1493] session 门(output 面同注,出生即强制——新面零消费者无软拍需要)。
|
|
668
987
|
const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
|
|
669
988
|
if (!trusted && run.sessionId && callerSession !== run.sessionId) {
|
|
670
989
|
sendError(res, 404, "not_found.run", "run not found");
|
|
671
990
|
return;
|
|
672
991
|
}
|
|
992
|
+
// 🔴 cli[1524] HIGH-2:**subscribe 先行,probe 在后**——原序(probe await→meta→subscribe)在
|
|
993
|
+
// probe 与 subscribe 之间留竞态窗:窗内子代 settle → task_settled 发布时无订阅者被丢(总线无
|
|
994
|
+
// replay/sticky)→ 订阅者只收 heartbeat 永挂。先建订阅,窗内 settle 自然落进队列;probe 判 404
|
|
995
|
+
// 时 it.return() 清订阅(未写任何帧,404 照常)。
|
|
673
996
|
const it = defaultSubagentTailBus.subscribe(target);
|
|
997
|
+
// 归属+型探测经 output 同 seam(含 1.250 durable 臂——跨实例行也探得到 status);not_found/型错=
|
|
998
|
+
// 同一不可区分 404(无 oracle)。探测是纯读(a* 形在 seam 内先过形判门;background_agent poll 无副作用)。
|
|
674
999
|
const probe = await deps.subagentTaskOutput(target, { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) });
|
|
675
1000
|
const probeDetails = probe.details;
|
|
676
1001
|
if (probeDetails?.error === "not_found" || probeDetails?.type !== "background_agent") {
|
|
@@ -679,22 +1004,30 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
679
1004
|
return;
|
|
680
1005
|
}
|
|
681
1006
|
sseHeaders(res);
|
|
1007
|
+
// meta 声明:live 帧 replica-local + replay 指引;status=探测快照(终态=不再有帧,发 meta 即收尾,
|
|
1008
|
+
// cli 拿到「走 replay 面」的信号而不是空挂)。seq=stop-cycle 世代号(settle 铸;core 1.377 起
|
|
1009
|
+
// **全域投影**——进程内 live 臂投当前复活周期号 [[1533] ask 兑现,[1548]①],durable 终态 serve
|
|
1010
|
+
// 同键同义;老 core 只在 durable 臂在场,缺省容忍)。1=spawn 周期,≥2=复活周期(retained revive
|
|
1011
|
+
// 或 tier-3 均 bump——codex 快审 #11 纠,勿窄读成仅 tier-3)。
|
|
682
1012
|
res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, runId, target, status: probeDetails.status ?? "running", ...(typeof probeDetails.seq === "number" ? { seq: probeDetails.seq } : {}), live: "replica-local", replayFace: "GET /v1/runs/:id/subagents/:handle/output" })}\n\n`);
|
|
1013
|
+
// core 1.382([1561] 提货单③):"parked" 非活非终态(durably suspended pending approval)——跟
|
|
1014
|
+
// running/pending 同归"还没完",tail 该继续等而不是收尾指去 replay 面(那是给真终态用的)。子代获批
|
|
1015
|
+
// 恢复后会在**同一条**活流上继续产帧,提前收尾会让客户端误判"已经结束",错过恢复后的后续内容。
|
|
683
1016
|
if (probeDetails.status !== "running" && probeDetails.status !== "pending" && probeDetails.status !== "parked") {
|
|
684
1017
|
void it.return?.();
|
|
685
|
-
res.end();
|
|
1018
|
+
res.end(); // 终态:tail 无帧可等——replay 面才是读处(meta 已指路)
|
|
686
1019
|
return;
|
|
687
1020
|
}
|
|
688
1021
|
let closed = false;
|
|
689
1022
|
const hb = setInterval(() => {
|
|
690
1023
|
if (!res.writableEnded)
|
|
691
|
-
res.write(`event: heartbeat\ndata: {}\n\n`);
|
|
1024
|
+
res.write(`event: heartbeat\ndata: {}\n\n`); // workflow stream 同款真帧(BFF 丢注释帧案)
|
|
692
1025
|
}, 15_000);
|
|
693
1026
|
if (typeof hb.unref === "function")
|
|
694
1027
|
hb.unref();
|
|
695
1028
|
req.on("close", () => {
|
|
696
1029
|
closed = true;
|
|
697
|
-
void it.return?.();
|
|
1030
|
+
void it.return?.(); // parked next() 不观察 closed 旗——显式 return 解除 park + 清订阅(subscribeWorkflow 同案)
|
|
698
1031
|
});
|
|
699
1032
|
try {
|
|
700
1033
|
for (;;) {
|
|
@@ -703,7 +1036,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
703
1036
|
break;
|
|
704
1037
|
res.write(`event: forward\ndata: ${JSON.stringify(n.value)}\n\n`);
|
|
705
1038
|
if (n.value.type === "task_settled")
|
|
706
|
-
break;
|
|
1039
|
+
break; // 复审 #3:终态帧即完整故事结束——收流,余下在 replay 面
|
|
707
1040
|
}
|
|
708
1041
|
}
|
|
709
1042
|
finally {
|
|
@@ -714,6 +1047,16 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
714
1047
|
}
|
|
715
1048
|
return;
|
|
716
1049
|
}
|
|
1050
|
+
// [1499] CC TaskOutput/TaskStop 人侧对位 — the GENERIC task-handle verbs. The narrow subagents/:h/output
|
|
1051
|
+
// face above stays byte-stable (公告过的 background_agent ONLY contract); THIS family serves the full
|
|
1052
|
+
// registry kind set the TaskOutput/TaskStop tools serve (background_bash stdout — NB whether a read
|
|
1053
|
+
// consumes the output cursor depends on the handle's shape: a spooled handle re-reads in full, a
|
|
1054
|
+
// cursor-only handle returns new bytes per read; the projection's own flags are authoritative; monitor
|
|
1055
|
+
// batches; background_agent final report), with `workflow` refused at the seam (the journal face owns
|
|
1056
|
+
// workflow reads; a poll here would fire onServedTerminal and suppress the completion push). Addressing
|
|
1057
|
+
// is EXACT task_id only — the tools' agent-name / legacy-shellId resolution is deliberately not on the
|
|
1058
|
+
// wire (fail-closed 404). Gates mirror subagentOutput: verified principal → owner via the run row →
|
|
1059
|
+
// SESSION (enforced from birth — new face, zero consumers, no soft phase needed).
|
|
717
1060
|
const taskVerbMatch = req.method === "GET" ? RUN_TASK_OUTPUT_RE.exec(url) : req.method === "POST" ? RUN_TASK_STOP_RE.exec(url) : null;
|
|
718
1061
|
if (taskVerbMatch) {
|
|
719
1062
|
const stopVerb = req.method === "POST";
|
|
@@ -722,7 +1065,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
722
1065
|
return;
|
|
723
1066
|
}
|
|
724
1067
|
if (stopVerb && rateLimited(req, res))
|
|
725
|
-
return;
|
|
1068
|
+
return; // mutating, runs no model (parity with detach — rateLimited only, no quota/lease gate)
|
|
726
1069
|
const principal = gatedPrincipal(req, deps.config);
|
|
727
1070
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
728
1071
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
@@ -740,12 +1083,25 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
740
1083
|
sendError(res, 404, "not_found.run", "run not found");
|
|
741
1084
|
return;
|
|
742
1085
|
}
|
|
1086
|
+
// SESSION-scoped content/action face ([1493] model): enforced fail-closed — same boundary rationale as
|
|
1087
|
+
// subagentOutput (principal = whose data, session = which conversation; honest caller-asserted session is
|
|
1088
|
+
// correct AND sufficient inside one principal; operators bypass; a session-unbound run is principal-only).
|
|
743
1089
|
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
744
1090
|
if (!trusted && run.sessionId && q.get("session") !== run.sessionId) {
|
|
745
1091
|
sendError(res, 404, "not_found.run", "run not found");
|
|
746
1092
|
return;
|
|
747
1093
|
}
|
|
748
1094
|
const access = { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) };
|
|
1095
|
+
// ?filter= is REFUSED on BOTH verbs, not ignored (fail-loud 400; the face shipped unreleased, zero
|
|
1096
|
+
// consumers — and codex R2: a GET-only refusal would leave the stop verb silently accepting it). The
|
|
1097
|
+
// tools' filter is a caller-supplied RegExp run line-by-line on the replica's shared event loop — over
|
|
1098
|
+
// the wire that lets one request stall the whole replica for every tenant (pathological patterns on
|
|
1099
|
+
// long output lines take unbounded time in a backtracking engine). Client-side filtering of the fetched
|
|
1100
|
+
// output is the alternative, with an honest limit (codex R2): the wire serves the registry's CLIPPED
|
|
1101
|
+
// projection (core filters BEFORE clipping; a client can only filter after) — matches inside a clipped
|
|
1102
|
+
// middle are not recoverable over the wire, and a cursor-only bash leg consumes what it serves.
|
|
1103
|
+
// Full-fidelity filtered reads stay an in-engine tool capability; if a linear-time engine lands in
|
|
1104
|
+
// core, the wire filter can reopen deliberately.
|
|
749
1105
|
if (q.get("filter") !== null) {
|
|
750
1106
|
sendError(res, 400, "request.param_unsupported", "filter is not accepted on the wire — fetch the output and filter client-side (note: the wire serves the clipped projection)");
|
|
751
1107
|
return;
|
|
@@ -758,10 +1114,28 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
758
1114
|
sendError(res, 404, "not_found.task_handle", `no background task "${target}" under this run (unknown handle, not this run's task, already reaped — handles live in the replica-local registry for the parent's lifetime — or a workflow handle: workflow rows read via GET /v1/workflows/:id/journal; stopping a workflow is not on this wire)`);
|
|
759
1115
|
return;
|
|
760
1116
|
}
|
|
1117
|
+
// [1499] codex R3: a stop whose kill did NOT land must not read as success. Core keeps the handle
|
|
1118
|
+
// honest (status stays "running", error = the env failure code) — surface that as a 409 so a
|
|
1119
|
+
// status-driven client never mistakes a failed stop for a dead process. The projection rides along
|
|
1120
|
+
// verbatim for diagnosis. NB the "user" attribution pre-mark stays pending on this arm (first-write-
|
|
1121
|
+
// wins, no public per-handle clear) — if the task later dies of another cause it reads stoppedBy:
|
|
1122
|
+
// "user"; the atomic fix is a `source` option on core's stopTask (asked on the board), same residue
|
|
1123
|
+
// shape as core's own internal "parent" pre-mark.
|
|
1124
|
+
// core 1.382([1561] 提货单③):parked 行的两个新 stop 结局——`parked_pending_approval`(stop 撞见
|
|
1125
|
+
// 一个仍在 parked 的行,没有活跃可 kill 的进程)与 `park_resume_won`(stop 与"审批已落地→恢复"发生
|
|
1126
|
+
// 竞态,恢复那侧赢了,行不再是 parked,但 status 不一定翻回 "running"——error 码本身才是权威判据,
|
|
1127
|
+
// 不能靠 status 反推)。二者都不该读成 200 成功("kill 没落地"同一条纪律的延伸),各给各的
|
|
1128
|
+
// errorCode(同 409 家族,additive,与既有 not_local/not_landed 同姿势)。
|
|
1129
|
+
// [1596]/[1600] core 1.397 三分:仲裁器**抛错**(店不可达)此前被误吞成 park_resume_won 的假话——
|
|
1130
|
+
// 「输给了并发 resume」与「根本问不到仲裁结果」是两种不同的行动建议(前者重查状态,后者原样重试
|
|
1131
|
+
// stop),新码 `park_arbiter_unreachable` 让二者不再共用一个 errorCode。
|
|
761
1132
|
const parkArbiterUnreachable = details?.error === "park_arbiter_unreachable";
|
|
762
1133
|
const parkResumeWon = details?.error === "park_resume_won";
|
|
763
1134
|
const stillParked = details?.error === "parked_pending_approval" || details?.status === "parked";
|
|
764
1135
|
if (stopVerb && details?.error !== undefined && (details.status === "running" || stillParked || parkResumeWon || parkArbiterUnreachable)) {
|
|
1136
|
+
// 1.250:durable 回落臂(core stopTask agentStore 分支)对他实例 running 行应答 error="not_local"
|
|
1137
|
+
// (没有 kill 被尝试)——与「kill 尝试了没落地」(stop.not_landed)是不同的失败形,各给各的
|
|
1138
|
+
// errorCode(同 409 家族,additive)。
|
|
765
1139
|
const notLocal = details.error === "not_local";
|
|
766
1140
|
sendError(res, 409, notLocal
|
|
767
1141
|
? "stop.not_local"
|
|
@@ -782,6 +1156,14 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
782
1156
|
: `stop did not land (${details.error}) — the process may still be running`, { taskId: runId, target, content: out.content, output: out.details });
|
|
783
1157
|
return;
|
|
784
1158
|
}
|
|
1159
|
+
// Registry projection passed through verbatim (status/retrieval_status/cursor flags); `content` is
|
|
1160
|
+
// UNTRUSTED tool/model output, same posture as subagentOutput.
|
|
1161
|
+
// G14([1833]/[1840]§五):顶层铸 `cursorSemantics` —— 此前「这次读是否消费了游标」只能从 content
|
|
1162
|
+
// 文本标记(`--- stdout (full, re-readable) ---` vs `--- new stdout ---`)反解,每个消费端都得抄
|
|
1163
|
+
// 字符串判别。判据(core task-registry dist 亲读):游标消费形(非 spool 的 pollBackground 路径)
|
|
1164
|
+
// 的内层 details.bytesDroppedBeforeCursor **恒在**(0 也发);spool bash / monitor / background_agent
|
|
1165
|
+
// 全是重读安全的 full 形。⚠️ 这仍是对 core 内形的一处嗅探——集中在 server 这一处替所有端挑担;
|
|
1166
|
+
// 已在黑板请 core 出一等 flag,到货换源、wire 键不变。error/not_ready 形不铸(没有输出体,别给假语义)。
|
|
785
1167
|
const g14 = (() => {
|
|
786
1168
|
const d = out.details;
|
|
787
1169
|
if (!d || d.error !== undefined || d.retrieval_status === "not_ready")
|
|
@@ -790,7 +1172,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
790
1172
|
return { cursorSemantics: d.details && "bytesDroppedBeforeCursor" in d.details ? "cursor" : "full" };
|
|
791
1173
|
if (d.type === "monitor" || d.type === "background_agent")
|
|
792
1174
|
return { cursorSemantics: "full" };
|
|
793
|
-
return {};
|
|
1175
|
+
return {}; // 未知 kind:不猜
|
|
794
1176
|
})();
|
|
795
1177
|
sendJson(res, 200, { taskId: runId, target, content: out.content, output: out.details, ...g14 });
|
|
796
1178
|
return;
|
|
@@ -799,11 +1181,13 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
799
1181
|
if (subVerbMatch) {
|
|
800
1182
|
const verb = url.endsWith("/resume") ? "resume" : "steer";
|
|
801
1183
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
802
|
-
return;
|
|
1184
|
+
return; // mutating + drives a model agent
|
|
803
1185
|
if (!deps.runStore) {
|
|
804
1186
|
sendError(res, 501, "capability.run_store_required", "async runs require the TiDB run store");
|
|
805
1187
|
return;
|
|
806
1188
|
}
|
|
1189
|
+
// Identity from gatedPrincipal (direct-door-secure single point), NEVER the spoofable header — `trusted`
|
|
1190
|
+
// carries operator authority (parity with RUN steer / workflow-agent steer; HANDOFF 铁律 + core §3 inv #1).
|
|
807
1191
|
const principal = gatedPrincipal(req, deps.config);
|
|
808
1192
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
809
1193
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
@@ -827,19 +1211,28 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
827
1211
|
sendError(res, 400, "request.body_shape", "body must be { content: string (non-empty) }");
|
|
828
1212
|
return;
|
|
829
1213
|
}
|
|
1214
|
+
// 入参上限(见 `STEER_IN_MAX_REQUEST_CHARS` 旁注):脱敏门是**同步**的,烧的是事件循环 ⇒
|
|
1215
|
+
// 一次超大 steer 卡住的是**整个副本**。**拒**而不是服务端先截 —— 先截会让引擎的
|
|
1216
|
+
// `[+N chars]` 披露低报,而"诚实标记不得低报"是本仓另一条钉守着的性质。
|
|
830
1217
|
if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
|
|
831
1218
|
sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
|
|
832
1219
|
return;
|
|
833
1220
|
}
|
|
1221
|
+
// trusted = explicit operator (NOT bare isOperator); a normal caller may drive only their OWN run's children.
|
|
834
1222
|
const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
835
1223
|
const run = await deps.runStore.getRun(runId);
|
|
1224
|
+
// Owner-gate via the parent run row (the child inherits the parent's tenancy — core threads the parent's
|
|
1225
|
+
// principal to the child, design/62). 404 (not 403) for a non-owner — no existence oracle.
|
|
836
1226
|
if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
|
|
837
1227
|
sendError(res, 404, "not_found.run", "run not found");
|
|
838
1228
|
return;
|
|
839
1229
|
}
|
|
840
1230
|
if (!runSessionAcceptOk(req, res, run, `subagent.${verb}`))
|
|
841
|
-
return;
|
|
1231
|
+
return; // [1499] accept-phase (child write verbs, same class)
|
|
842
1232
|
const send409 = (errorCode, error) => sendError(res, 409, errorCode, error);
|
|
1233
|
+
// STEER-IN REDACTION GATE — same trust gate as the workflow-agent route (one owner: redactSteerIn): the
|
|
1234
|
+
// human content is UNTRUSTED (host-leak/size/fence) BEFORE it reaches the child; core's handle.steer/
|
|
1235
|
+
// resume fence AGAIN inside their trusted marker framing (defense in depth).
|
|
843
1236
|
const redacted = redactSteerIn(body.content, target);
|
|
844
1237
|
const resolution = deps.subagentSteerRegistry?.resolve(runId, target);
|
|
845
1238
|
if (resolution && resolution.count > 1) {
|
|
@@ -850,6 +1243,8 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
850
1243
|
if (handle) {
|
|
851
1244
|
try {
|
|
852
1245
|
if (verb === "resume") {
|
|
1246
|
+
// design/122: `handle.resume` is absent when the parent run did not retain child sessions — the
|
|
1247
|
+
// same condition core's own rejection names; report it with core's code so the shell renders one story.
|
|
853
1248
|
if (!handle.resume) {
|
|
854
1249
|
send409("resume.retain_off", "the parent run did not retain sub-agent sessions (set retainSubagentSessions on the run to enable revival)");
|
|
855
1250
|
return;
|
|
@@ -859,11 +1254,14 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
859
1254
|
return;
|
|
860
1255
|
}
|
|
861
1256
|
const marker = await handle.steer(redacted);
|
|
1257
|
+
// Receipt wording aligned with CC ("Message queued for delivery…").
|
|
862
1258
|
sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target} at its next tool round.` });
|
|
863
1259
|
return;
|
|
864
1260
|
}
|
|
865
1261
|
catch (e) {
|
|
866
1262
|
const code = e.code;
|
|
1263
|
+
// core's typed rejections → honest 409s with the core code verbatim (steer: not_running only;
|
|
1264
|
+
// resume: still_running / retain_off / evicted / cap / session_not_found — design/122 D2 contract).
|
|
867
1265
|
if (code === "steering.not_running") {
|
|
868
1266
|
send409(code, "sub-agent just finished — no longer accepting steers");
|
|
869
1267
|
return;
|
|
@@ -876,22 +1274,32 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
876
1274
|
send409(code, e instanceof Error ? e.message : "resume rejected");
|
|
877
1275
|
return;
|
|
878
1276
|
}
|
|
879
|
-
throw e;
|
|
1277
|
+
throw e; // unexpected → outer handler 500
|
|
880
1278
|
}
|
|
881
1279
|
}
|
|
1280
|
+
// No handle here. Either the parent run is elsewhere/terminal, the child was never registered (bg
|
|
1281
|
+
// delegation — no handle by core's r1-M3 scope), or the owning leg already tore down. Honest 409.
|
|
882
1282
|
send409("steering.not_running", run.status === "running"
|
|
883
1283
|
? "no sub-agent matches on this replica (it runs backgrounded, or the run is on another replica)"
|
|
884
1284
|
: `run is ${run.status} — its sub-agents are no longer addressable`);
|
|
885
1285
|
return;
|
|
886
1286
|
}
|
|
1287
|
+
// E23 (shell-host contract): POST /v1/elicitations/:id/respond — the shell answers an inbound MCP elicitation. LIVE-ONLY +
|
|
1288
|
+
// same-replica (the parked promise is in-memory, like a live steer): the coordinator owner-gates (404 — no
|
|
1289
|
+
// existence oracle), validates the body (= core's McpElicitResponse), and resolves the promise so onElicit returns
|
|
1290
|
+
// to core, which replies to the MCP server on the still-open connection. A respond on the wrong replica / after
|
|
1291
|
+
// answer/TTL finds nothing → 404.
|
|
887
1292
|
const elicitMatch = req.method === "POST" ? ELICIT_RESPOND_RE.exec(url) : null;
|
|
888
1293
|
if (elicitMatch) {
|
|
889
1294
|
if (rateLimited(req, res))
|
|
890
|
-
return;
|
|
1295
|
+
return; // mutating, but only resolves an in-flight promise (no new billable work → no quota gate)
|
|
891
1296
|
if (!deps.elicitation) {
|
|
892
1297
|
sendError(res, 501, "feature.elicitation_disabled", "inbound MCP elicitation is not enabled on this worker (MCP_ELICITATION_ENABLED)");
|
|
893
1298
|
return;
|
|
894
1299
|
}
|
|
1300
|
+
// Identity from gatedPrincipal (direct-door-secure single point), NEVER the spoofable header — the answer is
|
|
1301
|
+
// owner-gated against the run's owner (the END USER the elicitation was routed to). No operator bypass: an
|
|
1302
|
+
// elicitation form is the user's to fill, not a supervisor's (unlike steer).
|
|
895
1303
|
const principal = gatedPrincipal(req, deps.config);
|
|
896
1304
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
897
1305
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
@@ -910,14 +1318,21 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
910
1318
|
sendJson(res, status, respBody);
|
|
911
1319
|
return;
|
|
912
1320
|
}
|
|
1321
|
+
// §4④: POST /v1/questions/:id/respond — the shell answers the agent's AskUserQuestion. LIVE-ONLY +
|
|
1322
|
+
// same-replica (parked promise in-memory), the sibling of the elicit respond: owner-gated (404, no existence
|
|
1323
|
+
// oracle), body validated (= core's QuestionAnswer), resolves the promise so onQuestion returns to core and the
|
|
1324
|
+
// model continues with "The user answered: …". The security ANSWER-fence (selected ⊆ options, note untrusted-fenced)
|
|
1325
|
+
// is core's AskUserQuestion tool, not here. A respond on the wrong replica / after answer/TTL finds nothing → 404.
|
|
913
1326
|
const questionMatch = req.method === "POST" ? QUESTION_RESPOND_RE.exec(url) : null;
|
|
914
1327
|
if (questionMatch) {
|
|
915
1328
|
if (rateLimited(req, res))
|
|
916
|
-
return;
|
|
1329
|
+
return; // resolves an in-flight promise (no new billable work → no quota gate)
|
|
917
1330
|
if (!deps.question) {
|
|
918
1331
|
sendError(res, 501, "feature.ask_question_disabled", "AskUserQuestion live HITL is not enabled on this worker (ASK_QUESTION_ENABLED)");
|
|
919
1332
|
return;
|
|
920
1333
|
}
|
|
1334
|
+
// Same owner-gate identity as elicit: the VERIFIED principal (gatedPrincipal), never the spoofable header. The
|
|
1335
|
+
// question is the END USER's to answer (routed to their stream) — no operator bypass.
|
|
921
1336
|
const principal = gatedPrincipal(req, deps.config);
|
|
922
1337
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
923
1338
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
@@ -936,14 +1351,22 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
936
1351
|
sendJson(res, status, respBody);
|
|
937
1352
|
return;
|
|
938
1353
|
}
|
|
1354
|
+
// [816]/[820]②: POST /v1/tool-approvals/:id/respond — the shell answers a policy `ask` (the CC three-choice
|
|
1355
|
+
// card: {decision: "allow" | "allow_session" | "deny"}). LIVE-ONLY + same-replica (parked promise in-memory),
|
|
1356
|
+
// the sibling of the question respond: owner-gated (404, no existence oracle), body validated first (400 is
|
|
1357
|
+
// existence-independent), resolves the promise so core's resolveAsk returns allow/deny and the tool call
|
|
1358
|
+
// proceeds/denies. `allow_session` additionally arms the coordinator's per-session allow-all (fs-write family
|
|
1359
|
+
// only — rationale in tool-approval.ts). A respond on the wrong replica / after settle/TTL finds nothing → 404.
|
|
939
1360
|
const approvalMatch = req.method === "POST" ? TOOL_APPROVAL_RESPOND_RE.exec(url) : null;
|
|
940
1361
|
if (approvalMatch) {
|
|
941
1362
|
if (rateLimited(req, res))
|
|
942
|
-
return;
|
|
1363
|
+
return; // resolves an in-flight promise (no new billable work → no quota gate)
|
|
943
1364
|
if (!deps.toolApproval) {
|
|
944
1365
|
sendError(res, 501, "feature.tool_approval_disabled", "live tool-approval HITL is not enabled on this worker (TOOL_APPROVAL_ENABLED)");
|
|
945
1366
|
return;
|
|
946
1367
|
}
|
|
1368
|
+
// Same owner-gate identity as question/elicit: the VERIFIED principal (gatedPrincipal), never the spoofable
|
|
1369
|
+
// header. The approval is the END USER's to answer (routed to their stream) — no operator bypass.
|
|
947
1370
|
const principal = gatedPrincipal(req, deps.config);
|
|
948
1371
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
949
1372
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|