@sema-agent/server 7.43.0 → 7.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
package/dist/runs.js
CHANGED
|
@@ -3,52 +3,14 @@ import { withPrincipal } from "./observability/principal-context.js";
|
|
|
3
3
|
import { redactSecrets } from "./trace/redact.js";
|
|
4
4
|
import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "./trace/project.js";
|
|
5
5
|
import { createLedgerSink } from "./trace/ledger-sink.js";
|
|
6
|
-
import { registerEngineNoticeLeg } from "./trace/engine-notice-wire.js";
|
|
6
|
+
import { registerEngineNoticeLeg } from "./trace/engine-notice-wire.js";
|
|
7
7
|
import { recordTurnActivity } from "./turn-activity.js";
|
|
8
8
|
import { createApprovalCardEmitter, resolveApprovalLeg } from "./tool-approval.js";
|
|
9
9
|
import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
|
|
10
10
|
import { defaultSubagentTailBus, projectTailFrame } from "./fleet/subagent-tail-bus.js";
|
|
11
11
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
|
|
12
|
-
import { recordFailOpen } from "./observability/fail-open.js";
|
|
13
|
-
import { LIVENESS_HEARTBEAT_MS } from "./config-invariants.js";
|
|
14
|
-
/**
|
|
15
|
-
* stoppedBy (core 1.252): a service cancel is a USER stop — mark the run's still-running background
|
|
16
|
-
* children BEFORE core's teardown reaps them (bare abort = attribution falls back to "system"; core's own
|
|
17
|
-
* reap marks "parent", but first-marker-wins means our earlier "user" is the one that lands). Register this
|
|
18
|
-
* FIRST on the cancel signal so it runs ahead of any later-registered core listener (listener-order
|
|
19
|
-
* guarantee); one registration covers every abort route on that signal (same-replica fast path + the
|
|
20
|
-
* cross-replica flag poll). Advisory only — the cancel itself must never fail on attribution.
|
|
21
|
-
*
|
|
22
|
-
* Access = TASK-scoped children ONLY: owner = the parent taskId (on a RESUME leg: the sessionId — core's
|
|
23
|
-
* canonical taskId there), scope = `principal ?? "default"` (core registers children under
|
|
24
|
-
* `spec.principal ?? "default"`, so an anonymous run's children live in scope "default" and an access
|
|
25
|
-
* WITHOUT scope matched nothing). Session-scoped children are DELIBERATELY not marked: a cancelled parent
|
|
26
|
-
* does NOT take them down (CC Backgrounded semantics — core's teardown reaps with skipSessionScoped), so
|
|
27
|
-
* pre-marking them "user" would misattribute a stop that isn't happening.
|
|
28
|
-
*
|
|
29
|
-
* core 1.256: `markStopSourceForOwner` replaces the old `list()+markStopSource`
|
|
30
|
-
* per-row loop, closing BOTH documented holes — ① the EXPLICIT `sessionScoped` flag
|
|
31
|
-
* (`skipSessionScoped:true`) makes the resume leg safe (its owner key == sessionId COINCIDES with
|
|
32
|
-
* session-scoped children's; the old code couldn't tell them apart and took an honest degrade instead),
|
|
33
|
-
* ② a direct handle-map walk (no `list()` 500-row display cap under-marking >500-children runs). Covers
|
|
34
|
-
* core's full markable set (background_agent / background_bash / monitor); running-only +
|
|
35
|
-
* first-marker-wins guards live inside the seam.
|
|
36
|
-
*/
|
|
37
|
-
/** [1488]③(b) + codex R2: the bg agent-handle OUTPUT read, kind-gated BEFORE any poll. `pollTask` is the
|
|
38
|
-
* generic registry face and polling is NOT side-effect-free for other kinds — a terminal workflow poll
|
|
39
|
-
* fires `onServedTerminal` (acknowledges the completion as served → suppresses its notification) and a
|
|
40
|
-
* bash poll advances the output cursor. `getAccessibleTask` is a pure lookup: resolve + access-check +
|
|
41
|
-
* type-check FIRST; only a `background_agent` row is ever polled (non-blocking). Everything else returns
|
|
42
|
-
* the registry's own not_found shape, indistinguishable from an unknown handle (the route 404s it).
|
|
43
|
-
*
|
|
44
|
-
* 1.250 durable arm (core 1.364, [1511] follow-up delivered): a FULL in-process miss (`!row`) falls
|
|
45
|
-
* through to `pollTask(…, { agentStore })` — core's durable arm only fires for `a*`-shaped handles
|
|
46
|
-
* (DURABLE_AGENT_HANDLE_RE) behind `canAccessAgentRecord`, and with NO in-process handle the live
|
|
47
|
-
* workflow/bash branches are unreachable, so the kind-gate's reason (onServedTerminal / cursor advance)
|
|
48
|
-
* cannot trigger. Terminal rows serve the durable snapshot cross-instance/post-restart; a foreign-writer
|
|
49
|
-
* running row answers the honest "outcome unknown here"; anything else is core's own not_found (byte-same
|
|
50
|
-
* route 404 as today). A row that IS in-process but of the wrong type stays a hard 404 — the process
|
|
51
|
-
* knows the truth, falling back would sidestep the kind gate. */
|
|
12
|
+
import { recordFailOpen } from "./observability/fail-open.js";
|
|
13
|
+
import { LIVENESS_HEARTBEAT_MS } from "./config-invariants.js";
|
|
52
14
|
export async function backgroundAgentOutput(registry, handle, access, agentStore) {
|
|
53
15
|
const row = registry.getAccessibleTask(handle, access);
|
|
54
16
|
if (!row || row.type !== "background_agent") {
|
|
@@ -58,56 +20,20 @@ export async function backgroundAgentOutput(registry, handle, access, agentStore
|
|
|
58
20
|
}
|
|
59
21
|
return registry.pollTask(handle, access);
|
|
60
22
|
}
|
|
61
|
-
/** [1499] CC TaskOutput 人侧对位 — the GENERIC task-handle read face (b* bash stdout — NB whether a read
|
|
62
|
-
* consumes the output cursor depends on the handle's shape: spooled handles re-read in full [GET-idempotent],
|
|
63
|
-
* cursor-only handles return new bytes per read; the projection's flags are authoritative; m* monitor
|
|
64
|
-
* batches; a* agent final report). No filter option: caller-supplied regexes are refused at the route (a
|
|
65
|
-
* backtracking engine on the shared event loop must not run wire-supplied patterns; client-side filtering
|
|
66
|
-
* sees only the clipped projection — full-fidelity filtered reads stay an in-engine tool capability).
|
|
67
|
-
* Addressing is exact task_id only (the tools' name/legacy-id resolution stays off the wire). Kind-gate
|
|
68
|
-
* FIRST on the pure getAccessibleTask (codex-R2 shape — pollTask is not side-effect-free): `workflow` is
|
|
69
|
-
* refused — its HTTP read face is the workflow journal ([1491] contract), and a pollTask here would fire
|
|
70
|
-
* onServedTerminal (ack the completion as served) and suppress the workflow completion push. Refusals
|
|
71
|
-
* return the registry's own not_found shape, indistinguishable from an unknown handle (the route 404s it). */
|
|
72
23
|
const TASK_HANDLE_KINDS = new Set(["background_bash", "monitor", "background_agent"]);
|
|
73
|
-
/** 1.250 复审 F1(HIGH):durable 回落的**形判门**——`getAccessibleTask` 是精确键(不 trim、不查
|
|
74
|
-
* 别名),而 core `pollTask/stopTask` 是通用解析面(先 trim;[2380] RB-479-A②:`legacyToTaskId`
|
|
75
|
-
* shellId 索引已随 5.0.0 整族删,b* task_id 是唯一 registry 地址——本门防的「legacy shellId 穿缝」
|
|
76
|
-
* 一臂如今在 core 层就响亮 not_found;stopTask 仍有 byName 臂)。裸 `!row` 回落会让垫充空白句柄
|
|
77
|
-
* (" "+wfId)/agent 名字从回落缝穿进 live 分支——kind 门要防的 onServedTerminal/游标消费/按名 stop
|
|
78
|
-
* 全部复活(复审实测复现)。
|
|
79
|
-
* 只有 **a\* 精确形**才回落:`DURABLE_AGENT_HANDLE_RE` = core 公共导出(core 1.369,[1518] ask 即答
|
|
80
|
-
* ——两边判词同源,镜像私拷已删)。残余(documented):恰为 a\* 形的 agent **名字**
|
|
81
|
-
* 在无 durable 行时仍会进 core stopTask 的 byName 臂——同 access 元组内、名字域与句柄域同形的病态窄面,
|
|
82
|
-
* 接受。 */
|
|
83
24
|
const DURABLE_AGENT_HANDLE_SHAPE = DURABLE_AGENT_HANDLE_RE;
|
|
84
25
|
export async function taskHandleOutput(registry, handle, access, agentStore) {
|
|
85
26
|
const row = registry.getAccessibleTask(handle, access);
|
|
86
27
|
if (!row || !TASK_HANDLE_KINDS.has(row.type)) {
|
|
87
|
-
// 1.250 durable arm — full in-process miss + a* 精确形才回落(F1 形判门,DURABLE_AGENT_HANDLE_SHAPE
|
|
88
|
-
// 顶注;b*/m* 无 durable 面照旧 not_found;进程内命中但型错 = 真相已知,不回落)。
|
|
89
28
|
if (!row && agentStore && DURABLE_AGENT_HANDLE_SHAPE.test(handle))
|
|
90
29
|
return registry.pollTask(handle, access, { agentStore });
|
|
91
30
|
return { content: "", details: { task_id: handle, retrieval_status: "not_ready", error: "not_found" } };
|
|
92
31
|
}
|
|
93
32
|
return registry.pollTask(handle, access);
|
|
94
33
|
}
|
|
95
|
-
/** [1499] CC TaskStop 人侧对位 — stop a background task handle (bash kill / monitor stop / agent abort).
|
|
96
|
-
* Same kind gate as the read face (a workflow stops via its own cancel face, never this verb). stopTask is
|
|
97
|
-
* idempotent on a terminal handle (the registry answers honestly); the stop is attributed "user" via the
|
|
98
|
-
* registry's existing markStopSource machinery (an HTTP stop is the human's hand — parity with the shell's
|
|
99
|
-
* TaskStop attribution). The pre-mark is first-write-wins and MUST come first (core's own stop path marks
|
|
100
|
-
* "parent" internally — marking after would lose the attribution race on success, the common path); the
|
|
101
|
-
* trade (codex R3) is that on a kill-that-didn't-land the pending "user" mark is not clearable through the
|
|
102
|
-
* public registry face — the route surfaces that arm as 409 stop.not_landed, and the atomic fix (a `source`
|
|
103
|
-
* option on stopTask) is a core ask. */
|
|
104
34
|
export async function taskHandleStop(registry, handle, access, agentStore) {
|
|
105
35
|
const row = registry.getAccessibleTask(handle, access);
|
|
106
36
|
if (!row || !TASK_HANDLE_KINDS.has(row.type)) {
|
|
107
|
-
// 1.250 durable arm — core stopTask's agentStore branch answers honestly (terminal row: "already
|
|
108
|
-
// <status> — nothing to stop"; foreign-writer running row: not_local, no kill is attempted). NO
|
|
109
|
-
// markStopSource here: there is no in-process kill to attribute, and the durable answer never reads
|
|
110
|
-
// as a landed stop. F1 形判门同 output 面(stopTask 还有 byName 臂——非 a* 形一律不进通用解析面)。
|
|
111
37
|
if (!row && agentStore && DURABLE_AGENT_HANDLE_SHAPE.test(handle))
|
|
112
38
|
return registry.stopTask(handle, access, { agentStore });
|
|
113
39
|
return { content: "", details: { task_id: handle, retrieval_status: "not_ready", error: "not_found" } };
|
|
@@ -115,21 +41,12 @@ export async function taskHandleStop(registry, handle, access, agentStore) {
|
|
|
115
41
|
registry.markStopSource(handle, "user");
|
|
116
42
|
return registry.stopTask(handle, access);
|
|
117
43
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Attribute this signal's abort as a USER stop for the owner's task-scoped children.
|
|
120
|
-
*
|
|
121
|
-
* 🔴 已经 abort 的信号在这里**立刻**记账,不是静默无操作(#168 件3 的配套)。调用点在 #168 件3 之后
|
|
122
|
-
* 一律挪到了「run 认领成功之后」—— 认领是一次 await,断连完全可能落在它之前。`addEventListener` 对一个
|
|
123
|
-
* 已 fire 的信号永不回调,那样一次真实的人为断连会被吞成「无归因」(子代 settle 时记成 system)。
|
|
124
|
-
* 语义上这两种时序是同一件事:本请求赢下了这条 session,而这条 session 的连接是被人断掉的。
|
|
125
|
-
*/
|
|
126
44
|
export function markChildrenStoppedByUserOnAbort(signal, taskId, principal) {
|
|
127
45
|
const mark = () => {
|
|
128
46
|
try {
|
|
129
47
|
defaultTaskRegistry.markStopSourceForOwner({ owner: taskId, scope: principal ?? "default" }, "user", { skipSessionScoped: true });
|
|
130
48
|
}
|
|
131
49
|
catch {
|
|
132
|
-
/* attribution is advisory — the cancel itself must never fail on it */
|
|
133
50
|
}
|
|
134
51
|
};
|
|
135
52
|
if (signal.aborted) {
|
|
@@ -138,11 +55,6 @@ export function markChildrenStoppedByUserOnAbort(signal, taskId, principal) {
|
|
|
138
55
|
}
|
|
139
56
|
signal.addEventListener("abort", mark, { once: true });
|
|
140
57
|
}
|
|
141
|
-
/**
|
|
142
|
-
* A session CAS conflict is swallowed by core into a failed `TaskResult` (it is not thrown from
|
|
143
|
-
* `runTaskStream`), so detect it from the result. Prefer the structured `errorCode` (core ≥1.8); fall
|
|
144
|
-
* back to the conflict wording only for older results without one.
|
|
145
|
-
*/
|
|
146
58
|
export function isSessionConflictResult(result) {
|
|
147
59
|
if (result.status !== "failed")
|
|
148
60
|
return false;
|
|
@@ -150,30 +62,6 @@ export function isSessionConflictResult(result) {
|
|
|
150
62
|
return result.errorCode === "conflict";
|
|
151
63
|
return /stale write|concurrent write|conflict/i.test(result.errorMessage ?? "");
|
|
152
64
|
}
|
|
153
|
-
/**
|
|
154
|
-
* E18 resume-at CALLER error → an HTTP 4xx. A bad/stale resume-at target (the resolved entryId is gone or is not a
|
|
155
|
-
* settled message boundary) surfaces as a FAILED TaskResult carrying a `resume_at.*` errorCode (core swallows the
|
|
156
|
-
* prepare-throw into the result, exactly like a session conflict — runtask catch → status:"failed"+errorCode). The
|
|
157
|
-
* sync path otherwise 200s a failed result; map these to 4xx so a caller mistake reads as one. `not_found` (the
|
|
158
|
-
* entry is genuinely absent) → 404; everything else (not_a_message / conflicts_resume / no_session) → 422.
|
|
159
|
-
*
|
|
160
|
-
* design/114 Phase3: `requireExistingSession` on a genuinely-missing/purged session ALSO surfaces as a failed result
|
|
161
|
-
* (core prepare-task maps the store's `not_found` → `resume.session_not_found`); map it to 404 (the session is gone),
|
|
162
|
-
* the same "caller mistake reads as a 4xx, not a silent 200 fresh run" discipline.
|
|
163
|
-
*
|
|
164
|
-
* [833] rewind exclusive mode (core 1.292 resumeAtMode:"before"): its two edge rejections ride the same `resume_at.*`
|
|
165
|
-
* prefix (before_target_not_user / before_root_unsupported → 422 via the existing branch, errorCode passed through
|
|
166
|
-
* UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
|
|
167
|
-
* point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
|
|
168
|
-
* billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
|
|
169
|
-
*
|
|
170
|
-
* design/170 件A(#148 件4): core 5.13.0's org-memory admission rides the SAME prepare-throw pipe
|
|
171
|
-
* (GOVERNANCE_CODES). `memory.admission_denied` is TERMINAL — the principal has no grant on that tenant
|
|
172
|
-
* plane → 403 (a retry cannot change the verdict). `memory.admission_required` is TRANSIENT fail-closed —
|
|
173
|
-
* the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
|
|
174
|
-
* result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
|
|
175
|
-
* codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
|
|
176
|
-
*/
|
|
177
65
|
export function resumeAtHttpStatus(result) {
|
|
178
66
|
if (result.status !== "failed")
|
|
179
67
|
return undefined;
|
|
@@ -189,71 +77,31 @@ export function resumeAtHttpStatus(result) {
|
|
|
189
77
|
return undefined;
|
|
190
78
|
return result.errorCode === "resume_at.not_found" ? 404 : 422;
|
|
191
79
|
}
|
|
192
|
-
/**
|
|
193
|
-
* E18 resume-at — per-turn anchor capture, shared by runInBackground AND the resume-leg driveResume so the (single,
|
|
194
|
-
* tricky) capture rule can't drift between the two stream-drain loops.
|
|
195
|
-
*
|
|
196
|
-
* A "turn" in core = ONE assistant message + its tool batch. resume-at is valid ONLY at a SETTLED rewindable boundary
|
|
197
|
-
* — core's prepare-task accepts "user messages or finished assistant-text turns" and REJECTS an assistant message that
|
|
198
|
-
* ends mid-tool-call (resume_at.not_a_message). So we capture an anchor ONLY for a turn that produced text AND ran NO
|
|
199
|
-
* tools: then the turn's leaf at turn_end IS that assistant-text message (the rewindable target). For a tool-using
|
|
200
|
-
* turn neither candidate works — at turn_end the leaf is the turn's LAST tool-result (core appends tool results before
|
|
201
|
-
* emitting turn_end; a tool-result is stored as a `message` so core would SILENTLY branch AFTER the tools ran), and
|
|
202
|
-
* the assistant message itself ends mid-tool-call so core would reject it. The shell still gets a per-message eventId
|
|
203
|
-
* handle on every text event (E2 identity); a rewind to a non-captured (tool-using) message simply 404s — honest,
|
|
204
|
-
* never a silent wrong-point branch. (The adversarial-review HIGH: capturing at turn_end unconditionally mis-anchored
|
|
205
|
-
* every tool-using turn — the common agentic shape — to the tool-result leaf.)
|
|
206
|
-
*
|
|
207
|
-
* Best-effort: a capture failure must NEVER fail the run (it only makes that one turn non-rewindable). `onCaptureFail`
|
|
208
|
-
* surfaces the degrade (a metric) so a SYSTEMATIC failure — e.g. an un-migrated DB missing `resume_anchor` — is
|
|
209
|
-
* observable instead of a silent feature outage.
|
|
210
|
-
*/
|
|
211
80
|
export class TurnAnchorCapture {
|
|
212
81
|
capture;
|
|
213
82
|
onCaptureFail;
|
|
214
83
|
captureUser;
|
|
215
|
-
/** The turn's FIRST text_delta eventId = the message's stable resume handle (also stamped on the durable text event). */
|
|
216
84
|
firstTextEventId;
|
|
217
85
|
turnHadTool = false;
|
|
218
|
-
constructor(capture, onCaptureFail,
|
|
219
|
-
/** R8 (CC-parity rewind): capture a USER-message anchor straight from the
|
|
220
|
-
* `message_committed{role:"user"}` event — its `entryId` is the persisted user-message `SessionTreeEntry.id`,
|
|
221
|
-
* ALWAYS a valid `resumeAt` target (core accepts a user message; it rejects a tool-result / mid-tool-call
|
|
222
|
-
* assistant). This is the CC rewind target ("rewind to the prompt" = the code-restore parity path — a code
|
|
223
|
-
* change happens in a tool turn WITHIN a prompt's run, so rewinding to the prompt + rewindFiles undoes it).
|
|
224
|
-
* Keyed by the run's taskId (the handle the shell already holds from the POST response — no eventId→entryId
|
|
225
|
-
* side map / no `getLeafId` inference needed). The assistant-turn capture above stays as an additive bonus. */
|
|
226
|
-
captureUser) {
|
|
86
|
+
constructor(capture, onCaptureFail, captureUser) {
|
|
227
87
|
this.capture = capture;
|
|
228
88
|
this.onCaptureFail = onCaptureFail;
|
|
229
89
|
this.captureUser = captureUser;
|
|
230
90
|
}
|
|
231
|
-
/** A `message_committed` arrived — capture a USER-message rewind anchor (CC parity). Best-effort (a failure only
|
|
232
|
-
* makes that one prompt non-rewindable). Only `role==="user"`; assistant/toolResult committed entries are handled
|
|
233
|
-
* by the turn-text path (assistant) or deliberately not anchored (toolResult — `resumeAt` rejects them). */
|
|
234
91
|
async onMessageCommitted(role, entryId) {
|
|
235
92
|
if (role === "user" && entryId && this.captureUser) {
|
|
236
93
|
await this.captureUser(entryId).catch(() => this.onCaptureFail?.());
|
|
237
94
|
}
|
|
238
95
|
}
|
|
239
|
-
/** A text_delta arrived — latch the first DEFINED eventId as the message handle (eventId is optional on the stream). */
|
|
240
96
|
onText(eventId) {
|
|
241
97
|
if (this.firstTextEventId === undefined) {
|
|
242
|
-
// First text of THIS turn. Any tool seen before now belonged to a PRIOR turn: on the resume leg, core streams
|
|
243
|
-
// the suspended turn's resolution tool_start/tool_end (the approved tool) with NO intervening turn_end before the
|
|
244
|
-
// continuation turn (runtask.js applyResumeDecision). Clearing turnHadTool here keeps a pure-text continuation
|
|
245
|
-
// turn anchorable. SAFE for normal turns: within a turn the assistant streams its text BEFORE its own tool calls,
|
|
246
|
-
// so a real same-turn tool always arrives AFTER this first text and re-sets the flag (guarded by `=== undefined`,
|
|
247
|
-
// so a later text part after a tool — [text, toolCall, text] — does NOT wrongly clear it).
|
|
248
98
|
this.turnHadTool = false;
|
|
249
99
|
}
|
|
250
100
|
this.firstTextEventId ??= eventId;
|
|
251
101
|
}
|
|
252
|
-
/** A tool ran this turn — mark it non-rewindable (its leaf will be a tool-result, not a settled assistant-text message). */
|
|
253
102
|
onTool() {
|
|
254
103
|
this.turnHadTool = true;
|
|
255
104
|
}
|
|
256
|
-
/** turn_end — capture the anchor iff this was a settled assistant-TEXT turn, then reset for the next turn. */
|
|
257
105
|
async onTurnEnd() {
|
|
258
106
|
if (this.firstTextEventId && !this.turnHadTool && this.capture) {
|
|
259
107
|
await this.capture(this.firstTextEventId).catch(() => this.onCaptureFail?.());
|
|
@@ -262,36 +110,13 @@ export class TurnAnchorCapture {
|
|
|
262
110
|
this.turnHadTool = false;
|
|
263
111
|
}
|
|
264
112
|
}
|
|
265
|
-
/**
|
|
266
|
-
* Strip the durable-resume capability token from a result before it is persisted to the replayable event
|
|
267
|
-
* log / run row or returned to the submitter. The token IS the resume credential (token-as-auth, search
|
|
268
|
-
* [18] Q1b) and must never leave the service. Safe on any result (no-op when absent) — notably core maps a
|
|
269
|
-
* NESTED suspend under verify/cascade to status:"failed" + errorCode "unexpected.suspended"(inner status
|
|
270
|
-
* "needs_review" ⇒ "unexpected.needs_review";core `agents/suspend-guard.js` `mapNestedSuspend`,由 verify.js
|
|
271
|
-
* 191/201/208 与 cascade.js:125 调用)while still
|
|
272
|
-
* spreading `checkpointToken`, so that "failed" path (unlike the stream path's explicit suspend handling)
|
|
273
|
-
* would otherwise leak it into task_event / task_run.result / the sync 200 response. `checkpointGate` is
|
|
274
|
-
* non-secret and intentionally kept.
|
|
275
|
-
*/
|
|
276
113
|
export function stripCheckpointToken(result) {
|
|
277
|
-
// Narrow to a plain object (reject null / arrays) and an OWN `checkpointToken` key, so a non-plain or
|
|
278
|
-
// array input is returned unchanged rather than reshaped.
|
|
279
114
|
if (result !== null && typeof result === "object" && !Array.isArray(result) && Object.hasOwn(result, "checkpointToken")) {
|
|
280
115
|
const { checkpointToken: _drop, ...rest } = result;
|
|
281
116
|
return rest;
|
|
282
117
|
}
|
|
283
118
|
return result;
|
|
284
119
|
}
|
|
285
|
-
/**
|
|
286
|
-
* [4913](cli 判别子 v3 请托)—— park 结果的**待批工具调用身份**:suspended/needs_review 结果在
|
|
287
|
-
* strip 之前手握 checkpointToken,用它 `cs.get(token)` 换 `pendingAction`,`tool_approval` 臂直读
|
|
288
|
-
* `toolCallId`(与 tool_approval 帧 ≥1.307 同键同义)。核心 `TaskResult` 上没有这个键(CheckpointGate
|
|
289
|
-
* 只带 toolName),checkpoint 本体是 park 的事实源——同源判据(A-058 教训)。
|
|
290
|
-
*
|
|
291
|
-
* 缺席形照 core `CheckpointSummary` 的 [1995]② OMITTED 契约:tool-less park(resource_limit /
|
|
292
|
-
* plan_review / task_done)回 `undefined` ⇒ 调用方**不铸键**,绝不编 null。读失败=F 类兜底
|
|
293
|
-
* (展示/关联键,缺席=该键到货前的现状),`recordFailOpen` 留痕,绝不挡 done/suspended 终局写链。
|
|
294
|
-
*/
|
|
295
120
|
export async function parkToolCallId(result, cs) {
|
|
296
121
|
if (!cs || result === null || typeof result !== "object")
|
|
297
122
|
return undefined;
|
|
@@ -307,112 +132,24 @@ export async function parkToolCallId(result, cs) {
|
|
|
307
132
|
return undefined;
|
|
308
133
|
}
|
|
309
134
|
}
|
|
310
|
-
/** [4913] 整对象形:park 结果顶层 additive 上 `toolCallId` 键(与 `checkpointId` 同位,#285 先例;
|
|
311
|
-
* 不塞进 `checkpointGate`——那是 core 的类型,server 不改其形)。非 park / 无 token / tool-less /
|
|
312
|
-
* 读失败 ⇒ **同一引用**原样返回(键缺席)。graft 不负责 strip——组合序恒为 graft → strip。 */
|
|
313
135
|
export async function graftParkToolCallId(result, cs) {
|
|
314
136
|
if (result !== null && typeof result === "object" && Object.hasOwn(result, "toolCallId"))
|
|
315
|
-
return result;
|
|
137
|
+
return result;
|
|
316
138
|
const id = await parkToolCallId(result, cs);
|
|
317
139
|
return id === undefined ? result : { ...result, toolCallId: id };
|
|
318
140
|
}
|
|
319
|
-
/** Evict a stale warm-cache entry after a cross-instance write conflict (broken-affinity backstop). */
|
|
320
141
|
export function evictIfConflict(runner, sessionId, result) {
|
|
321
142
|
if (sessionId && isSessionConflictResult(result)) {
|
|
322
143
|
runner.sessions.evict?.(sessionId);
|
|
323
144
|
}
|
|
324
145
|
}
|
|
325
|
-
|
|
326
|
-
* Drive a task to completion in the background, persisting its events to the durable run log (S2).
|
|
327
|
-
*
|
|
328
|
-
* Text deltas are coalesced into one `text` event per turn (the durable stream is turn-grained for
|
|
329
|
-
* text); tool/lifecycle events are recorded individually. Per-token live streaming stays on the
|
|
330
|
-
* synchronous `/v1/tasks/stream` endpoint. Never throws — failures are recorded on the run.
|
|
331
|
-
*
|
|
332
|
-
* Cancellation (POST /v1/runs/:id/cancel): the run registers an AbortController in `inflight` (same-replica
|
|
333
|
-
* fast path — the cancel handler aborts it directly) and its heartbeat tick polls the durable
|
|
334
|
-
* `cancel_requested` flag (cross-replica, ≤HEARTBEAT_MS lag). Either way the run settles to
|
|
335
|
-
* status "failed" + errorCode "cancelled" (metric label "cancelled"); the entry is removed in `finally`.
|
|
336
|
-
*
|
|
337
|
-
* Preemption (POST /v1/assistant/tasks/:id/preempt, design/80 seam #2): a SECOND, INDEPENDENT controller
|
|
338
|
-
* (`preemptCtrl`, registered in `preemptable`) carries the scheduler's graceful "yield this task" — when raised,
|
|
339
|
-
* core durably SUSPENDS the task at the next clean turn boundary (gate resource_limit, reason "preempt") so it is
|
|
340
|
-
* RESUMABLE, the opposite of cancel's kill. It is wired ONLY on the plain-stream path: the verify/cascade
|
|
341
|
-
* branches finalize with `setTerminal` unconditionally (no `setSuspended`), so a preempt-suspend there would write
|
|
342
|
-
* a non-terminal "suspended" as terminal and release the session lock — and preempt's contract is graceful, not a
|
|
343
|
-
* failure. Cross-replica via the `preempt_requested` flag (same heartbeat tick). Eligibility (resourceSuspend +
|
|
344
|
-
* durable store + durable tool-results + remote env) is core's call: an ineligible task's preemptSignal no-ops.
|
|
345
|
-
*
|
|
346
|
-
* Steering (POST /v1/runs/:id/steer, design/47): the live core `TaskStream` is registered in `steerable` so the
|
|
347
|
-
* HTTP route can inject a mid-task message via `stream.steer()` (drained at the next turn boundary). Same-replica
|
|
348
|
-
* only — a suspended run is steered durably via the checkpoint's `setPendingSteer` instead, and only the
|
|
349
|
-
* plain-stream path holds a steerable stream (the verify/cascade branches return a result, not a stream).
|
|
350
|
-
*/
|
|
351
|
-
export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, // 快审 F1:number 参升整对象——cost 顶随传
|
|
352
|
-
cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation,
|
|
353
|
-
/** E23: the VERIFIED principal (gatedPrincipal) that may answer this run's elicitations — matches the respond
|
|
354
|
-
* owner-gate's identity source (NOT the cost-attribution `principal`, which on a direct door is the spoofable
|
|
355
|
-
* header → can be null while a JWT principal exists). Falls back to the run owner when not supplied. */
|
|
356
|
-
elicitOwner,
|
|
357
|
-
/** E18 resume-at: capture the turn's (firstTextEventId → leaf entryId) anchor at each turn_end. Built by the caller
|
|
358
|
-
* closing over {sessionId, owner, sessionStore.getLeafId, resumeAnchorStore}; undefined ⇒ no anchor store wired. */
|
|
359
|
-
captureTurnAnchor,
|
|
360
|
-
/** MF-Fleet (shell-host contract): the run-scoped fleet-row publisher. `onStart` adds the live row; `onEvent` accrues
|
|
361
|
-
* tokens (turn_end) + subagent child rows (task_progress); `onTerminal` flips/removes it. Fired for ALL legs
|
|
362
|
-
* (verify/cascade/plain-stream) so a background run TRANSITIONS its `GET /v1/fleet/stream` row exactly like the
|
|
363
|
-
* sync leg. No-op when no fleet bus is wired (the publisher is a null-object). */
|
|
364
|
-
fleetPublisher,
|
|
365
|
-
/** R8 (CC-parity rewind): capture a USER-message anchor (keyed by this run's taskId) from `message_committed{role:"user"}`
|
|
366
|
-
* — the CC "rewind to the prompt" target. undefined ⇒ no anchor store wired. */
|
|
367
|
-
captureUserMessageAnchor,
|
|
368
|
-
/** §4④ : AskUserQuestion LIVE coordinator — establishes a per-run onQuestion ALS context (like `elicitation`)
|
|
369
|
-
* so an AskUserQuestion tool call routes to this run's stream + owner-gated respond. undefined ⇒ headless default. */
|
|
370
|
-
question,
|
|
371
|
-
/** P1 ①② follow-on (core): the async-workflow completion inbox, drained at the START of this background
|
|
372
|
-
* leg (parity with the sync stream-open drain) into durable `workflow_complete` events a tailing client replays.
|
|
373
|
-
* Owner gate = `elicitOwner ?? owner` (the same verified-principal-with-run-owner-fallback the elicit/question
|
|
374
|
-
* contexts use). undefined ⇒ no push half wired (the WorkflowStatus poll floor stands alone). */
|
|
375
|
-
workflowCompletionInbox,
|
|
376
|
-
/** C2 (core 1.219): the replica-local Task-subagent steer-handle registry. When present, this leg
|
|
377
|
-
* opts into core's `onSubagentSpawn` sink — each SYNC delegation's steer handle registers under THIS run's
|
|
378
|
-
* taskId (auto-evicted on child settle) so POST /v1/runs/:id/subagents/:target/steer can route into it. */
|
|
379
|
-
subagentSteer,
|
|
380
|
-
/** ask①: emit-target diagnostic sink for the completion-inbox drain below (route/connection/
|
|
381
|
-
* deliveryLagMs per delivered `workflow_complete`). runs.ts has no logger of its own — the caller closes over
|
|
382
|
-
* deps.logger. undefined ⇒ silent (behavior unchanged). */
|
|
383
|
-
completionDiagLog,
|
|
384
|
-
/** SendUserFile(切片2):per-run file_link 帧发射 ALS(question/elicitation 同款)。emit=durable append。 */
|
|
385
|
-
sendUserFile,
|
|
386
|
-
/** [998]② the shared prompt-manifest accumulator (tracer records at prepare; this leg drains the pending
|
|
387
|
-
* record into a durable `prompt_assembled` event — turns/stream read it). undefined ⇒ not wired. */
|
|
388
|
-
promptManifests,
|
|
389
|
-
/**
|
|
390
|
-
* #151 车3 刀 3b:**bg 腿的流内审批 ctx**(design/172 §4.3(b))。今天为止这条腿连 approval ALS 都没有
|
|
391
|
-
* ——`deps.onAsk` 拿不到 ctx ⇒ 恒 `"unavailable"` ⇒ durable park,一张卡都不产。接上之后,bg run 的
|
|
392
|
-
* 权限 ask 走本腿的 **durable events tail**(`GET /v1/runs/:id/events`,唯一带 SSE `id:` 的面)。
|
|
393
|
-
*
|
|
394
|
-
* `streamApprovalOn` = 协议上场判据(`resolveStreamApprovalGate`,由路由层求值后传进来 —— 本函数是纯
|
|
395
|
-
* 执行腿,不该自己读 backend/config)。为假 ⇒ 只接既有四键 ctx(呈卡口/腿轴都不接),本腿字节逐字不变。
|
|
396
|
-
*/
|
|
397
|
-
approval,
|
|
398
|
-
/** [4913]:park 投影腿的 checkpoint 读口(`deps.checkpointStore`)。在场时,durable park 的
|
|
399
|
-
* `suspended` 事件带 `toolCallId`(graft 见 {@link parkToolCallId});缺席=旧字节逐字不变。 */
|
|
400
|
-
parkCheckpointReader) {
|
|
146
|
+
export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader) {
|
|
401
147
|
const startedAt = Date.now();
|
|
402
148
|
metrics?.addGauge("runs_active", 1);
|
|
403
|
-
fleetPublisher?.onStart();
|
|
404
|
-
// Cancel (POST /v1/runs/:id/cancel): the controller aborts the run two ways — (a) same-replica fast path,
|
|
405
|
-
// the cancel handler looks this up in `inflight` and aborts now; (b) cross-replica, the heartbeat tick below
|
|
406
|
-
// polls the durable `cancel_requested` flag and aborts. Either way an aborted run finalizes to
|
|
407
|
-
// "failed" + errorCode "cancelled" (we do NOT extend TaskStatus — errorCode distinguishes user-cancel).
|
|
149
|
+
fleetPublisher?.onStart();
|
|
408
150
|
const cancelCtrl = new AbortController();
|
|
409
151
|
inflight?.set(taskId, cancelCtrl);
|
|
410
|
-
// stoppedBy (core 1.252): a service cancel is a USER stop — see markChildrenStoppedByUserOnAbort.
|
|
411
152
|
markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal);
|
|
412
|
-
// design/80 seam #2 (preemption): only the plain-stream path can durably suspend-and-resume on a preempt (the
|
|
413
|
-
// verify/cascade branches setTerminal unconditionally). Register the preempt controller — and poll the durable
|
|
414
|
-
// flag — ONLY for that path, so the endpoint that looks a task up in `preemptable` can honestly tell a
|
|
415
|
-
// verify/cascade task apart (not preemptible) from a stream task.
|
|
416
153
|
const preemptEligible = verify === undefined && cascade === undefined;
|
|
417
154
|
const preemptCtrl = new AbortController();
|
|
418
155
|
if (preemptEligible)
|
|
@@ -426,25 +163,11 @@ parkCheckpointReader) {
|
|
|
426
163
|
metrics?.inc("task_tokens_total", {}, result.stats.tokens);
|
|
427
164
|
if (result?.stats.cacheHitRate !== undefined)
|
|
428
165
|
metrics?.observe("task_cache_hit_rate", result.stats.cacheHitRate);
|
|
429
|
-
// Degenerate-output a/b instrument (1.59) — same as the sync path; no-op unless output.degenerate.
|
|
430
166
|
if (result)
|
|
431
167
|
instrumentDegenerate?.(result);
|
|
432
|
-
// Plan-cache recurrence probe (design/42): objective recurrence per scope. status "completed" feeds
|
|
433
|
-
// the history ring (mirrors the sync path); other terminal states still count toward the denominator.
|
|
434
168
|
planCacheProbe?.record(principal, spec.objective, status === "completed");
|
|
435
169
|
};
|
|
436
|
-
// Liveness heartbeat (independent of events): keeps a long-but-alive run — e.g. one blocked in a
|
|
437
|
-
// multi-minute subagent that streams no parent events — from being mistaken for a dead instance.
|
|
438
|
-
// The owning principal is this run's owner — thread it as the single-DB-fleet null-safe owner guard on every
|
|
439
|
-
// run-store mutation/poll below (the run row was created with owner = principal ?? null).
|
|
440
170
|
const owner = principal ?? null;
|
|
441
|
-
// #151 车3 刀 3b(§7.3 + §8.3):本腿的审批装配裁定走**三腿共用**的那一份判据。
|
|
442
|
-
// 🔴 codex 交叉复审 F3(2026-08-06 真 finding)的修:此前本腿**无条件**包 ALS,只把新协议的口与轴挂在
|
|
443
|
-
// `streamApprovalOn` 后面 —— 于是 ①开关关时本腿的行为变了(此前它根本没有 approval ctx,每只 ask 恒
|
|
444
|
-
// `"unavailable"` ⇒ 恒 park;包上之后变成活卡并等满窗,A-1 当场破),②窗=0 在本腿上不生效(照样落行、
|
|
445
|
-
// 注册资源、发帧,再等一个 0ms 的表)。现在两件都由下面这一个裁定统一管。
|
|
446
|
-
// 取值点 = **本腿开始执行的时刻** = `runInBackground` 进入(路由层 `void runInBackground(...)` 之后
|
|
447
|
-
// 立刻),不是请求到达时刻(§3.3:按任务创建至今算会系统性算错)。
|
|
448
171
|
const approvalLeg = resolveApprovalLeg({
|
|
449
172
|
streamApprovalOn: approval?.streamApprovalOn === true,
|
|
450
173
|
windowMs: approval?.windowMs ?? 0,
|
|
@@ -454,208 +177,73 @@ parkCheckpointReader) {
|
|
|
454
177
|
});
|
|
455
178
|
const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
|
|
456
179
|
const heartbeat = setInterval(() => {
|
|
457
|
-
// 鲁棒性批5 A5(2026-08-05):与下面 cancel/preempt 两条兄弟同族——此前裸吞错,store 持续故障期间本 run
|
|
458
|
-
// 的心跳每拍静默落空、运维零信号,直到某个副本的 reapStale 把它误判死亡(心跳失败正是那个误判的前兆)。
|
|
459
180
|
void runStore.heartbeat(taskId, owner).catch(() => { metrics?.inc("run_signal_poll_errors_total", { kind: "heartbeat" }); });
|
|
460
|
-
// Cross-replica cancel: the cancel may have landed on another instance, which only set the durable flag.
|
|
461
|
-
// Poll it here so the OWNING instance aborts its in-flight run (bounded by HEARTBEAT_MS).
|
|
462
|
-
// 鲁棒性批3 A5(2026-08-04,§M 感知链路):poll 失败本身有界(下一拍重试),但此前**零披露**——
|
|
463
|
-
// store 持续故障期间取消/抢占每拍静默落空、run 继续烧预算,运维零信号。修=计数指标(warn 会逐拍
|
|
464
|
-
// 刷屏,counter 是轮询披露的对形);abort 判定逻辑零变化。
|
|
465
181
|
if (!cancelCtrl.signal.aborted) {
|
|
466
182
|
void runStore.isCancelRequested(taskId, owner).then((c) => { if (c)
|
|
467
183
|
cancelCtrl.abort(); }).catch(() => { metrics?.inc("run_signal_poll_errors_total", { kind: "cancel" }); });
|
|
468
184
|
}
|
|
469
|
-
// Cross-replica preempt (design/80 seam #2): a preempt may have landed on another instance (durable flag
|
|
470
|
-
// only). Poll it so the OWNING instance raises its preemptSignal → core suspends at the next clean turn
|
|
471
|
-
// boundary (resumable). Only the stream path wires the signal, so only poll there.
|
|
472
185
|
if (preemptEligible && !preemptCtrl.signal.aborted) {
|
|
473
186
|
void runStore.isPreemptRequested(taskId, owner).then((p) => { if (p)
|
|
474
187
|
preemptCtrl.abort(); }).catch(() => { metrics?.inc("run_signal_poll_errors_total", { kind: "preempt" }); });
|
|
475
188
|
}
|
|
476
189
|
}, HEARTBEAT_MS);
|
|
477
190
|
heartbeat.unref?.();
|
|
478
|
-
// E18 resume-at: per-turn anchor capture (the turn's first text_delta eventId = the stable per-message handle, which
|
|
479
|
-
// is also stamped on the durable `text` event so the shell sees ONE resume handle per message). The shared
|
|
480
|
-
// TurnAnchorCapture only anchors a SETTLED assistant-text turn (text + no tools) — see its doc; the metric surfaces
|
|
481
|
-
// a systematic capture failure (un-migrated DB) instead of a silent feature outage.
|
|
482
191
|
const anchor = new TurnAnchorCapture(captureTurnAnchor, () => metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
|
|
483
|
-
// Once a terminal outcome's event is DURABLY appended, the run has reached that state — record it so a later
|
|
484
|
-
// failure (e.g. setTerminal throws after the `done` event is logged) re-attempts the SAME terminal write
|
|
485
|
-
// rather than clobbering a completed run with `failed` (which would log both `done` and `failed` and report a
|
|
486
|
-
// succeeded task as failed).
|
|
487
192
|
let reached;
|
|
488
|
-
// 1.78 (review LOW): the plain-stream leg's live TaskStream — held so the finally can identity-guard the
|
|
489
|
-
// `steerable` deregistration (see the finally).
|
|
490
193
|
let steerableStreamRef;
|
|
491
|
-
// false once this leg settles — a background subagent/bash completion arriving AFTER that has no live
|
|
492
|
-
// harness for core's in-process injection (the idle gap), so the observer below parks it durably instead.
|
|
493
194
|
let legLive = true;
|
|
494
|
-
// option A: taskNotificationStreamKey (域:task_id:status[#seq], core [784]④a + codex F2 域二分) keys
|
|
495
|
-
// already carried by THIS leg (start-of-leg inbox drain, event-stream branch OR observer) — each arm skips
|
|
496
|
-
// keys another arm already delivered, so a notification isn't double-appended to the durable log (the
|
|
497
|
-
// turn-TAIL reap notification, which never reaches the stream, is exactly the one the observer catches;
|
|
498
|
-
// core's pend-drain replay of a double-parked idle arrival is exactly the one the drain-registration
|
|
499
|
-
// catches). The shell's dedup stays the outer belt. codex F1: value = the claiming write's outcome promise
|
|
500
|
-
// (the observer arm appends fire-and-forget; a failed claim must NOT eat the stream replay — see NotifiedKeys).
|
|
501
195
|
const notifiedKeys = new NotifiedKeys();
|
|
502
|
-
// design/122 ③: THIS leg's subagent-handle unregister thunks (identity-guarded by the registry) — run at
|
|
503
|
-
// leg end (finally), NOT on child settle, so a settled child stays resumable while the parent run lives.
|
|
504
196
|
const subagentHandleEvictions = [];
|
|
505
|
-
// design/80 D-B: PARK a `needs_review` result (a dry-run diff review OR a pre-action `plan_review` pause; core
|
|
506
|
-
// assemble-result slot 8.6) like a suspend — KEEP task_active + flip the row non-terminal so the resume
|
|
507
|
-
// (getActiveTaskId → markResuming) can claim it. Without this every finalize site setTerminal'd it (deleting the
|
|
508
|
-
// session lock), and the plan_review resume then got taskId=undefined → drove the model UNPROTECTED on an
|
|
509
|
-
// unlocked session with the row stuck `needs_review` (the adversarial-review HIGH finding). Mirrors the suspended
|
|
510
|
-
// branch (append only the non-secret gate, never the capability token).
|
|
511
197
|
const parkNeedsReview = async (res) => {
|
|
512
198
|
await sink.appendParked("needs_review", res);
|
|
513
|
-
await flushModelUsage();
|
|
199
|
+
await flushModelUsage();
|
|
514
200
|
reached = { kind: "needs_review" };
|
|
515
201
|
await runStore.setNeedsReview(taskId);
|
|
516
202
|
metrics?.addGauge("runs_active", -1);
|
|
517
203
|
metrics?.inc("tasks_total", { status: "needs_review" });
|
|
518
204
|
};
|
|
519
|
-
// [900]①/[906]③: the event→ledger projection (text/reasoning coalescing + redaction + seq + the type/data
|
|
520
|
-
// whitelist shapes) lives in the SHARED LedgerSink — the sync /v1/tasks/stream detach lane consumes the same
|
|
521
|
-
// single switch, so the two ledgers can't drift. This leg keeps its non-ledger side-legs (anchor/metrics/
|
|
522
|
-
// model-usage/park-terminal/fleet) in place and drives the sink; `append`/`flush` below stay the leg-local
|
|
523
|
-
// names for every out-of-band row (elicit/question frames, model_usage deltas, suggestions, failed) so they
|
|
524
|
-
// share the run's single seq counter. `notifiedKeys` is the same per-leg instance the observer/drain arms use.
|
|
525
205
|
const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys, onActivity: () => recordTurnActivity(taskId) });
|
|
526
206
|
const append = (type, data) => sink.append(type, data);
|
|
527
207
|
const flush = () => sink.flush();
|
|
528
|
-
// E8 (shell-host contract): drain the per-task model-usage accumulator into an append-only `model_usage` DELTA event, so
|
|
529
|
-
// the durable log is the resume-safe source of `TaskStats.modelUsage`. Called at every turn boundary AND before a
|
|
530
|
-
// park (suspend/needs_review) — a run that suspends has its pre-suspend usage persisted before the resume leg
|
|
531
|
-
// continues accumulating; the append-only deltas SUM across all legs (no overwrite).
|
|
532
208
|
const flushModelUsage = () => appendModelUsageDelta(append, modelUsage, taskId);
|
|
533
|
-
// At a TERMINAL done: flush the final delta + SUM all `model_usage` events into result.stats.modelUsage (shared
|
|
534
|
-
// with the resume leg's terminal so a suspended→resumed run's total is summed across both legs).
|
|
535
209
|
const withModelUsage = (result) => attachModelUsage(result, { append, getEvents: (id, after) => runStore.getEvents(id, after), modelUsage, taskId });
|
|
536
|
-
modelUsage?.register(taskId);
|
|
537
|
-
// [998]②: same fence — sub-task manifests are never registered, never leak. The returned LEASE guards the
|
|
538
|
-
// finally's clear (B-3 window: a fast sibling resume re-registers this taskId before our finally runs —
|
|
539
|
-
// an unconditional clear would delete the NEW leg's registration/pending manifest).
|
|
210
|
+
modelUsage?.register(taskId);
|
|
540
211
|
const manifestLease = promptManifests?.register(taskId) ?? 0;
|
|
541
|
-
// [998]②: no-op when nothing is pending (one Map.get) — fired per stream event so the manifest lands at the
|
|
542
|
-
// top of the log right after core's prepare records it, and before the terminal on the no-stream legs.
|
|
543
212
|
const flushPromptManifest = () => appendPromptManifest(append, promptManifests, taskId);
|
|
544
|
-
// E23 (shell-host contract): an inbound MCP elicitation fired deep inside core's callTool surfaces via onElicit, which has
|
|
545
|
-
// no taskId — so establish a per-run elicitation context (ALS) carrying this run's emit (durable append → delivered
|
|
546
|
-
// on the events tail), owner (respond owner-gate) + cancel signal (a run abort cancels a parked elicitation). The
|
|
547
|
-
// elicitation append interleaves safely: onElicit is awaited SYNCHRONOUSLY inside callTool, so the loop is parked
|
|
548
|
-
// (not appending) while it runs. No-op when elicitation is off. `type` is the SSE event name; the rest is the data.
|
|
549
213
|
const elicitEmit = (frame) => {
|
|
550
214
|
const { type, ...rest } = frame;
|
|
551
215
|
return append(type, rest);
|
|
552
216
|
};
|
|
553
|
-
// §4④: same durable-append emit for AskUserQuestion frames (delivered on the events tail; the shell renders `question`
|
|
554
|
-
// + dismisses on `question_complete`). onQuestion is awaited SYNCHRONOUSLY inside core's callTool, so the loop parks
|
|
555
|
-
// (not appending) while it runs — the open frame's append is ordered ahead of the answer/terminal appends.
|
|
556
217
|
const questionEmit = (frame) => {
|
|
557
218
|
const { type, ...rest } = frame;
|
|
558
219
|
return append(type, rest);
|
|
559
220
|
};
|
|
560
|
-
// #151 车3 刀 3b(design/172 §4.3(b)):同款 durable-append emit,服务**审批**帧 —— 既有
|
|
561
|
-
// `tool_approval`/`tool_approval_complete` 走它,新协议的 `approval_request` 也走它(同一条投递面、
|
|
562
|
-
// 两种帧,§4.3(a) 的「不新增出口」在这条腿上的落实)。`type` 是 SSE 事件名,其余是 data。
|
|
563
|
-
// 撤卡帧(`approval_revoke`)**不接**:它是 live-only 语义(不进账本 —— reaper/收敛器那类调用点没有
|
|
564
|
-
// seq 可分配),而本腿的唯一投递面就是账本;丢帧的结构补偿是重连 preamble 的全量对账基准。
|
|
565
221
|
const approvalEmit = (frame) => {
|
|
566
222
|
const { type, ...rest } = frame;
|
|
567
223
|
return append(type, rest);
|
|
568
224
|
};
|
|
569
|
-
// #310 `engine_notice` wire 腿(设计稿 = 黑板 [4630],三腿之一:**bg**)。本腿**没有 live SSE**
|
|
570
|
-
// (消费方走 `GET /v1/runs/:id/events` 的 durable tail),所以只挂 durable 口 —— 这与本腿的 HITL 帧
|
|
571
|
-
// (elicit/question/approval 三族)姿态逐字相同:唯一投递面就是账本,tail 负责送到人眼前。
|
|
572
|
-
// fire-and-forget:`onNotice` 是 core 的同步回调,不能拿一次账本写去阻塞引擎;seq 在 `append` 的链步内
|
|
573
|
-
// 同步分配 ⇒ 与主循环的 await 写不会乱序(`onForwardEvent` 同款契约)。
|
|
574
|
-
// 🔴 写失败**留痕不裸吞**(codex 对抗复审 R1-[medium],验真后修):本腿账本是通告的**唯一**用户可见
|
|
575
|
-
// 终点,一次 store 抖动就让它对用户永久消失;`route` 的同步 try 观察不到异步 reject。同文件 resume 腿
|
|
576
|
-
// 的 `warnAppend` 是同一条纪律的先例(C2/C5 批2)。
|
|
577
225
|
const unregisterEngineNotice = registerEngineNoticeLeg({
|
|
578
226
|
sessionId: spec.sessionId,
|
|
579
|
-
// (本函数没有 logger 席 —— 留痕走 `recordFailOpen` 的三件套:逐次计数 + 逐次探针行[带 detail] +
|
|
580
|
-
// 一次性 warn。这正是 #157 为「结构上拿不到 logger 的兜底臂」准备的口,不为它加一个参数。)
|
|
581
227
|
durable: (row) => void append("engine_notice", row).catch(() => recordFailOpen("server.engine-notice.durable-append-failed", `leg=bg code=${row.code} task=${taskId}`)),
|
|
582
228
|
});
|
|
583
229
|
try {
|
|
584
|
-
// P1 ①② follow-on (core): drain this session's async-workflow completion inbox at the START of the
|
|
585
|
-
// background leg (before ANY branch — verify/cascade/plain-stream all get it; parity with the sync leg's
|
|
586
|
-
// stream-open drain) into durable `workflow_complete` events the tailing client (GET /v1/runs/:id/events)
|
|
587
|
-
// replays as <task-notification>s. Best-effort inside the helper (it never throws); inside the try so the
|
|
588
|
-
// heartbeat/gauge finally-cleanup would cover even a defensive miss. Owner gate mirrors the elicit/question owner.
|
|
589
230
|
await emitPendingWorkflowCompletions(workflowCompletionInbox, spec.sessionId, elicitOwner ?? owner, (frame) => {
|
|
590
231
|
const { type, ...rest } = frame;
|
|
591
|
-
// core [784]④b: register the drained frame's key on THIS leg — the same idle-arrival was ALSO parked
|
|
592
|
-
// core-side (pendingSessionNotifications); its pend-drain replay rides this run's stream as a
|
|
593
|
-
// task_notification event, and the loop's key check folds that second durable append (sync twin's note).
|
|
594
|
-
// 三路复审修2(MED,推翻旧 codex F1 注释的 resolved(true) 登记):emitPendingWorkflowCompletions
|
|
595
|
-
// 虽 await 这次 append,但它 CATCH 失败(留 row 不 ack)——键却已被 Promise.resolve(true) 认领;
|
|
596
|
-
// core 若把同一 pending 经 pend-drain 重放到本腿 stream 臂,stream 臂见「真成功」claim 抑制重写,
|
|
597
|
-
// 通知就延到下一次开腿(可能无限延)。登记真实 append 结果:失败 resolve false,stream 臂
|
|
598
|
-
// await 到 false 照写(咬合 NotifiedKeys 的 F1 语义);row 未 ack 的下腿重投仍是兜底。
|
|
599
|
-
// codex F2: task_type rides into the key(drained frames restore it from the entry extras)——
|
|
600
|
-
// external 帧独立键域,不与内部任务帧互吞.
|
|
601
232
|
const f = frame;
|
|
602
233
|
const write = append(type, rest);
|
|
603
234
|
if (f.type === "task_notification" && f.task_id)
|
|
604
235
|
notifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), write.then(() => true, () => false));
|
|
605
236
|
return write;
|
|
606
|
-
},
|
|
607
|
-
// ask①: emit-target diagnostics — this drain appends to THIS background run's durable event log.
|
|
608
|
-
completionDiagLog ? { route: "bg-run-start", connection: "durable-append", log: completionDiagLog } : undefined);
|
|
609
|
-
// Thread the owning principal so the cost tracer attributes this run's spend (incl. council/team
|
|
610
|
-
// sub-tasks) to the principal's cumulative quota — mirrors the synchronous path.
|
|
237
|
+
}, completionDiagLog ? { route: "bg-run-start", connection: "durable-append", log: completionDiagLog } : undefined);
|
|
611
238
|
const runBody = async () => {
|
|
612
|
-
// core 1.154 nested-subagent live tree: opt into the forward sink — a subagent's task_progress lives in the
|
|
613
|
-
// child run's ISOLATED stream (core's only emitter is parentToolCallId-gated and pushes onto the CHILD queue;
|
|
614
|
-
// the top stream NEVER carries it), so this sink is the ONLY path a tick reaches the top-run consumers. 1.78
|
|
615
|
-
// (review HIGH): fan it out to the DURABLE event log too (via the shared whitelist+redact builder, fire-and-
|
|
616
|
-
// forget — `++seq` is a sync increment so sink-vs-loop appends get unique seqs), not just the fleet bus —
|
|
617
|
-
// previously no fleet bus wired ⇒ ticks dropped entirely, and the stream-switch task_progress case below was
|
|
618
|
-
// forward-compat then — [2373]B-4:floor ^4.0.0 下它是热路径(腐注方向已翻转,勿按旧注当死支)。core swallow-guards a throwing sink.
|
|
619
|
-
// [3911] 定谳:per-agent tail 的**内容帧供给**由本腿 spec 的 forwardSubagentEvents 决定(core
|
|
620
|
-
// prepare-task 的 forward 门);缺席 ⇒ 那条流恒只有 task_progress。逐帧按**句柄**登记本腿的供给形,
|
|
621
|
-
// 让 tail 的 meta 帧如实声明(键为什么必须是句柄:SubagentTailBus.modeByHandle 顶注)。
|
|
622
|
-
//
|
|
623
|
-
// 🔴 **#253(core 5.33.0 #246 提货)起,这只 internals 在 verify/cascade 早返分支之前铸**:三条腿
|
|
624
|
-
// (verify 形 / cascade 形 / 普通流形)用**同一只**对象。5.32 及以前 core 的 `runWithVerification` /
|
|
625
|
-
// `runCascade` 签名里根本没有这一位,那两条腿因此对子代进度、活性打点、bg 终态通知**全盲**;5.33.0
|
|
626
|
-
// 开了尾可选 `internals` 座位并逐字转发给它驱动的每一条腿(实现腿/验证器腿/修复腿/每一级 rung),
|
|
627
|
-
// 于是这里只需要把既有供给链**接上去**,不新铸第二条链(黑盒判据见
|
|
628
|
-
// `test/issue190-status-wire-disclosure.test.ts` §4)。
|
|
629
|
-
// ⚠️ **`isolation` 一位永不传**:core 对 `internals.isolation` 在 verify/cascade 门上双相位响亮拒
|
|
630
|
-
// (`config.verify_internals_invalid` —— 这道门一次跑好几条腿,而 isolation 是 per-run 铸工作区),
|
|
631
|
-
// 本仓从不铸这一位,座位对象里也不许出现它。
|
|
632
239
|
const tailContentMode = spec.forwardSubagentEvents === true ? "on" : "progress_only";
|
|
633
240
|
const fwdInternals = {
|
|
634
|
-
// #253:活性打点 —— 「只回结果」形没有事件循环,`recordTurnActivity` 此前只挂在 LedgerSink 的
|
|
635
|
-
// append 链上(那条链在 verify/cascade 腿上整段任务只响一次:终局 done)。core 的 onActivity 在
|
|
636
|
-
// 每次工具 start/end 同步回调,于是一条长跑的 verify 腿不再被 `GET /v1/runs/:id` 的
|
|
637
|
-
// `msSinceLastActivity` / 409 冲突体读成僵死。普通流形不经它(那条腿的打点在 sink 里,同一个函数,
|
|
638
|
-
// 重复打点无害但也无必要——本键对三腿一律在场,少一条判别分支)。
|
|
639
241
|
onActivity: () => recordTurnActivity(taskId),
|
|
640
242
|
onForwardEvent: (e) => {
|
|
641
243
|
fleetPublisher?.onForwardEvent(e);
|
|
642
|
-
// C1 (core 1.219): a delegated child's tool lifecycle + task_progress ticks persist through the SHARED
|
|
643
|
-
// sink (same whitelist+redact builders as the top stream — §E1, forwarded events are UNTRUSTED RAW;
|
|
644
|
-
// fire-and-forget with sync seq allocation). Rationale + the deliberate per-chunk-delta omission live
|
|
645
|
-
// on LedgerSink.onForwardEvent.
|
|
646
|
-
//
|
|
647
|
-
// 🔴 **别在这里加「腿结束就不落账」的围栏**(#253 施工期实测:加了当场打红 `runs-coverage` 的
|
|
648
|
-
// [1840]§三定性钉)。那条钉是**取证结论**,不是习惯:`run_in_background` 收养形子代的**整个**
|
|
649
|
-
// 生命周期都在父 run 终态之后,live events 流按设计 terminal break —— 帧照落账,消费端 done 后
|
|
650
|
-
// 重拉即全见。撕掉 sink = 对那一族子代**真丢帧**(cli [1839] 轮②「子代内层零转发」的病因分析里
|
|
651
|
-
// 写死了这一条)。codex 对抗复审 R2 [medium] 提的正是「终局后不该再落行」,**验真后驳回**:
|
|
652
|
-
// 本仓已有属主裁定 + 复现器 + 防回归钉,不能因为一次静态复审把它反过来。
|
|
653
244
|
sink.onForwardEvent(e);
|
|
654
|
-
// S2 live tail(core 1.370 bgAgentId,[1520]{core}):bg 子代帧按句柄分发给 per-agent 订阅者
|
|
655
|
-
// (sync 腿 twin 在 http/server.ts)。hasSubscribers 快门=没人听不投影;投影一次 N 订阅者共享。
|
|
656
245
|
const bg = e.bgAgentId;
|
|
657
246
|
if (bg !== undefined) {
|
|
658
|
-
// 供给形登记在 hasSubscribers 快门**之前**:没人在听时也要学,否则晚开的 tail 读不到。
|
|
659
247
|
defaultSubagentTailBus.noteHandleContentMode(bg, tailContentMode);
|
|
660
248
|
if (defaultSubagentTailBus.hasSubscribers(bg)) {
|
|
661
249
|
const f = projectTailFrame(e);
|
|
@@ -664,66 +252,26 @@ parkCheckpointReader) {
|
|
|
664
252
|
}
|
|
665
253
|
}
|
|
666
254
|
},
|
|
667
|
-
// (durable half is the deployment's job): observe EVERY background
|
|
668
|
-
// completion notification. (1) flip the child's fleet row (task_progress never emits a terminal tick —
|
|
669
|
-
// without this a settled bg agent showed "running" until parent settle); (2) when the LEG already ended
|
|
670
|
-
// (idle — core's steer/followUp had nowhere to go), park it in the durable inbox; the next stream-open
|
|
671
|
-
// drain emits it as the same `task_notification` frame family. Workflow-type notifications are SKIPPED —
|
|
672
|
-
// they have their own durable chain (notify gate + inbox + serve fences); double-enqueueing would re-open
|
|
673
|
-
// the double-push. While the leg is LIVE, core injects in-process + the stream carries the display frame —
|
|
674
|
-
// no durable copy (a boundary-race notification that misses both is covered by the TaskOutput poll floor).
|
|
675
|
-
// core 1.283 `seq` (per-stop-cycle counter) rides into the dedup key + parked entry — core [784]④a.
|
|
676
|
-
// 白名单排查(clay 令,2026-07-24,「彻查 as {} 连环坑」):此前手写窄类型(仅 10/19 键),3 处
|
|
677
|
-
// (此处+http/server.ts 两处)复制粘贴、随 core payload 增field 逐渐过时——n 本身按引用直转
|
|
678
|
-
// taskNotificationEventData({notification:n}),运行时不丢字段(该函数自己的白名单是完整的),
|
|
679
|
-
// 但窄类型会让未来在这些回调体内新增读字段的人被 TS 误导「这个字段到不了这里」。改用 core
|
|
680
|
-
// 真实导出类型,杜绝副本漂移。
|
|
681
255
|
onTaskNotification: (n) => {
|
|
682
256
|
if (n.task_type === "workflow")
|
|
683
257
|
return;
|
|
684
|
-
// S2 复审 #3(sync 腿同注):中途终态信号。
|
|
685
|
-
// [3911] 终态即撤供给形登记:同一 a* 句柄可进入下一个**复活周期**,而新周期的宿主腿模式
|
|
686
|
-
// 可能相反——不撤会把上一周期的答案报给新周期的首帧前窗口(理由全文见 forgetHandleContentMode)。
|
|
687
258
|
defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
|
|
688
259
|
if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
|
|
689
260
|
defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
|
|
690
261
|
}
|
|
691
|
-
// id-domain alias (core): fleet child rows key by the TICK's taskId = the child's sessionId
|
|
692
|
-
// (uuid domain); the notification's task_id is the a* domain — payload.sessionId IS the tick-domain
|
|
693
|
-
// alias, so flip by it (fallback task_id for pre-1.238 payloads).
|
|
694
|
-
// [2687-cli] 幽灵行案:onChildTerminal 只对 **agent 族终态**调用(isFleetAgentTerminalNotification
|
|
695
|
-
// 单源判别,理由与病灶链见其 doc 注)。通知帧/park 面照走,只掐 fleet 铸行。三消费点孪生同形
|
|
696
|
-
// (http/server.ts resume 腿、routes/tasks.ts sync 腿)。
|
|
697
262
|
const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPublisher?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
|
|
698
263
|
const parked = !legLive && Boolean(workflowCompletionInbox && spec.sessionId);
|
|
699
|
-
// diagnosability: one line per observed bg completion. hadFleetRow=false = the terminal frame was
|
|
700
|
-
// FLIP-THROUGH synthesized (the row was gone — parent settle removed it, or a bg BASH never ticked).
|
|
701
264
|
completionDiagLog?.("task_notification_observed", { route: "bg-run-start", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive, parkedDurable: parked });
|
|
702
265
|
if (parked) {
|
|
703
|
-
// 对抗评审 2026-07-11(MED):log the enqueue failure — the diag line above already printed
|
|
704
|
-
// parkedDurable:true, so a swallow made that a false park✓ fingerprint. Never faults the callback.
|
|
705
266
|
void workflowCompletionInbox.enqueue(taskNotificationInboxEntry(spec.sessionId, elicitOwner ?? owner, n, Date.now(), taskId)).catch((err) => completionDiagLog?.("park_enqueue_failed", { route: "bg-run-start", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
|
|
706
267
|
}
|
|
707
268
|
else if (legLive) {
|
|
708
|
-
// option A (the turn-TAIL killed receipt must be IMMEDIATE): a teardown-time reap
|
|
709
|
-
// notification fires while the leg is still live but past the stream's delivery window — core's
|
|
710
|
-
// in-process steer is swallowed and the event never reaches the log. Append the display frame HERE,
|
|
711
|
-
// skipping keys the stream already delivered (mid-turn dedup); the tailing client renders the receipt
|
|
712
|
-
// this turn, not one turn late. core [784]④a: seq-folded shared key — the seq-less
|
|
713
|
-
// `${task_id}:${status}` shape reverse-swallowed a second stop-cycle's same-status frame here.
|
|
714
|
-
// codex F1 (HIGH): this append is fire-and-forget — claim the key with its REAL outcome, not
|
|
715
|
-
// unconditionally. A failed append (SQL blip) resolves the claim false, and the stream arm's
|
|
716
|
-
// await-then-write below re-delivers the same key instead of being swallowed by a dead claim.
|
|
717
269
|
const key = taskNotificationStreamKey(n);
|
|
718
270
|
if (notifiedKeys.get(key) === undefined) {
|
|
719
271
|
notifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n })).then(() => true, () => false));
|
|
720
272
|
}
|
|
721
273
|
}
|
|
722
274
|
},
|
|
723
|
-
// C2 (core 1.219) + design/122 (core 1.225 ③): register each SYNC child's steer handle under THIS
|
|
724
|
-
// run's taskId. NOT evicted on settle — `resume` is only legal AFTER the child settled (a settle-evict
|
|
725
|
-
// would make revival structurally unreachable). Evicted at LEG END via the collected identity-guarded
|
|
726
|
-
// thunks (finally below); a resume re-emits a FRESH handle under the same id (registry newest-wins).
|
|
727
275
|
...(subagentSteer
|
|
728
276
|
? {
|
|
729
277
|
onSubagentSpawn: (handle) => {
|
|
@@ -732,48 +280,18 @@ parkCheckpointReader) {
|
|
|
732
280
|
}
|
|
733
281
|
: {}),
|
|
734
282
|
};
|
|
735
|
-
// 🔴 **#255(core 5.34.0 #253 / 判据帖 [3967] G1):status 第五席** —— 与上面四席**不同**,它只在
|
|
736
|
-
// 「只回结果」的两条腿上展开,**绝不**挂到 `runTaskStream` 那一位。理由是 core 的 `statusEmit` 是
|
|
737
|
-
// **双写**(`queue.push` 照旧 + 座位在场即喂**同一个** frozen 帧对象):普通流形的那条 queue 就是本
|
|
738
|
-
// 函数自己 `for await` 的流 —— 下面 loop 里 `sink.onEvent(ev)` 已落账、`metrics.inc` 已计数 ⇒ 两处
|
|
739
|
-
// 都写 = **同一帧两行账本 + 计数翻倍**。组合腿(verify/cascade)没有这条流(引擎把内部腿的 queue
|
|
740
|
-
// 自己排干丢弃),座位是它**唯一**的载体,这正是 5.34.0 开这一席的理由。
|
|
741
|
-
// (反向钉:`test/issue190-status-wire-disclosure.test.ts` §4⑤ —— 夹具照 core 的真姿势 dual-write,
|
|
742
|
-
// 普通流形上账本必须恰五行不是十行。)
|
|
743
|
-
//
|
|
744
|
-
// ⚠️ **帧是 frozen 的**:core 在双写前 `Object.freeze(frame)`,所以这条链上任何一环都不许写帧字段
|
|
745
|
-
// (改一下再往下传会当场 TypeError)。`brainStatusEventData` 是纯读投影,不改源帧。
|
|
746
|
-
// ⚠️ **同步回调不能 await**:失败留痕不断链(孪生姿势 = `onForwardEvent` 的 fire-and-forget 写)。
|
|
747
|
-
// ⚠️ 下面那个 `.catch` **买的不是进程安全**(别照抄别处的「逃逸 rejection 杀进程」说法):
|
|
748
|
-
// `sink.appendStatus` 返回的是**链步**promise,而 `LedgerSink.append` 内部已经给每一步挂了处理器
|
|
749
|
-
// (`chain = step.then(ok, err)`,「失败不断链」)⇒ 调用方 `void` 掉也不会有逃逸的 rejection
|
|
750
|
-
// (实测:摘掉这个 `.catch`,issue190 §4⑥/⑥b 仍绿)。它买的是**留痕** —— 披露行被静默丢掉时至少
|
|
751
|
-
// 有一行诊断可查;判别力由 §4⑥ 的「诊断行在场」断言看守。
|
|
752
283
|
const statusSeat = {
|
|
753
284
|
onStatusEvent: (e) => {
|
|
754
|
-
// S4 P0-a 计数腿:与普通流形 loop 的 `case "status"`、sync 腿、resume 腿同键同标签。
|
|
755
285
|
metrics?.inc("brain_retry_total", { phase: String(e.phase) });
|
|
756
|
-
// 账本腿:共享 LedgerSink 的 **`appendStatus` 同步入链门**(与 `onEvent` 的 `case "status"` 共用
|
|
757
|
-
// 同一个白名单 builder ⇒ 行的 type/data 与其余三腿逐字同形;判据 = issue190 §2/§3/§4 共用一份
|
|
758
|
-
// EXPECTED_ROW_DATA)。🔴 **别改回 `sink.onEvent(e)`**:那条路先 `await flush()` 才入链,同一拍
|
|
759
|
-
// 后到的 `onForwardEvent`(同步入链)会抢到更小的序号 ⇒ 账本时间线倒挂(codex 对抗复审 R2
|
|
760
|
-
// [medium],红先复现后修;反向钉 = issue190 §4⑤b)。
|
|
761
286
|
void sink.appendStatus(e).catch((err) => completionDiagLog?.("status_event_append_failed", { route: "bg-run-composite", taskId, err: err instanceof Error ? err.message : String(err) }));
|
|
762
287
|
},
|
|
763
288
|
};
|
|
764
289
|
if (verify !== undefined) {
|
|
765
|
-
// Developer-mode verification gate (1.44): multi-round (impl → adversarial verify → fix loop), not
|
|
766
|
-
// a stream — await the result and persist it terminal. The durable event log gets just the `done`
|
|
767
|
-
// (the verifier subtask runs on its own session). `result.verification` carries the verdict.
|
|
768
|
-
// #253(core 5.33.0 #246):第 4 位 = 供给座席,core 逐字转发给实现腿 + 验证器腿 + 每一轮修复腿。
|
|
769
|
-
// #255(core 5.34.0):`statusSeat` 只在这两条「只回结果」腿上展开(理由见其顶注)。
|
|
770
290
|
const result = await runWithVerification(runner, { ...spec, taskId, signal: cancelCtrl.signal }, verify, { ...fwdInternals, ...statusSeat });
|
|
771
291
|
await flush();
|
|
772
292
|
evictIfConflict(runner, spec.sessionId, result);
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
await flushPromptManifest(); // [998]②: the verify leg has no stream — persist the manifest before the terminal
|
|
776
|
-
const safe = await withModelUsage(stripCheckpointToken(result)); // E8: attach stats.modelUsage at terminal
|
|
293
|
+
await flushPromptManifest();
|
|
294
|
+
const safe = await withModelUsage(stripCheckpointToken(result));
|
|
777
295
|
await append("done", { result: safe });
|
|
778
296
|
reached = { kind: "done", result: safe };
|
|
779
297
|
await runStore.setTerminal(taskId, safe.status, safe, safe.errorMessage ?? null);
|
|
@@ -781,74 +299,42 @@ parkCheckpointReader) {
|
|
|
781
299
|
return;
|
|
782
300
|
}
|
|
783
301
|
if (cascade !== undefined) {
|
|
784
|
-
// Cascade (1.45): cheap→strong ladder; multi-attempt cold re-runs, not a stream. Persist terminal.
|
|
785
|
-
// #253(core 5.33.0 #246):第 4 位 = 供给座席,core 逐字转发给**每一级 rung**(顺序跑,一只 sink
|
|
786
|
-
// 按梯序拼接收到各级的帧 —— core 的 ATTRIBUTION 注写明 ToolActivity 自身不带 rung 身份)。
|
|
787
302
|
const result = await runCascade(runner, { ...spec, taskId, signal: cancelCtrl.signal }, cascade, { ...fwdInternals, ...statusSeat });
|
|
788
303
|
await flush();
|
|
789
304
|
evictIfConflict(runner, spec.sessionId, result);
|
|
790
|
-
await flushPromptManifest();
|
|
791
|
-
const safe = await withModelUsage(stripCheckpointToken(result));
|
|
305
|
+
await flushPromptManifest();
|
|
306
|
+
const safe = await withModelUsage(stripCheckpointToken(result));
|
|
792
307
|
await append("done", { result: safe });
|
|
793
308
|
reached = { kind: "done", result: safe };
|
|
794
309
|
await runStore.setTerminal(taskId, safe.status, safe, safe.errorMessage ?? null);
|
|
795
310
|
recordDone(safe, safe.status);
|
|
796
311
|
return;
|
|
797
312
|
}
|
|
798
|
-
// design/47 (mid-task steering): hold the live stream handle so POST /v1/runs/:id/steer can inject a steer via
|
|
799
|
-
// stream.steer() (drained at the next turn boundary). Only this plain-stream path is steerable — the verify/
|
|
800
|
-
// cascade branches above return a result, not a stream (parity with the preemptable registration). Deregistered
|
|
801
|
-
// in `finally`. Registering it AFTER the verify/cascade early-returns means those legs never enter the map.
|
|
802
|
-
// (供给座席 `fwdInternals` 已在本函数顶部铸好并被三条腿共用 —— 见那里的顶注。)
|
|
803
313
|
const stream = runner.runTaskStream({ ...spec, taskId, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, undefined, fwdInternals);
|
|
804
|
-
steerableStreamRef = stream;
|
|
314
|
+
steerableStreamRef = stream;
|
|
805
315
|
steerable?.set(taskId, stream);
|
|
806
316
|
for await (const ev of stream) {
|
|
807
|
-
// MF-Fleet: accrue this run's fleet-row tokens (turn_end usage). Fired before the switch so every event
|
|
808
|
-
// reaches it. No-op when no bus is wired. (Subagent task_progress arrives via the FORWARD sink above, not
|
|
809
|
-
// this top stream — the case below is forward-compat only; a stale comment here claimed otherwise.)
|
|
810
317
|
fleetPublisher?.onEvent(ev);
|
|
811
|
-
// [998]② release-review round 2: drain the pending prompt manifest at the TOP of every iteration —
|
|
812
|
-
// BEFORE the done/suspend branches below. A done-first stream (post-prepare failure/abort, or an
|
|
813
|
-
// immediate suspend) would otherwise skip the flush entirely: the manifest recorded at prepare would
|
|
814
|
-
// be cleared by the finally (or, worse, survive a lease-guarded no-op clear into a fast sibling
|
|
815
|
-
// resume's queue and land AFTER the suspension event). Here it always precedes its own terminal.
|
|
816
318
|
await flushPromptManifest();
|
|
817
|
-
// Non-ledger side-legs stay HERE (E18 anchor capture / S4 metrics) — the event→ledger projection itself
|
|
818
|
-
// is the shared sink below (single switch; the sync detach lane consumes the same one, so the persisted
|
|
819
|
-
// type/data vocabulary has ONE source). Ordering parity: anchor.onTool precedes the sink's tool_start
|
|
820
|
-
// flush (as before); the metric fires before the brain_status row lands (metric ≠ ledger, no row moves).
|
|
821
319
|
switch (ev.type) {
|
|
822
320
|
case "text_delta":
|
|
823
|
-
anchor.onText(ev.eventId);
|
|
321
|
+
anchor.onText(ev.eventId);
|
|
824
322
|
break;
|
|
825
323
|
case "tool_start":
|
|
826
|
-
anchor.onTool();
|
|
324
|
+
anchor.onTool();
|
|
827
325
|
break;
|
|
828
326
|
case "status":
|
|
829
|
-
// S4 (SILENT-FALLBACK P0-a): count the brain-layer liveness observation (the sink persists the row —
|
|
830
|
-
// previously: sync SSE only, async runs completely dark).
|
|
831
327
|
metrics?.inc("brain_retry_total", { phase: String(ev.phase) });
|
|
832
328
|
break;
|
|
833
329
|
case "message_committed":
|
|
834
|
-
// R8 (CC-parity rewind): a persisted session entry was committed. Capture a USER-message rewind anchor
|
|
835
|
-
// (keyed by taskId) straight from its `entryId` — the CC "rewind to the prompt" target. assistant entries
|
|
836
|
-
// are anchored by the turn-text path; tool-results are deliberately not anchored (resumeAt rejects them).
|
|
837
|
-
// NOT forwarded to the durable trace / shell stream (core口径: replay walks the persisted session tree —
|
|
838
|
-
// the sink deliberately ignores this type).
|
|
839
330
|
await anchor.onMessageCommitted(ev.role, ev.entryId);
|
|
840
331
|
break;
|
|
841
332
|
}
|
|
842
333
|
if (ev.type === "done") {
|
|
843
334
|
await flush();
|
|
844
335
|
if (ev.result.status === "suspended") {
|
|
845
|
-
// Durable F4 pause (design/45): PARK the run (non-terminal), NOT a `done`. Never persist the
|
|
846
|
-
// capability `checkpointToken` to the replayable event log / run row (search [18] Q1b) — only the
|
|
847
|
-
// non-secret gate (sink.appendParked). The submitter polling GET /v1/runs/:id sees status:"suspended";
|
|
848
|
-
// an operator resumes via /v1/approvals (token looked up internally). The worker is freed (runs_active -1).
|
|
849
|
-
// [4913]:graft 先于投影——事件里带待批 toolCallId(tool-less park 键缺席;读失败=F 类留痕,不挡 park)。
|
|
850
336
|
await sink.appendParked("suspended", await graftParkToolCallId(ev.result, parkCheckpointReader));
|
|
851
|
-
await flushModelUsage();
|
|
337
|
+
await flushModelUsage();
|
|
852
338
|
reached = { kind: "suspended" };
|
|
853
339
|
await runStore.setSuspended(taskId);
|
|
854
340
|
metrics?.addGauge("runs_active", -1);
|
|
@@ -856,18 +342,12 @@ parkCheckpointReader) {
|
|
|
856
342
|
continue;
|
|
857
343
|
}
|
|
858
344
|
if (ev.result.status === "needs_review") {
|
|
859
|
-
// design/80 D-B: a review pause (plan_review / dry_run_review) — PARK it like a suspend (keep the lock)
|
|
860
|
-
// so the /v1/assistant/tasks/:id/plan_review resume can claim the row. NEVER persist the token.
|
|
861
345
|
await parkNeedsReview(ev.result);
|
|
862
346
|
continue;
|
|
863
347
|
}
|
|
864
|
-
// A session CAS conflict arrives here as a failed result (core swallows it) — evict the
|
|
865
|
-
// now-stale warm-cache entry so the next turn re-wakes from TiDB instead of re-conflicting.
|
|
866
348
|
evictIfConflict(runner, spec.sessionId, ev.result);
|
|
867
|
-
// Structural token fence (no-op today; this plain-stream path's only token-bearing result is the
|
|
868
|
-
// suspended branch above, handled separately) — symmetric with the verify/cascade + resume paths.
|
|
869
349
|
{
|
|
870
|
-
const safe = await withModelUsage(stripCheckpointToken(ev.result));
|
|
350
|
+
const safe = await withModelUsage(stripCheckpointToken(ev.result));
|
|
871
351
|
await sink.appendDone(safe);
|
|
872
352
|
reached = { kind: "done", result: safe };
|
|
873
353
|
await runStore.setTerminal(taskId, safe.status, safe, safe.errorMessage ?? null);
|
|
@@ -875,39 +355,17 @@ parkCheckpointReader) {
|
|
|
875
355
|
}
|
|
876
356
|
continue;
|
|
877
357
|
}
|
|
878
|
-
// The shared event→ledger projection (text/reasoning coalescing, whitelist+redact builders, flush-before
|
|
879
|
-
// discipline, task_notification dedup against this leg's `notifiedKeys`) — see LedgerSink.onEvent.
|
|
880
|
-
// (The prompt-manifest drain runs at the loop TOP — ahead of done/suspend too, see above.)
|
|
881
358
|
await sink.onEvent(ev);
|
|
882
359
|
if (ev.type === "turn_end") {
|
|
883
|
-
await flushModelUsage();
|
|
884
|
-
// E18 resume-at: anchor (this turn's message eventId → its leaf SessionTreeEntry.id), but ONLY for a settled
|
|
885
|
-
// assistant-TEXT turn (text + no tools) — for a tool-using turn the leaf here is the last tool-result, NOT
|
|
886
|
-
// the message the eventId names (see TurnAnchorCapture). Best-effort + reset for the next turn.
|
|
360
|
+
await flushModelUsage();
|
|
887
361
|
await anchor.onTurnEnd();
|
|
888
362
|
}
|
|
889
363
|
}
|
|
890
|
-
// E12 (shell-host contract): after a COMPLETED run, pull core's opt-in post-completion prompt suggestions (a fire-and-forget
|
|
891
|
-
// pass in core; suggestions() never rejects + resolves to [] when off / not completed / empty). UNTRUSTED model
|
|
892
|
-
// text for the shell UI ONLY → redact + persist as a `suggestions` event on the durable ledger; NEVER re-fed to a
|
|
893
|
-
// model. ⚠️ 读法(TASKS-RUNS-1 注漂移修,[2400] 审计):它落账在 setTerminal **之后**,而 live tail
|
|
894
|
-
// (sse-log)在终态时有意**不投递**这一帧(见 sse-log.ts 注)——消费契约是 `await result()` 后从
|
|
895
|
-
// `GET /v1/runs/:id` 的 RunRecord.suggestions 读,**不是**从 events tail 等它。 Gated on a COMPLETED terminal — a suspended/parked/failed leg has
|
|
896
|
-
// none (core also returns [] for a non-completed task; the explicit status gate avoids the needless call + pins the
|
|
897
|
-
// completed-only contract rather than depending on core's guard).
|
|
898
364
|
if (spec.suggestNextPrompts && reached?.kind === "done" && reached.result.status === "completed") {
|
|
899
365
|
const suggestions = await stream.suggestions().catch(() => []);
|
|
900
|
-
// 🔴 **空结果也要落事件**(2026-07-26):这个事件是「已结算」的唯一信号。此前 `if (suggestions.length)`
|
|
901
|
-
// 才 append ⇒ 「跑完了、确实一条都没有」在轮询面上与「还没跑完」完全同形(都是 `suggestions: undefined`)
|
|
902
|
-
// ⇒ 轮询方无从判断该不该停。而 `setTerminal`(上方)在本段**之前**执行,所以确实存在一个真实的
|
|
903
|
-
// pending 窗口 —— 这正是本仓刚在后台 shell `poll()` 上修过的**同一类**:消费方拿终态当停止条件,
|
|
904
|
-
// 而数据在终态之后才到。⇒ 事件在 = 已结算(哪怕是空);事件不在 = 仍在跑。
|
|
905
366
|
await append("suggestions", { suggestions: suggestions.map((s) => redactSecrets(s)) });
|
|
906
367
|
}
|
|
907
368
|
};
|
|
908
|
-
// E23 + §4④: nest the elicitation AND AskUserQuestion per-run contexts (each its own ALS) around the run body — a
|
|
909
|
-
// background run's inbound elicitation AND the agent's AskUserQuestion both route to this run's durable events tail
|
|
910
|
-
// + owner-gated respond. Same id/owner/signal; the nesting order is immaterial (disjoint ALS).
|
|
911
369
|
await withPrincipal(principal, () => {
|
|
912
370
|
const withElicit = elicitation
|
|
913
371
|
? () => elicitation.runWithContext({ taskId, owner: elicitOwner ?? owner, emit: elicitEmit, abortSignal: cancelCtrl.signal }, runBody)
|
|
@@ -915,14 +373,6 @@ parkCheckpointReader) {
|
|
|
915
373
|
const withQuestion = () => question
|
|
916
374
|
? question.runWithContext({ taskId, owner: elicitOwner ?? owner, emit: questionEmit, abortSignal: cancelCtrl.signal }, withElicit)
|
|
917
375
|
: withElicit();
|
|
918
|
-
// #151 车3 刀 3b:审批 ALS —— 本腿**首次**拿到卡面(此前 `deps.onAsk` 在这条腿上恒无 ctx ⇒
|
|
919
|
-
// `"unavailable"` ⇒ 恒 park)。嵌套序同样无关(各自独立 ALS)。
|
|
920
|
-
// 🔴 F3 修:包不包 ALS 由 `approvalLeg` 决定,不再是「协调器在场就包」。
|
|
921
|
-
// · `active === false`(协议没上场)⇒ 不包 ⇒ `deps.onAsk` 找不到 ctx ⇒ `"unavailable"` ⇒ core
|
|
922
|
-
// durable park —— 这正是本腿在协议之前的逐字行为(A-1)。
|
|
923
|
-
// · `windowZero`(运维关窗 / 贴 deadline)⇒ 同样不包。本腿没有 per-task `spec.onAsk` 装配点,
|
|
924
|
-
// 「不包 ALS」就是 sync 腿那个 immediate-unavailable 闭包在这条腿上的**等价形**(§8.2 已亲证:
|
|
925
|
-
// `approverUnavailable === true` 与「onAsk 缺席」在 park 结局上逐字等价)。
|
|
926
376
|
const withApproval = () => approval && approvalLeg.active && !approvalLeg.windowZero
|
|
927
377
|
? approval.coordinator.runWithContext({
|
|
928
378
|
taskId,
|
|
@@ -930,22 +380,15 @@ parkCheckpointReader) {
|
|
|
930
380
|
emit: approvalEmit,
|
|
931
381
|
abortSignal: cancelCtrl.signal,
|
|
932
382
|
...(spec.sessionId ? { sessionId: spec.sessionId } : {}),
|
|
933
|
-
// 同一条 durable tail(§4.3(b))。经共用的投递口工厂:账本 append 失败 ⇒ 如实算「这一路没
|
|
934
|
-
// 接下」(本腿只有这一个 sink,于是=整条没送到),协调器据此走 park 而不是挂到窗到期(R2-1)。
|
|
935
383
|
emitCard: createApprovalCardEmitter({ appendDurable: (f) => approvalEmit(f) }),
|
|
936
|
-
// bg run 的这条腿恒是**首腿**(park→resume 走的是 server.ts 的 resume 腿,那条自己带
|
|
937
|
-
// token 摘要);空串是首腿的真值,不是「未知」。
|
|
938
384
|
legKey: "",
|
|
939
385
|
...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
|
|
940
386
|
}, withQuestion)
|
|
941
387
|
: withQuestion();
|
|
942
|
-
// SendUserFile 帧与 question 帧同投递面(durable events tail,web 渲染文件卡);嵌套序无关(ALS 互不相交)。
|
|
943
388
|
return sendUserFile
|
|
944
389
|
? sendUserFile.runWithContext({ taskId, emit: (frame) => { const { type, ...rest } = frame; return append(type, rest); } }, withApproval)
|
|
945
390
|
: withApproval();
|
|
946
391
|
});
|
|
947
|
-
// Cancelled mid-stream and the generator ended WITHOUT a terminal event (abort can end the stream cleanly
|
|
948
|
-
// rather than throwing) — finalize cancelled here so the run isn't left "running" for the reaper to catch.
|
|
949
392
|
if (!reached && cancelCtrl.signal.aborted) {
|
|
950
393
|
await flush();
|
|
951
394
|
const c = cancelledResult();
|
|
@@ -958,41 +401,29 @@ parkCheckpointReader) {
|
|
|
958
401
|
catch (err) {
|
|
959
402
|
const message = err instanceof Error ? err.message : String(err);
|
|
960
403
|
try {
|
|
961
|
-
await flush();
|
|
404
|
+
await flush();
|
|
962
405
|
if (reached?.kind === "done") {
|
|
963
|
-
// The task already produced a terminal result (its `done` event is durably logged); only the run-row
|
|
964
|
-
// write failed. Re-attempt THAT write — never write `failed` over a completed run.
|
|
965
406
|
await runStore.setTerminal(taskId, reached.result.status, reached.result, reached.result.errorMessage ?? null);
|
|
966
407
|
}
|
|
967
408
|
else if (reached?.kind === "suspended") {
|
|
968
|
-
await runStore.setSuspended(taskId);
|
|
409
|
+
await runStore.setSuspended(taskId);
|
|
969
410
|
}
|
|
970
411
|
else if (reached?.kind === "needs_review") {
|
|
971
|
-
await runStore.setNeedsReview(taskId);
|
|
412
|
+
await runStore.setNeedsReview(taskId);
|
|
972
413
|
}
|
|
973
414
|
else if (cancelCtrl.signal.aborted) {
|
|
974
|
-
// Cancelled: the abort threw out of the stream/verify/cascade. Record a cancelled terminal (queryable
|
|
975
|
-
// errorCode) instead of a generic failure, so the portal can show "cancelled by user" not an error.
|
|
976
415
|
const c = cancelledResult();
|
|
977
416
|
await append("failed", { errorMessage: c.errorMessage ?? "cancelled by user", errorCode: "cancelled" });
|
|
978
417
|
await runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null);
|
|
979
418
|
}
|
|
980
419
|
else {
|
|
981
|
-
// BL-12: redact the raw exception message before it lands in the durable event log / run row — a tool
|
|
982
|
-
// or transport error can carry a secret (a credentialed URL, a token) and the trace surface is served
|
|
983
|
-
// to TRACE_TOKEN holders. redactSecrets is best-effort + idempotent (the read side redacts again, BL-13).
|
|
984
420
|
const safeMsg = redactSecrets(message);
|
|
985
421
|
await append("failed", { errorMessage: safeMsg });
|
|
986
422
|
await runStore.setTerminal(taskId, "failed", null, safeMsg);
|
|
987
423
|
}
|
|
988
424
|
}
|
|
989
425
|
catch {
|
|
990
|
-
/* best-effort: the stale reaper will catch a run we couldn't finalize */
|
|
991
426
|
}
|
|
992
|
-
// recordDone runs exactly once: the success paths call it inline (and return/break before the throw); we
|
|
993
|
-
// reach here only if the terminal-row write threw AFTER the event was logged, or on a genuine failure.
|
|
994
|
-
// A cancelled terminal keeps its distinct metric label even on this retry path (council B2: the result's
|
|
995
|
-
// STATUS is "failed" — labeling by status would count a user cancel as a failure).
|
|
996
427
|
if (reached?.kind === "done")
|
|
997
428
|
recordDone(reached.result, reached.result.errorCode === "cancelled" ? "cancelled" : reached.result.status);
|
|
998
429
|
else if (reached?.kind === "suspended") {
|
|
@@ -1000,23 +431,16 @@ parkCheckpointReader) {
|
|
|
1000
431
|
metrics?.inc("tasks_total", { status: "suspended" });
|
|
1001
432
|
}
|
|
1002
433
|
else if (cancelCtrl.signal.aborted) {
|
|
1003
|
-
recordDone(null, "cancelled");
|
|
434
|
+
recordDone(null, "cancelled");
|
|
1004
435
|
}
|
|
1005
436
|
else {
|
|
1006
437
|
recordDone(null, "failed");
|
|
1007
438
|
}
|
|
1008
439
|
}
|
|
1009
440
|
finally {
|
|
1010
|
-
legLive = false;
|
|
1011
|
-
unregisterEngineNotice();
|
|
1012
|
-
// MF-Fleet: flip/remove the fleet row at the SINGLE finalization point — runs once for EVERY exit path
|
|
1013
|
-
// (success / cancel / error / suspend / needs_review). A truly-terminal status leaves the fleet; a PARKED
|
|
1014
|
-
// status (suspended→waiting / needs_review→awaiting approval) STAYS (the run is still active). A cancelled
|
|
1015
|
-
// run carries status:"failed"+errorCode:"cancelled" — map it to "killed" so the row reads as a kill, not a
|
|
1016
|
-
// failure. `reached` undefined ⇒ errored/aborted before any terminal event (catch recorded failed/cancelled).
|
|
441
|
+
legLive = false;
|
|
442
|
+
unregisterEngineNotice();
|
|
1017
443
|
if (fleetPublisher) {
|
|
1018
|
-
// [1839]①/(a) 案:真终态帧带残局键(usage=stats 映射 / cancel⇒stoppedBy:"user" / transcriptId=sessionId);
|
|
1019
|
-
// parked(suspended/needs_review)不带(publisher 侧也再挡一层)。
|
|
1020
444
|
if (reached?.kind === "done")
|
|
1021
445
|
fleetPublisher.onTerminal(reached.result.errorCode === "cancelled" ? "cancelled" : reached.result.status, fleetRunResiduals(reached.result));
|
|
1022
446
|
else if (reached?.kind === "suspended")
|
|
@@ -1025,34 +449,22 @@ parkCheckpointReader) {
|
|
|
1025
449
|
fleetPublisher.onTerminal("needs_review");
|
|
1026
450
|
else
|
|
1027
451
|
fleetPublisher.onTerminal(cancelCtrl.signal.aborted ? "cancelled" : "failed", {
|
|
1028
|
-
// errored/aborted before any terminal event — no TaskResult.stats to map; the transcript still exists.
|
|
1029
452
|
...(cancelCtrl.signal.aborted ? { stoppedBy: "user" } : {}),
|
|
1030
453
|
...(spec.sessionId ? { transcriptId: spec.sessionId } : {}),
|
|
1031
454
|
});
|
|
1032
455
|
}
|
|
1033
|
-
// 对抗复查 B-3(HIGH):identity-guard these two like `steerable` below — the run row flips suspended
|
|
1034
|
-
// BEFORE this finally runs, so a fast resume can re-register the SAME taskId (a fresh cancelCtrl /
|
|
1035
|
-
// preemptCtrl) in the window; an unconditional delete then evicted the NEW leg's entry, and drain
|
|
1036
|
-
// under-counted the resuming leg (its cancel/preempt handles also went unreachable).
|
|
1037
456
|
if (inflight?.get(taskId) === cancelCtrl)
|
|
1038
457
|
inflight.delete(taskId);
|
|
1039
458
|
if (preemptable?.get(taskId) === preemptCtrl)
|
|
1040
459
|
preemptable.delete(taskId);
|
|
1041
|
-
// Identity-guarded (1.78 review LOW, parity with the sync/resume legs): only delete the handle THIS leg
|
|
1042
|
-
// registered and only if it is still ours — a fast sibling leg re-registering the same taskId must not lose
|
|
1043
|
-
// its fresh handle to our teardown; never registered ⇒ nothing of ours to delete.
|
|
1044
460
|
if (steerableStreamRef !== undefined && steerable?.get(taskId) === steerableStreamRef)
|
|
1045
461
|
steerable.delete(taskId);
|
|
1046
462
|
for (const evict of subagentHandleEvictions)
|
|
1047
|
-
evict();
|
|
1048
|
-
modelUsage?.clear(taskId);
|
|
1049
|
-
promptManifests?.clear(taskId, manifestLease);
|
|
463
|
+
evict();
|
|
464
|
+
modelUsage?.clear(taskId);
|
|
465
|
+
promptManifests?.clear(taskId, manifestLease);
|
|
1050
466
|
clearInterval(heartbeat);
|
|
1051
467
|
}
|
|
1052
468
|
}
|
|
1053
|
-
/** How often a running instance refreshes its run's updated_at (liveness, independent of events).
|
|
1054
|
-
* Must stay strictly below `runStaleSec` (asserted at startup AND on every hot config candidate) or the
|
|
1055
|
-
* reaper would race live runs. #322:数值的单源是 `config-invariants.ts`(那条不变量的属主),两个消费面
|
|
1056
|
-
* ——心跳环与 run-stale 判据——因此不可能各读各的。 */
|
|
1057
469
|
export const HEARTBEAT_MS = LIVENESS_HEARTBEAT_MS;
|
|
1058
470
|
//# sourceMappingURL=runs.js.map
|