@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
|
@@ -5,7 +5,7 @@ import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTermi
|
|
|
5
5
|
import { defaultSubagentTailBus, projectTailFrame } from "../../fleet/subagent-tail-bus.js";
|
|
6
6
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
|
|
7
7
|
import { createLedgerSink } from "../../trace/ledger-sink.js";
|
|
8
|
-
import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
|
|
8
|
+
import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
|
|
9
9
|
import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
|
|
10
10
|
import { redactSecrets } from "../../trace/redact.js";
|
|
11
11
|
import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
|
|
@@ -18,45 +18,14 @@ import { createApprovalCardEmitter, resolveApprovalLeg, resolveStreamApprovalGat
|
|
|
18
18
|
import { buildActiveRunConflict, toDoneFrameResult } from "../active-run-conflict.js";
|
|
19
19
|
import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
20
20
|
import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
|
|
21
|
-
/** #135([C95]/[C96]):live 腿 SSE 数据帧的**唯一**出口 —— 载荷带账本可锚 `eventId`(非空 string)的帧
|
|
22
|
-
* 前缀 `id: <eventId>` 行(SSE 游标=账本 eventId,壳/SDK 拿它锚进账本或断线换道 events tail 续读);
|
|
23
|
-
* 无 eventId 的帧**不发行**,EventSource 沿用上一游标 —— 绝不合成占位 id(那会把不可锚的位置伪装成可锚)。
|
|
24
|
-
* durable 腿的 `id:` 行在 sse-log.ts(同一契约的另半);行为钉=test/live-sse-cursor.test.ts(规则式全流判据)。 */
|
|
25
|
-
/** #241([3731] 裁定 A):断连转 park 的有界宽限——ask 已被强转 park 路由后,给 core 从「收到
|
|
26
|
-
* "unavailable"」走到「铸 checkpoint + done(suspended)」的时间。到点仍没收到 done ⇒ 照旧 abort,
|
|
27
|
-
* 交断连支既有的 findPendingTokenBySession 竞态卫兵兜底(checkpoint 已落盘则仍 park)。
|
|
28
|
-
* 取值:core 的 park 是本地 IO(checkpoint 写盘/写库)+一次 done 帧,秒级富余;不设旋钮——它不是
|
|
29
|
-
* 行为面选择,只是竞态兜底的等待上限(设长了唯一代价是极端卡死时 abort 晚到)。 */
|
|
30
21
|
const DISCONNECT_PARK_GRACE_MS = 10_000;
|
|
31
22
|
function sseData(res, payload) {
|
|
32
23
|
const anchor = payload.eventId;
|
|
33
24
|
const idLine = typeof anchor === "string" && anchor.length > 0 ? `id: ${anchor}\n` : "";
|
|
34
25
|
res.write(`${idLine}data: ${JSON.stringify(payload)}\n\n`);
|
|
35
26
|
}
|
|
36
|
-
/**
|
|
37
|
-
* 同步提交腿的**唯一**应答口:`retryAfterSec` 在体里 ⇒ 标准 `retry-after` 头同值在头里。
|
|
38
|
-
*
|
|
39
|
-
* 🔴 为什么必须是同一个口(复审车 C,2026-08-05):`memory.admission_required`(503)与
|
|
40
|
-
* `usage.window_exhausted`(429)是同族「等一会儿再来」,而后者在 `http/server.ts` 的 pre-admission 门里
|
|
41
|
-
* **头体双给**。本腿此前只给体——但**自动重试引擎读的是头**:SDK `Transport.request` 取
|
|
42
|
-
* `parseRetryAfter(res.headers.get("retry-after"))`,退避睡 `retryAfterMs ?? backoff(attempt)`,而 503 在
|
|
43
|
-
* `RETRYABLE_STATUS` 内。头缺席 ⇒ 引擎回落 250ms/500ms 通用退避,默认 `maxRetries: 2` 的三次尝试**全部**
|
|
44
|
-
* 落在 core 的不可用窗内(core 侧 `Math.max(backoffMs, 1000)`),提交方在 1 秒内烧光重试预算然后报错 ——
|
|
45
|
-
* 这个提示存在的唯一目的正好被绕过。头体同源(体有才给、值逐字相同)也让「体说等 11 秒、头说别的」不可能。
|
|
46
|
-
*
|
|
47
|
-
* 放在应答口而不是铸体处:同步腿的 4xx/5xx **不进** idem 缓存(谓词 `status < 400`),但并发同键的调用方会
|
|
48
|
-
* 被 `peek` 喂在飞 promise 的**同一份**结果 —— 那条腿不重跑铸体处的代码,只走这里。
|
|
49
|
-
*/
|
|
50
27
|
function sendSubmitResult(res, resp) {
|
|
51
28
|
const body = resp.body;
|
|
52
|
-
// 两个判定各司其职,都不能省:`Object.hasOwn` 管**语义**——`in` 认原型链,而 `JSON.stringify` 只序列化
|
|
53
|
-
// 自有可枚举键,只用 `in` 就存在「头说等 11 秒、体里根本没这个键」的形(继承来的键足以点亮头);`in` 管
|
|
54
|
-
// **窄化**——本仓 lib 里的 `Object.hasOwn` 没有类型谓词重载,少了它就得写断言(边界禁项)。
|
|
55
|
-
// 🔴 诚实边界:这两个判定合起来**不等于**「JSON 里一定看得见」的通用证明(不可枚举自有键、数组上的具名键
|
|
56
|
-
// 都能同时通过而不被序列化)。本函数的真保证是**受控生产者**级的:本腿流经这里的应答体形状不止一种
|
|
57
|
-
// (200 的 `stripCheckpointToken(result)` 是引擎对象、409 是冲突材料……),而其中**唯一**带
|
|
58
|
-
// `retryAfterSec` 的是下面那个 memory.admission_required 臂里的对象字面量。写死这条而不是宣称一个更强的
|
|
59
|
-
// 不变式 —— 宣称过强正是「过期声明」的铸法。
|
|
60
29
|
if (typeof body === "object" && body !== null && Object.hasOwn(body, "retryAfterSec") && "retryAfterSec" in body) {
|
|
61
30
|
const sec = body.retryAfterSec;
|
|
62
31
|
if (typeof sec === "number" && Number.isFinite(sec))
|
|
@@ -64,20 +33,12 @@ function sendSubmitResult(res, resp) {
|
|
|
64
33
|
}
|
|
65
34
|
sendJson(res, resp.status, resp.body);
|
|
66
35
|
}
|
|
67
|
-
/**
|
|
68
|
-
* [3979] §2 发现性指路:`POST /v1/tasks` 收 verify/cascade 不拒、多尝试语义照常执行,但该同步形
|
|
69
|
-
* **没有事件通道**(契约 §4a-sext 成文;#253 的裁定=不是缺座位,是端点形状)。test 黑盒撞到
|
|
70
|
-
* 「200 却零事件回放」证明成文≠可发现,所以带 verify/cascade 的 200 体附本句(不改行为)。
|
|
71
|
-
* 注入点在 runSyncSubmitLeg 的三处 200 return(idem 缓存体自带 ⇒ peek 重放腿同形,不必二处注入)。
|
|
72
|
-
*/
|
|
73
36
|
const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their normal multi-attempt semantics, but the synchronous /v1/tasks endpoint has no event channel — no status/progress frames exist to replay on this shape. For observable events submit via POST /v1/runs and read GET /v1/runs/:id/events.";
|
|
74
37
|
export async function handleTasks(req, res, url, ctx) {
|
|
75
38
|
const miss = { fell: false };
|
|
76
39
|
await handleTasksBody(req, res, url, ctx, miss);
|
|
77
40
|
return !miss.fell;
|
|
78
41
|
}
|
|
79
|
-
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(A9 的唯一改动:去缩进;B3 的唯一改动:
|
|
80
|
-
* 给两个匿名 `idemCache.run` 回调与三条驱动腿起名)。裸 `return;` = 「本域已应答」;走到函数尾才是「没匹配上」。 */
|
|
81
42
|
async function handleTasksBody(req, res, url, ctx, miss) {
|
|
82
43
|
const { deps } = ctx;
|
|
83
44
|
const { idemCache, inflightRuns, cancelledViaVerb, steerableRuns, counters } = ctx.registry;
|
|
@@ -86,27 +47,17 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
86
47
|
const reqState = ctx.req;
|
|
87
48
|
const source = ctx.req.source;
|
|
88
49
|
if (req.method === "POST" && (url === "/v1/tasks" || url === "/v1/tasks/stream")) {
|
|
89
|
-
// Idempotency-Key replay BEFORE the rate/quota gates (council blocker): a retry with a known key must get
|
|
90
|
-
// the SAME response, not a fresh 429 because the window filled meanwhile. No-op without a key / on a miss.
|
|
91
50
|
const rawIdem = headerStr(req.headers["idempotency-key"]);
|
|
92
|
-
// Scope by identity only when there IS a key (BL-3) — avoids reading the principal header on every
|
|
93
|
-
// submit, and keeps the no-key path independent of principal config.
|
|
94
|
-
// F-fix: scope on the VERIFIED principal (gatedPrincipal) — on a direct door a spoofable header would let a
|
|
95
|
-
// caller poison/collide another principal's idempotency cache. (BFF/gated: identical to principalFrom.)
|
|
96
51
|
const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, gatedPrincipal(req, deps.config)) : undefined;
|
|
97
52
|
const cached = idemKey ? idemCache.peek(idemKey) : undefined;
|
|
98
53
|
if (cached) {
|
|
99
54
|
if (url === "/v1/tasks/stream") {
|
|
100
|
-
// The cached entry may be IN-FLIGHT (a retry arriving during the original stream) → set up the heartbeat
|
|
101
|
-
// BEFORE awaiting so a long wait doesn't idle-timeout, then replay the terminal result.
|
|
102
55
|
sseHeaders(res);
|
|
103
56
|
const hb = setInterval(() => {
|
|
104
57
|
if (!res.writableEnded)
|
|
105
|
-
res.write(`event: heartbeat\ndata: {}\n\n`);
|
|
58
|
+
res.write(`event: heartbeat\ndata: {}\n\n`);
|
|
106
59
|
}, 15_000);
|
|
107
|
-
hb.unref?.();
|
|
108
|
-
// #131-5②:重试客户端断连即清——`cached` 可悬到原始流 settle(deadline 级时长),此前这个
|
|
109
|
-
// timer 一直空转到那时(写有 writableEnded 守卫,烧的是 tick 本身);finally 仍是兜底清。
|
|
60
|
+
hb.unref?.();
|
|
110
61
|
res.on("close", () => clearInterval(hb));
|
|
111
62
|
try {
|
|
112
63
|
const resp = await cached;
|
|
@@ -120,7 +71,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
120
71
|
}
|
|
121
72
|
else {
|
|
122
73
|
const resp = await cached;
|
|
123
|
-
sendSubmitResult(res, resp);
|
|
74
|
+
sendSubmitResult(res, resp);
|
|
124
75
|
}
|
|
125
76
|
return;
|
|
126
77
|
}
|
|
@@ -129,65 +80,31 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
129
80
|
const prepared = await prepareSpec(req, res);
|
|
130
81
|
if (!prepared)
|
|
131
82
|
return;
|
|
132
|
-
// withPrincipal threads the caller's identity through the run so the cost tracer attributes spend
|
|
133
|
-
// (incl. council/team sub-tasks) to this principal's cumulative quota.
|
|
134
83
|
const principal = prepared.auth?.principal;
|
|
135
84
|
if (url === "/v1/tasks/stream") {
|
|
136
85
|
if (prepared.verify || prepared.cascade) {
|
|
137
86
|
sendError(res, 400, "request.field_conflict", "verify / cascade are not supported on /v1/tasks/stream (both are multi-attempt, not a single stream) — use /v1/tasks or /v1/runs");
|
|
138
87
|
return;
|
|
139
88
|
}
|
|
140
|
-
// R8 (CC parity, fixed for the AgentEvent contract — workflow re-review #2): mint the durable rewind
|
|
141
|
-
// handle (taskId) BEFORE the SSE headers so it rides as an `X-Task-Id` RESPONSE HEADER. The shell reads it even
|
|
142
|
-
// on a Ctrl+C before `done`, WITHOUT an out-of-contract `run_started` data frame (the SDK AgentEvent stream is a
|
|
143
|
-
// CLOSED oneOf; a synthetic data frame makes exhaustive consumers assertNever-throw). createRun below CLAIMS this id.
|
|
144
89
|
const earlyDurableTid = deps.runStore && prepared.spec.sessionId ? uuidv7() : undefined;
|
|
145
|
-
// [854]①b per-request opt-in 断连宽限:请求头 `x-detach-on-disconnect: true`(选头不选 body 字段——
|
|
146
|
-
// 这是本条连接的传输层语义,不该进持久化 body / 不该随 resume 重放;与 idempotency-key 同类)。
|
|
147
|
-
// 默认行为不动:交互 Esc = 断连即杀的反 token-burn 契约保留(下方 onDisconnect 的 abort 路径)。
|
|
148
|
-
// opt-in 后 client 断连不 abort,run 转后台跑完,结果按既有 durable 路径可查(X-Task-Id → GET /v1/runs/:id)。
|
|
149
|
-
// 无 durable 账本(无 runStore 或无 sessionId)时 400 fail-loud 而非静默忽略:忽略 = 断连仍杀 run,
|
|
150
|
-
// 与 caller 声明的语义相反;就算不杀,跑完的结果也无处可查(纯烧 token)—— body.model silent-drop 同类,
|
|
151
|
-
// 「不做临时方案」判 400 是真版。SSE 头未发,还能干净 400。
|
|
152
90
|
const detachOnDisconnect = headerStr(req.headers["x-detach-on-disconnect"]) === "true";
|
|
153
91
|
if (detachOnDisconnect && earlyDurableTid === undefined) {
|
|
154
92
|
sendError(res, 400, "request.precondition_unmet", "x-detach-on-disconnect requires a durable run (a run store + sessionId) — without one the detached result would be unqueryable");
|
|
155
93
|
return;
|
|
156
94
|
}
|
|
157
|
-
reqState.streamTaskId = earlyDurableTid;
|
|
158
|
-
// E8 双腿对齐:detach 腿的 modelUsage 归因键(sessionId 域)——声明在此层,leak-fence clear 在本层 finally。
|
|
95
|
+
reqState.streamTaskId = earlyDurableTid;
|
|
159
96
|
let usageKey;
|
|
160
|
-
// [854]①b/⑥:opt-in 一被受理就置 detached 标记(而非等断连时置)——record 的 res 'close' listener
|
|
161
|
-
// 注册在 onDisconnect 之前,断连时 record 先跑;在这里置位才能让 499 行真带上 detached:true。
|
|
162
|
-
// 语义不变:opt-in 受理后的任何断连都是 detach(run 不杀)。
|
|
163
97
|
reqState.streamDetached = detachOnDisconnect;
|
|
164
98
|
sseHeaders(res, earlyDurableTid ? { "x-task-id": earlyDurableTid } : undefined);
|
|
165
|
-
// G15([1833]/[1840]§五):taskId 一等信号进流首帧——浏览器 EventSource 与部分代理面读不到响应头,
|
|
166
|
-
// 消费端此前只能靠 x-task-id(cli liveClient 亲测取头)。meta 帧与头同源同条件(durable 腿才有 id,
|
|
167
|
-
// 无 durable ⇒ 头/帧同缺席,不铸空值);sessionId 同帧(server 铸新 session 时 caller 需要它绑定)。
|
|
168
|
-
// additive:未知 event 型按 SSE 语义被旧消费端忽略。
|
|
169
99
|
if (earlyDurableTid) {
|
|
170
100
|
res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", taskId: earlyDurableTid, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) })}\n\n`);
|
|
171
101
|
}
|
|
172
|
-
// Abort the underlying run (and stop the heartbeat) when the client disconnects — otherwise a dropped
|
|
173
|
-
// relay keeps burning billable tokens to completion (council). Mirrors the closed-flag pattern of the
|
|
174
|
-
// poll-based SSE endpoints, and additionally cancels the generator via spec.signal.
|
|
175
102
|
const ac = new AbortController();
|
|
176
|
-
// 🔴 stoppedBy 归因的注册点在**下面**(createRun 认领成功之后),不在这里 —— #168 件3,见那里的注。
|
|
177
103
|
let closed = false;
|
|
178
|
-
let detachLogged = false;
|
|
179
|
-
// #241([3731] 裁定 A):断连转 park 的宽限表——非空 = 已触发过 ask 转 park,断连支不再二次进臂。
|
|
104
|
+
let detachLogged = false;
|
|
180
105
|
let disconnectParkGrace;
|
|
181
106
|
const onDisconnect = () => {
|
|
182
|
-
// [854]①b:detach opt-in 的断连分支 —— 不置 closed(for-await 继续消费到 done,终态走既有
|
|
183
|
-
// setTerminal 路径落 durable 账本)、不 abort(run 不杀)。断连后的 res.write 落在已 destroy 的
|
|
184
|
-
// socket 上被 Node 丢弃;防御:挂一个 res 'error' swallow(destroy 后的 write 在部分 Node 版本
|
|
185
|
-
// 经 nextTick 发 ERR_STREAM_DESTROYED,无 listener 会变 uncaught)—— 只在 detach 已断连后挂,
|
|
186
|
-
// 正常车道的 res 错误面不被吞。task_notification 的 deliverable 谓词含 res.destroyed,断连后
|
|
187
|
-
// 自动改走 durable inbox(既有行为,正合 detach 语义)。
|
|
188
107
|
if (detachOnDisconnect) {
|
|
189
|
-
// 正常完成后的 req 'close' 不是断连(默认车道靠 abort-after-done 无害才容得下无 guard 的
|
|
190
|
-
// req listener;detach 车道要防它打出误导的 detached info)。
|
|
191
108
|
if (res.writableEnded)
|
|
192
109
|
return;
|
|
193
110
|
if (!detachLogged) {
|
|
@@ -197,17 +114,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
197
114
|
}
|
|
198
115
|
return;
|
|
199
116
|
}
|
|
200
|
-
// #241([3731] 裁定 A「park 应跨壳死存活」,[3730] core 零改动承运;cli [3729] L3 真机复现=本臂
|
|
201
|
-
// 缺席时的行为):断连瞬间本 run 有**流内未决 ask** 且 park 设施在场 ⇒ 不立即 abort——先把 ask 按
|
|
202
|
-
// 窗到期同路强转 park(coordinator 交 `"unavailable"` ⇒ core 走 durable park ⇒ done(suspended)),
|
|
203
|
-
// 给 core 一个**有界宽限**走完 park:宽限内 done 到 ⇒ 循环自然收束、走上面的 setSuspended 臂
|
|
204
|
-
// (行 park、卡存活、resume 重呈闭环);宽限用尽(park 卡在半路)⇒ 照旧 closed+abort,下面既有的
|
|
205
|
-
// findPendingTokenBySession 竞态卫兵接手(checkpoint 已落盘则仍 park)。真算力中断(无未决 ask)
|
|
206
|
-
// 语义**不变**:立即 abort——那是这个断连臂存在的原始理由(dropped relay 不烧 token)。
|
|
207
117
|
if (deps.checkpointStore && earlyDurableTid && disconnectParkGrace === undefined) {
|
|
208
118
|
const parked = deps.toolApproval?.parkOpenAsksForTask(earlyDurableTid) ?? 0;
|
|
209
119
|
if (parked > 0) {
|
|
210
|
-
res.on("error", () => undefined);
|
|
120
|
+
res.on("error", () => undefined);
|
|
211
121
|
deps.logger?.info("stream_disconnect_ask_parked", { taskId: earlyDurableTid, sessionId: prepared.spec.sessionId, asks: parked, graceMs: DISCONNECT_PARK_GRACE_MS });
|
|
212
122
|
disconnectParkGrace = setTimeout(() => {
|
|
213
123
|
closed = true;
|
|
@@ -221,54 +131,13 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
221
131
|
ac.abort();
|
|
222
132
|
};
|
|
223
133
|
req.on("close", onDisconnect);
|
|
224
|
-
// 🔴 req 'close' does NOT fire on a client disconnect once the request body has been fully consumed
|
|
225
|
-
// (prepareSpec read it before we get here; verified against node 24) — only res 'close' does. With only
|
|
226
|
-
// the req listener the disconnect-abort above never fired on this route, i.e. the dropped-relay token
|
|
227
|
-
// burn it exists to stop went right on burning. writableEnded guards the normal-completion close
|
|
228
|
-
// (end() already called → that close is not a disconnect).
|
|
229
134
|
res.on("close", () => {
|
|
230
135
|
if (!res.writableEnded)
|
|
231
136
|
onDisconnect();
|
|
232
137
|
});
|
|
233
|
-
// 注册后回看(合并码扫 C2,2026-08-13;判据与 src/http/sse-lifecycle.ts:76 同源,理由见其顶注
|
|
234
|
-
// 「为什么注册完还要回看一眼状态」):本腿的注册点排在 prepareSpec/门族若干 await 之后,Node 的
|
|
235
|
-
// 'close' 只发一次、后装监听器不补发——那段窗内断掉的连接若不回看,ac 永不 abort,整条 run 对着
|
|
236
|
-
// 死连接烧完(dropped-relay token burn 正是这套接线存在的原始理由,被自己的注册时序绕过)。
|
|
237
|
-
// writableEnded 卫兵同上一行:正常收流的 close 不是断连。
|
|
238
138
|
if ((res.destroyed || res.closed) && !res.writableEnded)
|
|
239
139
|
onDisconnect();
|
|
240
|
-
// #154 件一:本连接的 `wiring_manifest` 帧投哪一形。判据 = `explicitOperatorOk`(**不是** `isOperator`:
|
|
241
|
-
// 空 `OPERATOR_PRINCIPALS` 必须是「没有人是 operator」;把它读成「所有人都是」会让未配名单的部署把
|
|
242
|
-
// 治理面发给每一个订阅者)。**每连接求一次**、不逐帧求:身份在一条连接内不会变,而逐帧调用会让
|
|
243
|
-
// 每帧都重跑一遍凭证解析 —— 更重要的是,身份只在这一处判,帧循环里没有第二个可漂的判据。
|
|
244
140
|
const wiringOperatorFace = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
|
|
245
|
-
// #151 车3 §5.1:sync 腿的**开流重放**——`sseHeaders` + meta 帧之后、`streamBody` 之前,把同一
|
|
246
|
-
// (owner, session) 的未决审批卡投成 `approval_request` preamble 帧。
|
|
247
|
-
//
|
|
248
|
-
// 🔴 位置:必须排在**上面那套断连接线之后**(codex 复审 2026-08-06 的 high)。Node 的 `'close'`
|
|
249
|
-
// 只发一次 —— preamble 的 ≤2s 等待窗若插在接线之前,窗内断连的 close 事件在监听器注册前就烧掉了,
|
|
250
|
-
// `closed` 恒 false、`ac` 从不 abort ⇒ 模型腿在一条已断的连接上照常起跑并烧 token,「断连即杀 run」
|
|
251
|
-
// 这条既有契约在窗口内失效。排在接线之后 = 窗内断连照常 abort,腿拿到的是已 abort 的 signal
|
|
252
|
-
// (与「断连发生在起跑之后」同一条既有路径,不新增早退分支)。
|
|
253
|
-
//
|
|
254
|
-
// 🔴 为什么按 **session** 读而不是按 taskId:sync 腿每次开流都是**新 taskId**;它要接的未决卡来自
|
|
255
|
-
// 「同一 (owner, session) 的上一条腿 / 长命 bg 子代」——正是 `boundAsk` broker 的
|
|
256
|
-
// `["session", owner, sessionId]` 键域。按 taskId 读永远读不到它们。
|
|
257
|
-
//
|
|
258
|
-
// `sessionId` 缺席的 adhoc 流**不重放**:那条腿的行上 task_id/session_id 都是客户端永远看不到的
|
|
259
|
-
// 一次性 uuid,没有「重连回同一条腿」的概念(§5.1 表:这不是缺陷,是该腿形本来的语义)。
|
|
260
|
-
//
|
|
261
|
-
// owner = **verified** principal(与下方 `askOwner`、与 respond 的 owner gate 同一姿势)——重放面是
|
|
262
|
-
// 跨腿投递,租户门必须带上,不能让 A 租户的开流看到 B 租户的卡。
|
|
263
|
-
// 有界(2s)+ fail-open:store 抛或挂起都只记一次 warn、零卡继续开流(§5.3)。
|
|
264
|
-
// 🔴 开关谓词与 runs.ts 同源同理由(codex 复审第二轮 high:回滚下表里可能有残留 STREAM_PENDING
|
|
265
|
-
// 行,无谓词会破「开关关 = 逐字零变化」;顺带省掉关闭态每次开流的一次 store 往返)。钉:场景9。
|
|
266
|
-
//
|
|
267
|
-
// 🔴 A-010.7 随修(**兄弟扫查**:台账只点名了 runs.ts 那一处,这里是同形的第二处):谓词是
|
|
268
|
-
// **上场门**不是裸开关。五项合取(协调器在场 / 开关真 / backend 在场 / 账本非易失 / park 设施在场)
|
|
269
|
-
// 里少查三项 ⇒ 一个协议根本没上场的 SQL 部署,每次开流仍要白打一次库;回滚时更坏 —— 别的副本
|
|
270
|
-
// 落下的残留 `STREAM_PENDING` 行会被无门的重放腿照常投上 wire。换成与协调器注入 / 能力面 /
|
|
271
|
-
// 回决 501 口**同一个**谓词,「说没上场」⟺「零帧且零 store 读」才是结构成立的。
|
|
272
141
|
const replayGate = resolveStreamApprovalGate({
|
|
273
142
|
toolApprovalEnabled: Boolean(deps.toolApproval),
|
|
274
143
|
streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
|
|
@@ -290,57 +159,24 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
290
159
|
deps.logger?.warn?.("approval_preamble_capped", { sessionId: replaySessionId, dropped, replayMax: deps.config.streamApproval.replayMax, face: "tasks.stream" });
|
|
291
160
|
return buildApprovalPreambleSseFrames(frames);
|
|
292
161
|
}, (reason, err) => deps.logger?.warn?.("approval_preamble_failed", { sessionId: replaySessionId, reason, face: "tasks.stream", error: err instanceof Error ? err.message : String(err ?? "") }));
|
|
293
|
-
// 具名事件、**无 `id:` 行**:preamble 不是账本行,不参与 live 腿的 eventId 游标(sseData 的契约)。
|
|
294
|
-
// 等待窗里断连了就别再写(与心跳的 `!res.writableEnded && !res.destroyed` 同一判据)。
|
|
295
162
|
if (!res.writableEnded && !res.destroyed) {
|
|
296
163
|
for (const f of preambleFrames)
|
|
297
164
|
res.write(`event: ${f.event}\ndata: ${JSON.stringify(f.data)}\n\n`);
|
|
298
165
|
}
|
|
299
166
|
}
|
|
300
|
-
// Heartbeat so a long no-token gap (e.g. a debate phase) doesn't trip a mesh/ingress idle timeout on
|
|
301
|
-
// the relay (center). SSE comment lines are ignored by EventSource but keep the socket alive.
|
|
302
167
|
const hb = setInterval(() => {
|
|
303
|
-
// [854]①b: `!res.destroyed` — detach 断连后 run 可能再跑很久,别每 15s 往已死 socket 写心跳
|
|
304
|
-
// (destroyed = client 已断,写了也只是被丢弃/触发 error;默认车道断连即 abort,窗口极短,不受影响)。
|
|
305
168
|
if (!res.writableEnded && !res.destroyed)
|
|
306
|
-
res.write(`event: heartbeat\ndata: {}\n\n`);
|
|
169
|
+
res.write(`event: heartbeat\ndata: {}\n\n`);
|
|
307
170
|
}, 15_000);
|
|
308
|
-
hb.unref?.();
|
|
309
|
-
// Wrap the whole stream in idemCache.run so the IN-FLIGHT promise is stored BEFORE work begins — a retry
|
|
310
|
-
// that arrives WHILE this stream is still running (relay timeout → re-send same key) is then caught by the
|
|
311
|
-
// peek above and replays this result instead of starting a second billable stream (council). `ranLive`
|
|
312
|
-
// tells the caller that actually produced the stream (wrote events live) apart from a concurrent caller the
|
|
313
|
-
// cache deduplicated — the latter got no live events, so it must be handed the terminal result.
|
|
171
|
+
hb.unref?.();
|
|
314
172
|
let ranLive = false;
|
|
315
173
|
let resp;
|
|
316
|
-
// ── B3 ── 原文此处是 `idemCache.run` 的第二个实参、一个 **703 行匿名 async**。提为具名闭包:
|
|
317
|
-
// plain-stream 腿。捕获的自由变量(`ranLive`/`prepared`/`principal`/`res`/`ac`/`closed`/`hb`/
|
|
318
|
-
// `earlyDurableTid`/`usageKey`/`reqState`…)与原匿名回调完全同一批,故行为逐字。
|
|
319
174
|
const runStreamSubmitLeg = async () => {
|
|
320
175
|
ranLive = true;
|
|
321
176
|
let finalResult;
|
|
322
|
-
// [868]③: true once the post-stream settle block below recorded the run row's park/terminal state.
|
|
323
|
-
// A throw OUT of the stream (abort mid-iteration, or any plain Error before `done`) skips that whole
|
|
324
|
-
// block — the finally's backstop then settles the leaked `running` row (the durable twin of the
|
|
325
|
-
// fleet-row leak C fixed below; the fleet half got its settleFleet("failed") backstop, the run row
|
|
326
|
-
// was left to reapStale's stale-timeout with a misleading "run stalled" verdict).
|
|
327
177
|
let runRowSettled = false;
|
|
328
|
-
// Durable F4/ask on the stream path. Two things the sync/async paths do that the stream path
|
|
329
|
-
// used to skip (audit functional gap; both caught live on the gate#3 P3 cross-replica drill):
|
|
330
|
-
// 1. putCtx BEFORE the run → a suspend here (e.g. an AskUserQuestion gate, TC-5.4) is resumable
|
|
331
|
-
// on any replica; without it the operator's resume fails "resume context missing".
|
|
332
|
-
// 2. createRun a task_active claim at SUBMIT (+ heartbeat) → reapCtx's task_active guard then
|
|
333
|
-
// protects the ctx for the WHOLE first leg. The ctx's updated_at is stamped at submit and
|
|
334
|
-
// never refreshed, so a multi-minute first leg (a 5-min gradle build outlived the 120s
|
|
335
|
-
// REAP_RUN_STALE_SEC) ages the ctx past the cutoff and reapCtx deletes it MID-RUN — before
|
|
336
|
-
// the suspend's pending checkpoint can protect it. setSuspended KEEPS the claim (session
|
|
337
|
-
// stays locked submit→resume); a clean completion releases it via setTerminal.
|
|
338
178
|
let durableTaskId;
|
|
339
179
|
let durableHeartbeat;
|
|
340
|
-
// R8 (CC parity, interactive rewind on disconnect): the user-message entryId is captured INSIDE
|
|
341
|
-
// streamBody, but the rewind anchor must be writable from BOTH the done branch AND the disconnect branch
|
|
342
|
-
// (Ctrl+C before done) — so hoist it + a once-guarded put to this scope. Without this, a turn cancelled
|
|
343
|
-
// before `done` left NO anchor → the shell, even holding the early taskId, 404'd on rewind.
|
|
344
180
|
let userMsgEntryId;
|
|
345
181
|
let anchorPut = false;
|
|
346
182
|
const putRewindAnchor = () => {
|
|
@@ -351,68 +187,24 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
351
187
|
.put(prepared.spec.sessionId, durableTaskId, userMsgEntryId, principal ?? null)
|
|
352
188
|
.catch(() => deps.metrics?.inc("resume_anchor_capture_failed"));
|
|
353
189
|
};
|
|
354
|
-
// R8 (CC parity): the run-row claim + `durableTaskId` (the REWIND anchor key + the unique
|
|
355
|
-
// result.taskId re-stamp) is gated on `runStore` ALONE; the durable-suspend resume ctx (`putCtx`) on
|
|
356
|
-
// `checkpointStore`. They were bundled under ONE checkpointStore gate, over-coupling REWIND to
|
|
357
|
-
// DURABLE_APPROVAL — but E18 resume-at needs NO checkpoint (main.ts:815). Now matches the /v1/runs bg path:
|
|
358
|
-
// rewind works with just runStore (DB_BACKEND=mysql|pg|local), no approval. Claim-first preserved (session locked
|
|
359
|
-
// submit→resume; a concurrent submit → 409, never a silent 2nd leg, and a rejected submit never clobbers the ctx).
|
|
360
190
|
if (deps.runStore && prepared.spec.sessionId) {
|
|
361
|
-
const tid = earlyDurableTid ?? uuidv7();
|
|
191
|
+
const tid = earlyDurableTid ?? uuidv7();
|
|
362
192
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
363
193
|
if (created.ok)
|
|
364
|
-
clearTurnActivity(tid);
|
|
194
|
+
clearTurnActivity(tid);
|
|
365
195
|
if (created.ok)
|
|
366
|
-
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
|
|
196
|
+
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
|
|
367
197
|
if (!created.ok) {
|
|
368
|
-
const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId);
|
|
369
|
-
// Headers are already SSE — encode the rejection as the stream's terminal event; the 409 in
|
|
370
|
-
// the returned resp is for a deduplicated concurrent caller (and is never idem-cached).
|
|
371
|
-
// done 帧 additive 携带同一份材料 —— 形状铸在 toDoneFrameResult(有名字有类型),不在这行内联挑键。
|
|
198
|
+
const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId);
|
|
372
199
|
sseData(res, { type: "done", result: toDoneFrameResult(conflict) });
|
|
373
200
|
return { status: 409, body: conflict };
|
|
374
201
|
}
|
|
375
202
|
durableTaskId = tid;
|
|
376
203
|
}
|
|
377
|
-
// ── stoppedBy 归因(#168 件3:注册点必须晚于 run 认领)────────────────────────────────────
|
|
378
|
-
// probe-stoppedby: a client DISCONNECT on the sync stream lane is a USER stop too — Ctrl+C on the
|
|
379
|
-
// shell drops the SSE and this abort kills the run, but the mark was only wired on the
|
|
380
|
-
// cancel-verb/resume legs, so background children reaped by THIS abort read "parent"/"system"
|
|
381
|
-
// instead of "user". Same registration discipline as runs.ts (FIRST on the signal, before core's
|
|
382
|
-
// teardown listeners). Owner = the leg's core-canonical taskId(`spec.taskId ?? sessionId`——[3806] 起
|
|
383
|
-
// 本腿设 spec.taskId=durableTaskId,canonical 随之;无 durable 行的部署仍回落 sessionId,codex F4 连带);
|
|
384
|
-
// skipSessionScoped inside the helper keeps CC Backgrounded children unmarked.
|
|
385
|
-
//
|
|
386
|
-
// 🔴 **位置即正确性**(#168 件3,3c codex R3 定性的先存病):这条规则说的是「本请求断连 ⇒ 把
|
|
387
|
-
// owner=sessionId 的 task-scoped 子代记成人为停止」,而**只有抢到了这条 session 的请求**才有资格
|
|
388
|
-
// 那么说。注册早于 `createRun` 时,一次 pre-start 断连会把**另一条活 run** 的子代标成 user-stopped
|
|
389
|
-
// (认领随后 409,本请求根本没代表过这条 session)。窗 = SSE 头/首帧 + 开流 preamble(有界 2s)+
|
|
390
|
-
// 一次 DB 往返。认领失败那条路在上面就 return 了 ⇒ 一次都不注册。
|
|
391
|
-
// 无 durable 账本(无 runStore / 无 sessionId)的部署没有认领这回事,注册点与从前等价。
|
|
392
204
|
if (prepared.spec.sessionId)
|
|
393
205
|
markChildrenStoppedByUserOnAbort(ac.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
|
|
394
|
-
// [900]①/[906]③ detach 车道逐事件落账:sync 腿历史上只 createRun+setTerminal(events 账本恒空,
|
|
395
|
-
// detach 后壳只能轮询终局)。opt-in 车道把 for-await 的每个事件喂给与 bg 腿(runs.ts)共享的
|
|
396
|
-
// LedgerSink(src/trace/ledger-sink.ts —— 单一 switch,落账 type/data 形状与 GET /v1/runs/:id/events
|
|
397
|
-
// 的 bg 产出完全一致),detach 后壳换道 events tail 全帧续收。非 detach 车道恒 undefined = 零改动
|
|
398
|
-
// (不落账,byte-compat)。reasoning gate 与 bg 腿同一配置源(config.traceThinking / TRACE_THINKING)。
|
|
399
|
-
// 本腿账面差异(如实;[1840]§五车后已缩两项):suggestions 不落(本腿不调 stream.suggestions)、
|
|
400
|
-
// elicitation/question/approval 帧走 live emitAsk 面不落账、stream-open inbox drain 与 observer 臂
|
|
401
|
-
// 的 task_notification 走 wire/durable-inbox 既有链(post-detach 自动 park 进 inbox,下一腿 drain
|
|
402
|
-
// 补投)——不在本账本。已对齐:model_usage(E8 三件,下方 register/turn_end delta/终局 attach)、
|
|
403
|
-
// file_link(live+durable 双写,emit 回调处)。
|
|
404
206
|
let ledgerSink;
|
|
405
|
-
// [900]①: a terminal/park event already landed in the ledger — the failure backstops below must
|
|
406
|
-
// never write `failed` over it (bg 腿 `reached` 同款纪律).
|
|
407
207
|
let ledgerTerminal = false;
|
|
408
|
-
// model_usage 双腿对齐([1840]§五,W9 同根):detach 账本腿补上 bg 腿的 E8 三件(register /
|
|
409
|
-
// turn_end delta 落账 / 终局 SUM 进 stats.modelUsage)。归因键=**durableTaskId**(codex F4,
|
|
410
|
-
// 2026-08-13):core 的 taskId mint = `spec.taskId ?? sessionId`,而 [3806] 起本腿把 durableTaskId
|
|
411
|
-
// 塞进了 spec.taskId ⇒ tracer 的 brain.call e.taskId = durableTaskId——usageKey 若还钉 sessionId,
|
|
412
|
-
// tracker 的 register-gate 会把整条腿的用量当未注册 no-op(detach 腿 model_usage 帧全丢)。归因键
|
|
413
|
-
// 与账本键自此同源(此前「两域分家」的分家理由正是 spec.taskId 缺席,随 [3806] 一并消失)。
|
|
414
|
-
// 非 detach 车道照旧不登记(下方 E8 INTENTIONAL 排除注释仍准确——那腿无账本消费者)。
|
|
415
|
-
// (usageKey 声明在外层——leak-fence clear 在外层 finally,须覆盖 throw 路径。)
|
|
416
208
|
if (detachOnDisconnect && durableTaskId && deps.runStore) {
|
|
417
209
|
const rs = deps.runStore;
|
|
418
210
|
const tid = durableTaskId;
|
|
@@ -422,20 +214,16 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
422
214
|
deps.modelUsage?.register(usageKey);
|
|
423
215
|
}
|
|
424
216
|
if (deps.checkpointStore && prepared.spec.sessionId) {
|
|
425
|
-
// putCtx AFTER the claim (resumable). If this throws, the claim row self-heals: heartbeat hasn't
|
|
426
|
-
// started, so reapStale flips it failed + releases task_active after runStaleSec.
|
|
427
217
|
await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
|
|
428
218
|
}
|
|
429
219
|
if (durableTaskId && deps.runStore) {
|
|
430
220
|
const rs = deps.runStore;
|
|
431
221
|
const tid = durableTaskId;
|
|
432
|
-
const owner = principal ?? null;
|
|
222
|
+
const owner = principal ?? null;
|
|
433
223
|
const abortFromVerb = () => {
|
|
434
|
-
cancelledViaVerb.add(tid);
|
|
224
|
+
cancelledViaVerb.add(tid);
|
|
435
225
|
ac.abort();
|
|
436
226
|
};
|
|
437
|
-
// [1.207 codex H1] heartbeat ALSO polls the durable cancel flag (bg-leg parity, runs.ts) — a
|
|
438
|
-
// cancel landing on ANOTHER replica only sets the flag, and this leg previously had no consumer.
|
|
439
227
|
durableHeartbeat = setInterval(() => {
|
|
440
228
|
void rs.heartbeat(tid, owner).catch(() => undefined);
|
|
441
229
|
if (!ac.signal.aborted) {
|
|
@@ -444,36 +232,14 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
444
232
|
}
|
|
445
233
|
}, HEARTBEAT_MS);
|
|
446
234
|
durableHeartbeat.unref?.();
|
|
447
|
-
// [888]① interactive live-run cancel: register this leg's abort controller under the SAME
|
|
448
|
-
// in-flight map the bg/resume legs use — POST /v1/runs/:id/cancel's fast path (`inflightRuns
|
|
449
|
-
// .get(taskId)?.abort()`) previously only ever hit bg legs, so cancelling an interactive run
|
|
450
|
-
// flipped the durable flag that NOTHING on this leg consumed (release deferred to reapStale).
|
|
451
|
-
// Abort rides the leg's existing disconnect-kill chain (spec.signal), which settles the row.
|
|
452
235
|
inflightRuns.set(tid, ac);
|
|
453
|
-
// [1.207 codex H1] registration-window catch-up: a cancel that landed between createRun and this
|
|
454
|
-
// point (the awaited putCtx window) flipped the flag while the fast path found no entry — check
|
|
455
|
-
// once so it is honored now, not at the first heartbeat.
|
|
456
236
|
void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !ac.signal.aborted)
|
|
457
237
|
abortFromVerb(); }).catch(() => undefined);
|
|
458
238
|
}
|
|
459
|
-
// MF-Fleet: publish this run's row transitions to the fleet bus (a tracked/durable run becomes a
|
|
460
|
-
// fleet row; turn_end accrues tokens; core `task_progress` adds subagent child rows; terminal removes
|
|
461
|
-
// it). No-op for an ephemeral sync run (no durableTaskId) or when no fleetBus is wired.
|
|
462
|
-
// [1.211 codex L] fleet 装配纳入清理保护:heartbeat/inflightRuns 注册在上面已发生,而外层
|
|
463
|
-
// try/finally 从更后面才开始——fleetRunPublisher/onStart 若在这窗内 throw,计时器与 cancel 句柄
|
|
464
|
-
// 无人清理(SSE 头已发,请求以 500 帧终止但资源漏)。let+窄 try/catch 清理后 rethrow 补上这段。
|
|
465
239
|
let fleetPub;
|
|
466
240
|
try {
|
|
467
241
|
fleetPub = durableTaskId
|
|
468
|
-
|
|
469
|
-
// [3806] 起本腿设 spec.taskId = durableTaskId ⇒ canonical == durableTaskId(codex F4 连带):rootTaskId
|
|
470
|
-
// 若还钉 sessionId,core forward 的子代 parentTaskId(=canonical)嵌不进这棵树——fleet 树上成孤儿行。
|
|
471
|
-
// 🔒 Scope the fleet row with the SAME identity GET /v1/fleet/stream FILTERS by — `gatedPrincipal`
|
|
472
|
-
// (crypto-verified on a direct door), NOT the cost-attribution `principal` (= the spoofable header).
|
|
473
|
-
// On a direct door the two diverge: a spoofed header would tag the row under a victim's scope (cross-
|
|
474
|
-
// tenant leak) or under "default" while the verified owner's stream filters it OUT (the row vanishes
|
|
475
|
-
// from its real owner's view). Tenant-isolation parity with the stream filter (TOC review #6).
|
|
476
|
-
? fleetRunPublisher(deps.fleetBus, { runId: durableTaskId, scope: gatedPrincipal(req, deps.config) ?? "default", rootTaskId: durableTaskId, ...fleetRunLabels(prepared.spec.objective) }) // BC-1: name = short objective preview (description = live-activity, set by the publisher onEvent tool_start)
|
|
242
|
+
? fleetRunPublisher(deps.fleetBus, { runId: durableTaskId, scope: gatedPrincipal(req, deps.config) ?? "default", rootTaskId: durableTaskId, ...fleetRunLabels(prepared.spec.objective) })
|
|
477
243
|
: undefined;
|
|
478
244
|
}
|
|
479
245
|
catch (e) {
|
|
@@ -481,59 +247,34 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
481
247
|
clearInterval(durableHeartbeat);
|
|
482
248
|
if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
|
|
483
249
|
inflightRuns.delete(durableTaskId);
|
|
484
|
-
cancelledViaVerb.delete(durableTaskId);
|
|
250
|
+
cancelledViaVerb.delete(durableTaskId);
|
|
485
251
|
}
|
|
486
252
|
throw e;
|
|
487
253
|
}
|
|
488
|
-
// C (TOC adversarial review): settle the fleet row EXACTLY once. A plain Error thrown mid-stream
|
|
489
|
-
// (not done/CheckpointError/cancel) escapes the loop before any onTerminal fires → the row leaks as a
|
|
490
|
-
// stale "running" forever. Route every onTerminal through this guard so the finally below can settle a
|
|
491
|
-
// row no terminal branch reached, without ever double-settling one that did.
|
|
492
254
|
let fleetSettled = false;
|
|
493
255
|
const settleFleet = (status, residuals) => {
|
|
494
256
|
if (fleetSettled)
|
|
495
257
|
return;
|
|
496
258
|
fleetSettled = true;
|
|
497
|
-
fleetPub?.onTerminal(status, residuals);
|
|
259
|
+
fleetPub?.onTerminal(status, residuals);
|
|
498
260
|
};
|
|
499
|
-
// 1.207: THIS leg's live TaskStream (assigned inside streamBody) — held here so the outer finally can
|
|
500
|
-
// identity-guard its deregistration (only delete the handle WE registered; see the finally).
|
|
501
261
|
let liveStreamRef;
|
|
502
|
-
// design/122 ③: this leg's subagent-handle unregister thunks — run at leg end (finally), not on settle.
|
|
503
262
|
const subagentHandleEvictions = [];
|
|
504
|
-
// false once this leg settles — a bg subagent/bash completion after that has no live
|
|
505
|
-
// harness (idle) → the observer parks it in the durable inbox (see fwdInternals below).
|
|
506
263
|
let syncLegLive = true;
|
|
507
|
-
// option A: taskNotificationStreamKey (域:task_id:status[#seq], core [784]④a + codex F2 域二分)
|
|
508
|
-
// keys this leg already delivered (turn-open inbox drain, stream branch OR observer) — each arm
|
|
509
|
-
// skips keys another arm already carried, so a notification isn't double-written to the SSE (the
|
|
510
|
-
// turn-TAIL reap notification, which never reaches the stream, is the one the observer catches;
|
|
511
|
-
// core's pend-drain replay of a double-parked idle arrival is the one the drain-registration
|
|
512
|
-
// catches — core [784]④b). codex F1: NotifiedKeys (Map keyed on write outcome) for three-leg
|
|
513
|
-
// parity — this leg's writes are all synchronous res.write (no async failure window), so every
|
|
514
|
-
// claim registers resolved(true) and the has/skip semantics are unchanged here.
|
|
515
264
|
const syncNotifiedKeys = new NotifiedKeys();
|
|
516
265
|
try {
|
|
517
|
-
fleetPub?.onStart();
|
|
266
|
+
fleetPub?.onStart();
|
|
518
267
|
}
|
|
519
268
|
catch (e) {
|
|
520
269
|
if (durableHeartbeat)
|
|
521
270
|
clearInterval(durableHeartbeat);
|
|
522
271
|
if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
|
|
523
272
|
inflightRuns.delete(durableTaskId);
|
|
524
|
-
cancelledViaVerb.delete(durableTaskId);
|
|
273
|
+
cancelledViaVerb.delete(durableTaskId);
|
|
525
274
|
}
|
|
526
|
-
settleFleet("failed");
|
|
275
|
+
settleFleet("failed");
|
|
527
276
|
throw e;
|
|
528
277
|
}
|
|
529
|
-
// #310 `engine_notice` wire 腿(设计稿 = 黑板 [4630],三腿之一:**sync stream**)。
|
|
530
|
-
// core 的 `RunnerDeps.onNotice` 是 deps 级单 sink,通告上没有 runId ⇒ 归属只能靠 `detail.sessionId`;
|
|
531
|
-
// 分流器(`createEngineNoticeForwarder`)按会话把白名单三码投到这里注册的口。两口分开注册:
|
|
532
|
-
// · live = 本连接的 SSE(断流按既有 fail-open 词表留痕,不静默丢);
|
|
533
|
-
// · durable = detach 车道的账本(非 detach 车道 ledgerSink 恒 undefined ⇒ 无 durable 口,与本腿
|
|
534
|
-
// 其它帧的账面姿态一致;断连补看的那半由 detach 车道 / bg 腿承担)。
|
|
535
|
-
// 账本写不 flush 缓冲文本 —— 与 `onForwardEvent` 同款 fire-and-forget 姿态(seq 在链步内同步分配,
|
|
536
|
-
// 不会与主循环的 await 写乱序);代价是通告行可能排在同一 turn 的 text 行之前,那是"带外行"的既有账面语义。
|
|
537
278
|
const noticeLedger = ledgerSink;
|
|
538
279
|
const unregisterEngineNotice = registerEngineNoticeLeg({
|
|
539
280
|
sessionId: prepared.spec.sessionId,
|
|
@@ -544,44 +285,23 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
544
285
|
}
|
|
545
286
|
sseData(res, { type: "engine_notice", ...row });
|
|
546
287
|
},
|
|
547
|
-
// 写失败留痕不裸吞(codex 对抗复审 R1-[medium]):`route` 的同步 try 观察不到异步 reject,
|
|
548
|
-
// 本腿丢的是**断连补看**那一份(live SSE 已投),仍必须让"系统性在漏"在遥测里显形。
|
|
549
288
|
...(noticeLedger
|
|
550
289
|
? {
|
|
551
290
|
durable: (row) => void noticeLedger.append("engine_notice", row).catch(() => recordFailOpen("server.engine-notice.durable-append-failed", `leg=sync code=${row.code} task=${durableTaskId ?? ""}`)),
|
|
552
291
|
}
|
|
553
292
|
: {}),
|
|
554
293
|
});
|
|
555
|
-
// R8 rewind handle is now the X-Task-Id RESPONSE HEADER (set with earlyDurableTid above), not an
|
|
556
|
-
// out-of-contract `run_started` data frame — the shell reads it from the response headers even on an early
|
|
557
|
-
// disconnect, and the SDK's CLOSED AgentEvent stream stays unpolluted (workflow re-review #2 caught the drift).
|
|
558
294
|
try {
|
|
559
295
|
const streamBody = async () => {
|
|
560
296
|
await withPrincipal(principal, async () => {
|
|
561
|
-
// core 1.154 nested-subagent live tree: opt into the forward sink — a subagent's task_progress
|
|
562
|
-
// lives in its ISOLATED child stream (never on THIS top stream; core's only emitter is
|
|
563
|
-
// parentToolCallId-gated), so this sink is the ONLY path a tick reaches the top-run consumers.
|
|
564
|
-
// 1.78 (review HIGH): fan it out to the LIVE SSE too (shared whitelist+redact builder, carrying
|
|
565
|
-
// the E2 identity the shell binds concurrent ticks by) — previously it fed ONLY the fleet bus,
|
|
566
|
-
// so the conversation lane never saw a real tick and the in-loop task_progress branch below was
|
|
567
|
-
// forward-compat then — [2373]B-4:floor ^4.0.0 下它是热路径(腐注方向已翻转,勿按旧注当死支)。
|
|
568
|
-
// core swallow-guards a throwing sink.
|
|
569
|
-
// [3911] 定谳(bg 腿 twin 在 runs.ts):per-agent tail 的**内容帧供给**由本腿 spec 的
|
|
570
|
-
// forwardSubagentEvents 决定(core prepare-task 的 forward 门);缺席 ⇒ 那条流恒只有
|
|
571
|
-
// task_progress。逐帧按**句柄**登记供给形(键为什么是句柄:SubagentTailBus.modeByHandle 顶注)。
|
|
572
297
|
const tailContentMode = prepared.spec.forwardSubagentEvents === true ? "on" : "progress_only";
|
|
573
298
|
const fwdInternals = {
|
|
574
299
|
onForwardEvent: (e) => {
|
|
575
300
|
fleetPub?.onForwardEvent(e);
|
|
576
|
-
// [900]① detach 车道:子代理 forward 事件(tool_start/end + task_progress)也落账 —— bg 腿
|
|
577
|
-
// 同姿势(共享 sink 的 fire-and-forget 门,child deltas 有意不逐块落,见 LedgerSink.onForwardEvent)。
|
|
578
301
|
ledgerSink?.onForwardEvent(e);
|
|
579
|
-
// S2 live tail(core 1.370 bgAgentId;bg 腿 twin 在 runs.ts):句柄分发在 res 存活判之前
|
|
580
|
-
// ——主流断了 tail 订阅者照收(tail 是独立连接)。
|
|
581
302
|
{
|
|
582
303
|
const bg = e.bgAgentId;
|
|
583
304
|
if (bg !== undefined) {
|
|
584
|
-
// 登记在 hasSubscribers 快门**之前**:没人在听时也要学,否则晚开的 tail 读不到供给形。
|
|
585
305
|
defaultSubagentTailBus.noteHandleContentMode(bg, tailContentMode);
|
|
586
306
|
if (defaultSubagentTailBus.hasSubscribers(bg)) {
|
|
587
307
|
const f = projectTailFrame(e);
|
|
@@ -597,78 +317,37 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
597
317
|
sseData(res, { type: "task_progress", ...taskProgressEventData(e) });
|
|
598
318
|
}
|
|
599
319
|
else if (t === "tool_start") {
|
|
600
|
-
// C1 (core 1.219, spec.forwardSubagentEvents): a delegated child's live CONTENT events now
|
|
601
|
-
// ride the forward sink (each stamped parentToolCallId — the shell nests them under the
|
|
602
|
-
// delegation card). Echo through the SAME shared whitelist+redact builders as the top
|
|
603
|
-
// stream (§E1 — forwarded events are UNTRUSTED RAW; redaction is ours at the wire).
|
|
604
320
|
const tsv = e;
|
|
605
321
|
sseData(res, { type: "tool_start", ...toolStartEventData(tsv) });
|
|
606
322
|
}
|
|
607
323
|
else if (t === "tool_end") {
|
|
608
324
|
const te = e;
|
|
609
|
-
sseData(res, { type: "tool_end", ...toolEndEventData(te) });
|
|
325
|
+
sseData(res, { type: "tool_end", ...toolEndEventData(te) });
|
|
610
326
|
}
|
|
611
327
|
else if (t === "text_delta") {
|
|
612
|
-
// Child text is plain model output (same trust class as the top stream's text_delta, which
|
|
613
|
-
// rides verbatim); parentToolCallId attributes it. Whitelist the three fields explicitly.
|
|
614
328
|
const td = e;
|
|
615
329
|
sseData(res, { type: "text_delta", delta: td.delta, ...(td.eventId ? { eventId: td.eventId } : {}), ...(td.parentToolCallId ? { parentToolCallId: td.parentToolCallId } : {}) });
|
|
616
330
|
}
|
|
617
331
|
else if (t === "reasoning_delta") {
|
|
618
|
-
// §E1 parity with the top stream's live CoT: redactSecrets before echo.
|
|
619
332
|
const rd = e;
|
|
620
333
|
sseData(res, { type: "reasoning_delta", delta: redactSecrets(rd.delta), ...(rd.eventId ? { eventId: rd.eventId } : {}), ...(rd.parentToolCallId ? { parentToolCallId: rd.parentToolCallId } : {}) });
|
|
621
334
|
}
|
|
622
335
|
},
|
|
623
|
-
// background-completion observer — flip the child's fleet
|
|
624
|
-
// row (task_progress never emits a terminal tick) + park an idle-arrival in the durable
|
|
625
|
-
// inbox (workflow-type skipped: own durable chain). Rationale in runs.ts (bg-leg twin).
|
|
626
|
-
// core 1.283: `seq` = the per-stop-cycle counter (stop→wake→stop mints 1,2,…) — it MUST
|
|
627
|
-
// ride into the dedup key below (core [784]④a) and into the parked inbox entry.
|
|
628
|
-
// 白名单排查(2026-07-24,「彻查 as {} 连环坑」):手写窄类型换 core 真实类型,理由同 runs.ts twin。
|
|
629
336
|
onTaskNotification: (n) => {
|
|
630
337
|
if (n.task_type === "workflow")
|
|
631
338
|
return;
|
|
632
|
-
// S2 复审 #3:中途终态信号——task_settled 帧让订阅者不再 heartbeat 永挂(路由收帧即收流)。
|
|
633
|
-
// [3911] 终态即撤供给形登记:同一 a* 句柄可进入下一个**复活周期**,而新周期的宿主腿模式
|
|
634
|
-
// 可能相反——不撤会把上一周期的答案报给新周期的首帧前窗口(理由全文见 forgetHandleContentMode)。
|
|
635
339
|
defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
|
|
636
340
|
if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
|
|
637
341
|
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) } : {}) });
|
|
638
342
|
}
|
|
639
|
-
// id-domain alias: flip by payload.sessionId (= the tick's uuid domain), fallback task_id.
|
|
640
|
-
// [2687-cli] 幽灵行案:只对 agent 族终态打(isFleetAgentTerminalNotification 单源判别,
|
|
641
|
-
// 病灶链见其 doc 注);bash/monitor 的通知帧/park 面照走。孪生:runs.ts bg 腿、http/server.ts resume 腿。
|
|
642
343
|
const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
|
|
643
|
-
// 🔴 对抗评审 2026-07-11(HIGH,「下一 turn 0 帧」最强根因):the teardown WINDOW — after the
|
|
644
|
-
// client disconnects (the user killed the turn → ac.abort → core reaps the bg child → THIS
|
|
645
|
-
// notification fires) but BEFORE the outer finally flips `syncLegLive=false` (several awaits sit
|
|
646
|
-
// between the for-await break and the finally). In that window the old predicate neither parked
|
|
647
|
-
// (legLive still true) nor wrote (socket dead) — the notification vanished with no inbox copy and
|
|
648
|
-
// no frame. Fold the dead-socket state into the park predicate: the leg is only DELIVERABLE when
|
|
649
|
-
// it is live AND the socket can still be written. 🔴 `res.destroyed` is the CLIENT-disconnect
|
|
650
|
-
// signal (`writableEnded` only flips when WE call res.end()) — the real-HTTP retest caught the
|
|
651
|
-
// writableEnded-only predicate still writing into the dead socket.
|
|
652
344
|
const deliverable = syncLegLive && !res.writableEnded && !res.destroyed;
|
|
653
345
|
const parked = !deliverable && Boolean(deps.workflowCompletionInbox && prepared.spec.sessionId);
|
|
654
|
-
// diagnosability (rationale in runs.ts twin).
|
|
655
346
|
deps.logger?.info?.("task_notification_observed", { route: "sync-stream", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: syncLegLive, parkedDurable: parked });
|
|
656
347
|
if (parked) {
|
|
657
|
-
// 对抗评审 2026-07-11(MED):the fire-and-forget swallow hid EVERY enqueue failure (SQL blip,
|
|
658
|
-
// fence race) while the diag line above had already printed parkedDurable:true — a false park✓
|
|
659
|
-
// fingerprint. Log the failure; behavior (never fault the callback) unchanged.
|
|
660
348
|
void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(prepared.spec.sessionId, askOwner, n, Date.now(), durableTaskId)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "sync-stream", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
|
|
661
349
|
}
|
|
662
350
|
else if (deliverable) {
|
|
663
|
-
// option A (turn-TAIL killed receipt must be IMMEDIATE): a teardown-time reap
|
|
664
|
-
// notification fires while the SSE is still open but past the stream's delivery window —
|
|
665
|
-
// write the display frame directly, skipping keys the stream already carried (mid-turn
|
|
666
|
-
// dedup; the shell's dedup stays the outer belt). core [784]④a: the key folds `seq` in
|
|
667
|
-
// (shared taskNotificationStreamKey) — the old seq-less `${task_id}:${status}` shape
|
|
668
|
-
// REVERSE-swallowed a second stop-cycle's same-status frame on this arm (stop→wake→stop:
|
|
669
|
-
// the seq=2 completed frame collided with cycle 1's key and the user never saw it).
|
|
670
|
-
// codex F1: res.write is a synchronous enqueue (no async failure window — a dead socket
|
|
671
|
-
// is already folded into `deliverable` above), so the claim registers resolved(true).
|
|
672
351
|
const key = taskNotificationStreamKey(n);
|
|
673
352
|
if (syncNotifiedKeys.get(key) === undefined) {
|
|
674
353
|
syncNotifiedKeys.set(key, Promise.resolve(true));
|
|
@@ -676,10 +355,6 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
676
355
|
}
|
|
677
356
|
}
|
|
678
357
|
},
|
|
679
|
-
// C2 (core 1.219) + design/122 (core 1.225 ③): register each sync delegation's steer handle
|
|
680
|
-
// under this run's durable taskId (the X-Task-Id the shell holds). NOT evicted on settle —
|
|
681
|
-
// resume is only legal after settle; evicted at leg end via the collected identity-guarded
|
|
682
|
-
// thunks (outer finally), so a settled child stays steer-409/resume-able while the run lives.
|
|
683
358
|
...(deps.subagentSteerRegistry && durableTaskId
|
|
684
359
|
? {
|
|
685
360
|
onSubagentSpawn: (handle) => {
|
|
@@ -688,16 +363,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
688
363
|
}
|
|
689
364
|
: {}),
|
|
690
365
|
};
|
|
691
|
-
|
|
692
|
-
// leave a per-prompt rewind anchor, else an interactive-turn rewind 404s (the bg + resume legs capture
|
|
693
|
-
// it, this one didn't). Buffer the committed USER-message entryId on the HOISTED `userMsgEntryId` (so
|
|
694
|
-
// the disconnect branch can anchor too); the done branch anchors here, the disconnect branch below.
|
|
695
|
-
// core 1.207: hold the LIVE TaskStream handle + register it (keyed by durableTaskId = the
|
|
696
|
-
// X-Task-Id the shell already holds) so the replica-local verbs — POST /v1/runs/:id/detach
|
|
697
|
-
// (mid-flight ctrl+b) and, as a natural consequence, steer/compact — now reach a mid-turn
|
|
698
|
-
// INTERACTIVE run too (previously only the bg + resume legs registered). Deregistered in the
|
|
699
|
-
// outer finally (with the fleet settle), so an early throw can't leak a stale handle.
|
|
700
|
-
const liveStream = prepared.runner.runTaskStream({ ...prepared.spec, signal: ac.signal, ...(durableTaskId ? { taskId: durableTaskId } : {}) }, undefined, fwdInternals); // [3806]:tid 必须进 spec.taskId——缺席时 core 按 sessionId fallback 喂 noteTaskRun,lastRunId 投影被污染(bg 腿一直有传,两腿对称) // #196:场景 hands 判别位选出的 Runner
|
|
366
|
+
const liveStream = prepared.runner.runTaskStream({ ...prepared.spec, signal: ac.signal, ...(durableTaskId ? { taskId: durableTaskId } : {}) }, undefined, fwdInternals);
|
|
701
367
|
liveStreamRef = liveStream;
|
|
702
368
|
if (durableTaskId)
|
|
703
369
|
steerableRuns.set(durableTaskId, liveStream);
|
|
@@ -707,47 +373,19 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
707
373
|
if (ev.type === "message_committed" && ev.role === "user")
|
|
708
374
|
userMsgEntryId = ev.entryId;
|
|
709
375
|
if (ev.type === "done") {
|
|
710
|
-
// Never stream the durable-resume capability token to the client: a
|
|
711
|
-
// durable suspend's done event carries it. Strip it from BOTH the live write and the cached
|
|
712
|
-
// body (handed to a concurrent deduplicated caller below). No-op for a normal result.
|
|
713
|
-
// [4913]:graft 先于 strip——durable park 的 done 帧顶层带待批 toolCallId(cli 判别子 v3
|
|
714
|
-
// 写者② 的读点;tool-less park / 非 park / 读失败 = 键缺席,strip 语义不变)。
|
|
715
376
|
finalResult = stripCheckpointToken(await graftParkToolCallId(ev.result, deps.checkpointStore));
|
|
716
|
-
// R8 (CC parity, interactive rewind): on the sync leg core sets result.taskId = sessionId
|
|
717
|
-
// (spec.taskId is left unset so a forwarded subagent nests under the run — server.ts above), which
|
|
718
|
-
// is NOT unique per prompt → it can't be a per-turn rewind handle (every turn would collide). Re-
|
|
719
|
-
// stamp the WIRE result.taskId to the unique durable run id (uuidv7, == the bg path's result.taskId
|
|
720
|
-
// → consistent across paths) so the shell gets a unique per-prompt handle. Internal canonical
|
|
721
|
-
// (fleet/subagent nesting) keeps sessionId — only the external done-frame value changes. Durable-run
|
|
722
|
-
// only (ephemeral has no anchor store ⇒ no rewind anyway), so the fallback keeps sessionId.
|
|
723
377
|
if (durableTaskId && finalResult)
|
|
724
378
|
finalResult = { ...finalResult, taskId: durableTaskId };
|
|
725
|
-
// [1.207 codex H2] cancel-verb abort label: core's external-signal abort yields
|
|
726
|
-
// failed WITHOUT an errorCode — project `cancelled` onto the wire+persisted form so
|
|
727
|
-
// /cancel's outcome is uniform (first-writer-wins would pin the unlabeled form). Only
|
|
728
|
-
// an abort-shaped FAILED result is labeled — a completion that raced the abort stands.
|
|
729
379
|
if (durableTaskId && cancelledViaVerb.has(durableTaskId) && finalResult?.status === "failed" && !finalResult.errorCode) {
|
|
730
380
|
finalResult = { ...finalResult, errorCode: "cancelled" };
|
|
731
381
|
}
|
|
732
382
|
finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
|
|
733
|
-
// Anchor this interactive prompt (taskId → user-message entryId) keyed by the SAME unique id the
|
|
734
|
-
// shell now holds (done.result.taskId == durableTaskId). entryId came straight from
|
|
735
|
-
// `message_committed{role:"user"}` (no getLeafId). Best-effort + once-guarded — a capture failure
|
|
736
|
-
// only makes THIS turn non-rewindable, never faults the run; the disconnect branch shares the guard.
|
|
737
383
|
putRewindAnchor();
|
|
738
|
-
settleFleet(finalResult?.status ?? "completed", fleetRunResiduals(finalResult));
|
|
739
|
-
// [900]① detach 车道:done 也落账(flush 攒批 + suspended/needs_review/done 投影,bg 词汇;
|
|
740
|
-
// finalResult 已 stripCheckpointToken + taskId 重印 + cancel 标注 —— 账本形 == wire 形)。
|
|
741
|
-
// 放在 res.write 之前:壳收到 wire done 后立刻换道 events tail 也必见终局帧。既有
|
|
742
|
-
// finalize/settleFleet/res.write/setTerminal 顺序不动(落账是旁路)。
|
|
384
|
+
settleFleet(finalResult?.status ?? "completed", fleetRunResiduals(finalResult));
|
|
743
385
|
if (ledgerSink && finalResult) {
|
|
744
|
-
// E8 双腿对齐:终局 flush 最后一拍 delta + SUM 全账本 model_usage 进 stats.modelUsage
|
|
745
|
-
// (bg 腿 withModelUsage 同形)。键自 codex F4 修后同源:usageKey=durableTaskId=账本键
|
|
746
|
-
// (getEvents 包一层保留——按变量各取所需,同值不同职,防未来再分家时又要拆)。
|
|
747
386
|
if (usageKey && deps.modelUsage && deps.runStore && durableTaskId) {
|
|
748
387
|
const rs2 = deps.runStore;
|
|
749
388
|
const tid2 = durableTaskId;
|
|
750
|
-
// attach 失败(SUM 读瞬断)不许炸掉 onDone——终局帧比 modelUsage 汇总重要,回落原 result。
|
|
751
389
|
const fr = finalResult;
|
|
752
390
|
finalResult = await attachModelUsage(fr, { append: ledgerSink.append, getEvents: (_id, after) => rs2.getEvents(tid2, after), modelUsage: deps.modelUsage, taskId: usageKey }).catch(() => fr);
|
|
753
391
|
}
|
|
@@ -757,119 +395,57 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
757
395
|
sseData(res, { ...ev, result: finalResult });
|
|
758
396
|
continue;
|
|
759
397
|
}
|
|
760
|
-
// [900]① detach 车道逐事件落账(await 保序 —— 与 bg 腿同代价姿势;message_committed/未知型
|
|
761
|
-
// sink 内部 no-op)。非 detach 车道 ledgerSink 恒 undefined = 零改动。
|
|
762
398
|
if (ledgerSink) {
|
|
763
399
|
await ledgerSink.onEvent(ev);
|
|
764
|
-
// E8 双腿对齐:turn 边界 drain per-model delta 落账(bg 腿 flushModelUsage 同位同形)。
|
|
765
400
|
if (ev.type === "turn_end" && usageKey)
|
|
766
401
|
await appendModelUsageDelta(ledgerSink.append, deps.modelUsage, usageKey);
|
|
767
402
|
}
|
|
768
|
-
fleetPub?.onEvent(ev);
|
|
769
|
-
// S4 P0-a: count brain-liveness phases on the sync leg too. The bg/resume legs count at
|
|
770
|
-
// their append sites.
|
|
403
|
+
fleetPub?.onEvent(ev);
|
|
771
404
|
if (ev.type === "status")
|
|
772
405
|
deps.metrics?.inc("brain_retry_total", { phase: String(ev.phase) });
|
|
773
|
-
// E1 (shell-host contract): tool_end.output AND (core 1.203) tool_end.structured are
|
|
774
|
-
// UNTRUSTED RAW from core (file/shell/API bodies; a structured card carries `originalFile`) —
|
|
775
|
-
// redact at the live wire boundary before echo (core only SIZE-bounds; redaction is ours, same
|
|
776
|
-
// contract as the durable path). 🔒 EXPLICIT WHITELIST, not `{...ev}` (the 1.72 task_progress
|
|
777
|
-
// lesson, re-proven by `structured`: a spread forwards a future core field RAW — 1.203 landing
|
|
778
|
-
// meant `structured` rode this leg unredacted until this build). Known-safe fields verbatim:
|
|
779
|
-
// ids/name/label/isError/truncated + E2 identity. NB the old guard also skipped redaction when
|
|
780
|
-
// `output` was undefined (→ raw catch-all) — the whitelist path now covers every tool_end.
|
|
781
406
|
if (ev.type === "status") {
|
|
782
|
-
// TaskEvent 三腿审查(steering_injected 跟进批):`status`(BrainStatus.detail,UNTRUSTED —
|
|
783
|
-
// 可能回显 provider 的 rate-limit/retry 报文)此前落裸 catch-all 未脱敏,与 durable 两腿
|
|
784
|
-
// (ledger-sink.onEvent + resume switch)共用的 brainStatusEventData 脱敏不一致——同类
|
|
785
|
-
// 「留在 catch-all」教训(1.72 task_progress),现挪显式分支,三腿同门同形。
|
|
786
407
|
sseData(res, { type: "status", ...brainStatusEventData(ev) });
|
|
787
408
|
}
|
|
788
409
|
else if (ev.type === "tool_end") {
|
|
789
410
|
const te = ev;
|
|
790
|
-
sseData(res, { type: "tool_end", ...toolEndEventData(te) });
|
|
411
|
+
sseData(res, { type: "tool_end", ...toolEndEventData(te) });
|
|
791
412
|
}
|
|
792
413
|
else if (ev.type === "tool_start") {
|
|
793
|
-
// 🔒 E1 parity (TOC adversarial review): redact tool_start.args on the LIVE wire too — the model's
|
|
794
|
-
// tool-call inputs can echo a secret it read (e.g. a value from a redacted tool_end fed into the
|
|
795
|
-
// next call). Whitelist for the same reason as tool_end (shared durable builder = one shape).
|
|
796
414
|
const tsv = ev;
|
|
797
415
|
sseData(res, { type: "tool_start", ...toolStartEventData(tsv) });
|
|
798
416
|
}
|
|
799
417
|
else if (ev.type === "task_notification") {
|
|
800
|
-
// core 1.202 (design/115 P2): a background task (workflow) finished while THIS harness was live —
|
|
801
|
-
// core already injected the <task-notification> XML into the model in-process; this is the display
|
|
802
|
-
// projection for the shell. Whitelist + redact via the shared builder (summary/result are UNTRUSTED
|
|
803
|
-
// free text). The shell dedups against the inbox-drain `workflow_complete` lane by run id.
|
|
804
|
-
// core [784]④b(跨层双泊车): a notification that arrived while the previous leg was idle is
|
|
805
|
-
// parked TWICE — core pends it in-Runner (pendingSessionNotifications) AND our sync observer
|
|
806
|
-
// parks it in the durable inbox. On this turn-open, our inbox drain (below the streamBody def)
|
|
807
|
-
// already wrote the frame and REGISTERED its key; core's pend-drain replays the same payload as
|
|
808
|
-
// a stream `task_notification` event (runtask.js queue.push) — skip it here instead of writing
|
|
809
|
-
// the wire twice. Same check also folds the mid-turn observer-then-stream echo. A frame whose
|
|
810
|
-
// key is NOT yet on this stream writes exactly as before (the first copy is never eaten).
|
|
811
|
-
// codex F1/F2: same await-the-claim shape as the bg/resume twins (on THIS leg every claim
|
|
812
|
-
// is resolved(true) — sync writes — so the await is immediate); task_type rides the key.
|
|
813
418
|
const tnv = ev.notification;
|
|
814
419
|
const tnvKey = tnv?.task_id ? taskNotificationStreamKey({ task_type: tnv.task_type, task_id: tnv.task_id, status: String(tnv.status), seq: tnv.seq }) : undefined;
|
|
815
420
|
const tnvPrior = tnvKey !== undefined ? syncNotifiedKeys.get(tnvKey) : undefined;
|
|
816
421
|
if (tnvPrior === undefined || !(await tnvPrior)) {
|
|
817
422
|
if (tnvKey !== undefined)
|
|
818
|
-
syncNotifiedKeys.set(tnvKey, Promise.resolve(true));
|
|
423
|
+
syncNotifiedKeys.set(tnvKey, Promise.resolve(true));
|
|
819
424
|
sseData(res, { type: "task_notification", ...taskNotificationEventData(ev) });
|
|
820
425
|
}
|
|
821
426
|
}
|
|
822
427
|
else if (ev.type === "reasoning_delta" && typeof ev.delta === "string") {
|
|
823
|
-
// 🔒 E1 parity (TOC adversarial review): redact the CoT on the LIVE wire too. The agent's
|
|
824
|
-
// reasoning can echo a secret it read (core [R57]: CoT may carry reasoned-about secrets). The
|
|
825
|
-
// DURABLE path redacts it (runs.ts → redactSecrets, gated by persistThinking/TRACE_THINKING);
|
|
826
|
-
// the live mirror MUST match. Keep STREAMING the (redacted) thinking so live CoT still renders.
|
|
827
|
-
// 🔒 EXPLICIT WHITELIST, not `{...ev}` (TaskEvent 三腿审查跟进批, 1.72 task_progress lesson
|
|
828
|
-
// re-proven here: a spread forwards an unknown/future field RAW; this arm was the one
|
|
829
|
-
// sibling branch that still spread — the FORWARDED twin below [t === "reasoning_delta"]
|
|
830
|
-
// already used the narrow form, this now matches it byte-for-byte).
|
|
831
428
|
sseData(res, { type: "reasoning_delta", delta: redactSecrets(ev.delta), ...(ev.eventId ? { eventId: ev.eventId } : {}), ...(ev.parentToolCallId ? { parentToolCallId: ev.parentToolCallId } : {}) });
|
|
832
429
|
}
|
|
833
430
|
else if (ev.type === "task_progress") {
|
|
834
|
-
// 🔒 E1 live-wire parity: explicit whitelist via the SHARED builder (UNTRUSTED
|
|
835
|
-
// `name` redacted; 1.78: E2 identity rides — the shell binds concurrent subagent ticks by
|
|
836
|
-
// eventId/parentToolCallId) — parity with the durable runs.ts + resume legs.
|
|
837
431
|
sseData(res, { type: "task_progress", ...taskProgressEventData(ev) });
|
|
838
432
|
}
|
|
839
433
|
else if (ev.type === "compacted") {
|
|
840
|
-
// core 1.219 (attachWorkingFiles default ON): `compacted` now carries `attachedFiles` — route it
|
|
841
|
-
// through the SHARED whitelist builder (paths redactSecrets'd) instead of the raw catch-all, so
|
|
842
|
-
// the live wire matches the durable legs' shape exactly (one builder, no drift).
|
|
843
434
|
sseData(res, { type: "compacted", ...compactedEventData(ev) });
|
|
844
435
|
}
|
|
845
436
|
else if (ev.type === "diagnostics") {
|
|
846
|
-
// core 1.220 (design/121): the shell's render source for CC's diagnostics row.
|
|
847
|
-
// Shared whitelist builder (§E1: language-server output UNTRUSTED RAW → redactSecrets).
|
|
848
437
|
sseData(res, { type: "diagnostics", ...diagnosticsEventData(ev) });
|
|
849
438
|
}
|
|
850
439
|
else if (ev.type === "steering_injected") {
|
|
851
|
-
// [1550]/[1552]: core's mid-run system-reminder injection — shared whitelist builder
|
|
852
|
-
// (§E1: preview is UNTRUSTED free text → redactSecrets). Previously fell through to the
|
|
853
|
-
// raw catch-all below (unredacted `{...ev}`) — moved to an explicit builder for the same
|
|
854
|
-
// reason every other content type left the catch-all (the 1.72 task_progress lesson).
|
|
855
440
|
sseData(res, { type: "steering_injected", ...steeringInjectedEventData(ev) });
|
|
856
441
|
}
|
|
857
442
|
else if (ev.type === "workspace_changed") {
|
|
858
|
-
// [1559]二 core 裁定:bash cd/EnterWorktree 移动任务逻辑 cwd 后一帧,给 shell UI 的路径
|
|
859
|
-
// 提示行。cwd 与已展示的工具 args 同信任级 → 共享白名单构造器(redactSecrets + bound)。
|
|
860
443
|
sseData(res, { type: "workspace_changed", ...workspaceChangedEventData(ev) });
|
|
861
444
|
}
|
|
862
445
|
else if (ev.type === "compaction_outcome") {
|
|
863
|
-
// core 2.5.0([2105]②)压缩非 compacted 结局帧。[2373]C-2:内联挑键收进共享 builder,
|
|
864
|
-
// 与两条 durable 腿同源(它曾是唯一不走 builder 的内容帧=三方普查共同盲区)。
|
|
865
446
|
sseData(res, { type: "compaction_outcome", ...compactionOutcomeEventData(ev) });
|
|
866
447
|
}
|
|
867
448
|
else if (ev.type === "wiring_manifest") {
|
|
868
|
-
// 🔴 core 5.14.0 design/173 新臂。`manifest.governance` 盖着 `audience:"operator"`,core 把
|
|
869
|
-
// 失败方向写死成「no projection ⇒ do not disclose」——所以默认(租户)形整段剥除,含**对该段
|
|
870
|
-
// 取的哈希** `configFingerprint`(无盐 + 4 布尔 = 16 组合谕示器)。
|
|
871
|
-
// #154 件一:分叉只在这条 live 腿(**唯一知道调用方身份的腿**),两条 durable 腿恒存剥后形
|
|
872
|
-
// ——账本写时不知道未来读者是谁,存全量 = 把 operator-only 面写进租户可读介质。
|
|
873
449
|
sseData(res, {
|
|
874
450
|
type: "wiring_manifest",
|
|
875
451
|
...(wiringOperatorFace
|
|
@@ -878,19 +454,9 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
878
454
|
});
|
|
879
455
|
}
|
|
880
456
|
else if (ev.type === "human_input") {
|
|
881
|
-
// core 5.14.0 design/171 新臂:谁把什么喂进了这条 run(本帧不带正文,只带载体/署名)。
|
|
882
|
-
// 同上——离开裸 catch-all:`issuer`/`actor.*` 是宿主派生的身份串(自由文本形)⇒ 构造器
|
|
883
|
-
// 内过 redactSecrets;`principal` 刻意不投影(帧本就走 owner-scoped 流)。
|
|
884
457
|
sseData(res, { type: "human_input", ...humanInputEventData(ev) });
|
|
885
458
|
}
|
|
886
459
|
else if (ev.type === "text_delta" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
|
|
887
|
-
// 裸 catch-all 白名单化收官(probe-wire-1 表征钉的根治半场):最后四个已知臂离开
|
|
888
|
-
// `JSON.stringify(ev)`。**零字节变化**白名单——已知键(含 E2 四 identity 键:live 现状
|
|
889
|
-
// 带全四键,消费端可能已依赖,与 durable builder 的「刻意排除」是两腿两策)逐一挑,
|
|
890
|
-
// 只挡 core 未来新键的裸穿透(structured 在 1.203 就真穿透过本腿)。text_delta 的 delta
|
|
891
|
-
// 不做内容变换:逐 token 片段上 redactSecrets 对跨 chunk 的秘密无效还破流式——durable
|
|
892
|
-
// 腿攒批后 redact 是那半的正解,两腿口径注在 ledger-sink.flush。catch-all(下方 else)
|
|
893
|
-
// 从此只剩**真未知臂**:L1 臂集门(core-keyset-guard ⑤)在 core 加臂时 tsc 红逼显式处置。
|
|
894
460
|
const e = ev;
|
|
895
461
|
const ident = {
|
|
896
462
|
...(e.eventId !== undefined ? { eventId: e.eventId } : {}),
|
|
@@ -913,83 +479,26 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
913
479
|
}
|
|
914
480
|
});
|
|
915
481
|
};
|
|
916
|
-
// E23 + §4④: wrap the synchronous stream in the per-run elicitation AND AskUserQuestion contexts (nested
|
|
917
|
-
// — each uses its own ALS). emit = a named SSE frame on THIS open connection; taskId keys each throttle
|
|
918
|
-
// (the durable run id, else a synthetic internal id); owner gates the respond (the VERIFIED principal —
|
|
919
|
-
// NOT the cost-attribution `principal`, which is the spoofable header and null on a direct door → would
|
|
920
|
-
// park owner=null and let any tenant answer); ac.signal cancels a parked ask on client disconnect. Each
|
|
921
|
-
// wrap is a no-op when its coordinator is off; the two share the identical id/owner/emit/signal.
|
|
922
482
|
const askTaskId = durableTaskId ?? uuidv7();
|
|
923
483
|
const askOwner = gatedPrincipal(req, deps.config) ?? null;
|
|
924
484
|
const emitAsk = (frame) => {
|
|
925
|
-
// #157(codex R2 复审 HIGH):流已断时这里是**静默 no-op** —— 上游 coordinator 的 try/catch
|
|
926
|
-
// 因此拿不到任何信号,一次断连就被记成"投递成功",该 ask 挂到 TTL 才按无人应答结算。当时
|
|
927
|
-
// 方向不动,但**必须留痕**:否则 `open-frame-undelivered` 只覆盖"持久化抛错",覆盖不到真正
|
|
928
|
-
// 常见的那条路。
|
|
929
|
-
// ⚠️ **question 半场已在 #173 换了方向**(a318fa1):question / question_complete 两型帧改走
|
|
930
|
-
// 下方 `emitQuestion` 的断流 THROW,于是它们**到不了**本 no-op 臂(两道守卫同步相邻、其间无
|
|
931
|
-
// await);实测 park 墙钟 8002ms → <160ms。今天真正还落在这条静默臂上的只有 elicitation 族
|
|
932
|
-
// 与其它运行流帧。别再据本段把 question 的 TTL 悬挂当作现行行为。
|
|
933
|
-
// `destroyed` 与 `writableEnded` 一起判——emitApproval 用的就是这对谓词。
|
|
934
|
-
// 分类按帧型走(`failOpenTagForDroppedFrame`):这条闭包同时驮着 open 帧、完成面包屑与
|
|
935
|
-
// 其它运行流帧,一刀切会让**一次**断连把保护型计数记两次,真 open 失败率被面包屑盖住。
|
|
936
485
|
if (res.writableEnded || res.destroyed) {
|
|
937
486
|
recordFailOpen(failOpenTagForDroppedFrame(frame.type), `frame=${frame.type}`);
|
|
938
487
|
return;
|
|
939
488
|
}
|
|
940
489
|
res.write(`event: ${frame.type}\ndata: ${JSON.stringify(frame)}\n\n`);
|
|
941
490
|
};
|
|
942
|
-
// P1 ①②: this new turn is a stream-open for the session — push any async-workflow completions that
|
|
943
|
-
// finished since (frames ride the SAME connection, before the run's own events; the shell dedups by
|
|
944
|
-
// runId). `askOwner` = the VERIFIED principal → only this caller's own completions are emitted.
|
|
945
|
-
// 🔒 THROW on a dead/ended stream instead of emitAsk's silent no-op (1.80 review: a disconnect
|
|
946
|
-
// between request-send and this drain made the two-phase ack a FALSE success — the entry was
|
|
947
|
-
// removed with no frame ever written; throwing keeps it pending for the next leg). This wrapper was
|
|
948
|
-
// once the ONLY one that threw; since #173 (a318fa1) emitQuestion, emitApproval and writeLive all
|
|
949
|
-
// throw on a dead stream too. Of the HITL family only ELICIT still rides emitAsk's no-op — but emitAsk
|
|
950
|
-
// is NOT elicit-only: SendUserFile's `file_link` (below) and other runtime frames also write straight
|
|
951
|
-
// through it, so its dead-stream drop surface is wider than the HITL lanes (file_link is covered by its
|
|
952
|
-
// own durable twin, not by a throw).
|
|
953
|
-
// 🔴 drain 案: write these as ANONYMOUS `data:` frames, NOT emitAsk's NAMED
|
|
954
|
-
// `event: <type>` form — the run's own live-leg task_notification frames on this SAME connection
|
|
955
|
-
// are anonymous (L1392/L1474), so a shell parsing the default message channel silently never saw
|
|
956
|
-
// a drained frame (the Monitor "park✓ drain✗" fingerprint). The HITL frames keep their own
|
|
957
|
-
// established NAMED-event contract — elicit still writes straight through emitAsk, question rides
|
|
958
|
-
// emitQuestion (same named-event write, one extra dead-stream guard in front, #173).
|
|
959
491
|
await emitPendingWorkflowCompletions(deps.workflowCompletionInbox, prepared.spec.sessionId, askOwner, (frame) => {
|
|
960
492
|
if (closed || res.writableEnded || res.destroyed)
|
|
961
493
|
throw new Error("stream ended before the completion frame was written");
|
|
962
|
-
// core [784]④b: register the drained frame's key on THIS stream — the same idle-arrival was
|
|
963
|
-
// ALSO parked core-side (pendingSessionNotifications), and its pend-drain replay lands on this
|
|
964
|
-
// very stream as a task_notification event; the loop's key check folds that second wire copy.
|
|
965
|
-
// codex F1: resolved(true) — the res.write below is synchronous (the dead-stream guard above
|
|
966
|
-
// already threw). codex F2: task_type (restored from the entry extras) rides into the key —
|
|
967
|
-
// an external notify's caller-chosen task_id must not pre-claim an internal task's key.
|
|
968
494
|
const f = frame;
|
|
969
495
|
if (f.type === "task_notification" && f.task_id)
|
|
970
496
|
syncNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), Promise.resolve(true));
|
|
971
497
|
sseData(res, frame);
|
|
972
|
-
},
|
|
973
|
-
// emit-target diagnostics — this drain rides the caller's OWN live SSE response.
|
|
974
|
-
{ route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
|
|
498
|
+
}, { route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
|
|
975
499
|
const withElicit = deps.elicitation
|
|
976
500
|
? () => deps.elicitation.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, streamBody)
|
|
977
501
|
: streamBody;
|
|
978
|
-
// #152 复审 A1:`deliverable` 是 durable 部署上 park-vs-live 判决的投递面谓词(见 question.ts 该键注)。
|
|
979
|
-
// 本腿的投递面就是这条 SSE 连接,而 ①b detach 车道下客户端断连**不 abort、不结束这条腿**——run 继续在
|
|
980
|
-
// 本 runWithContext 作用域里跑。谓词让判决面此时如实读作「无活流」⇒ durable park(运维可经 /decide
|
|
981
|
-
// 补答),而不是 allow 后挂满 ttl。
|
|
982
|
-
// #173([2937] test 墙钟反证定谳):question 开帧的 emit 不再共享 emitAsk 的静默 no-op——死流上
|
|
983
|
-
// no-op = 开帧「假投递成功」,coordinator 等满 TTL 才以 unavailable 结算(实测 park 墙钟 ≈TTL,
|
|
984
|
-
// 断连→ask 间隔拉到 15s 也一样:判决在 gate 时刻,而 gate 早已判过的窗口里 ask 只剩 emit 一道门)。
|
|
985
|
-
// 收紧与 emitApproval 同形:断流 THROW ⇒ #166 的 delivery==="failed" 臂**立即** settle
|
|
986
|
-
// unavailable,不再等满 TTL。
|
|
987
|
-
// ⚠️ 收紧买到的是**结算速度**,不是可恢复性(2026-08-07 亲读 core 5.16 定):本闭包只在工具已
|
|
988
|
-
// 执行时被调用,即 `createDurableQuestionGate` 已判 allow、core 已过 `suspendAsk` 挂起点 ⇒ 这一
|
|
989
|
-
// 形落 `declined_unavailable` 自答续跑卡(赎回既有审批的腿返 isError),**不铸 checkpoint**。
|
|
990
|
-
// 真正 park 的是判决时投递面就不可达的腿(`deliverable`/`hasLiveContext` 读 false ⇒ 门判 ask),
|
|
991
|
-
// 它们根本走不到这里。别把两形写成一句。
|
|
992
|
-
// elicitation 保持 no-op(其 decline 语义 fail-closed,#166 车登记另议)。
|
|
993
502
|
const emitQuestion = (frame) => {
|
|
994
503
|
if (res.writableEnded || res.destroyed)
|
|
995
504
|
throw new Error("live stream ended — question frame undeliverable");
|
|
@@ -998,27 +507,11 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
998
507
|
const withQuestion = () => deps.question
|
|
999
508
|
? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitQuestion, abortSignal: ac.signal, deliverable: () => !res.writableEnded && !res.destroyed }, withElicit)
|
|
1000
509
|
: withElicit();
|
|
1001
|
-
// [816]/[820]②: the live tool-approval context — SAME id/owner/emit/signal as question/elicit
|
|
1002
|
-
// (nested ALS, each its own store). sessionId keys the "allow all edits this session" state.
|
|
1003
|
-
// [1535] 正名:本 sync 腿仍是唯一 runWithContext 包裹,但不再是子代唯一到达路——下方
|
|
1004
|
-
// spec.onAsk 的 bound 闭包经 core 继承链给了 bg/嵌套子代直达宿主流的路;真正无宿主流的
|
|
1005
|
-
// 残余腿(durable-submit/headless resume)才落 "unavailable" → durable park / core deny。
|
|
1006
|
-
// 🔒 approval 帧不用共享 emitAsk 的静默 no-op 语义:res 已结束时 no-op = 卡「假投递成功」,
|
|
1007
|
-
// coordinator 等满 TTL 落 deny —— G1 "unavailable"→park 腿在真 wiring 里就不可达(cross-review
|
|
1008
|
-
// 抓获)。这里 THROW,coordinator 的 emit-catch 按「无活人可达」返 "unavailable"(durable 部署
|
|
1009
|
-
// park;非 durable 由 core fail-closed deny,行为不劣于旧 no-op)。
|
|
1010
|
-
// 当年本注写的「仅 approval 帧收紧」已被 #173 推进一格:question 帧此后与本臂**同形**收紧(见上面
|
|
1011
|
-
// `emitQuestion`)。**HITL 三族里**仍留在共享 emitAsk 静默 no-op 上的只有 elicitation(其 decline
|
|
1012
|
-
// 结算本身就是 fail-closed,收紧收益小,另议);emitAsk 本身还驮着 SendUserFile 的 `file_link` 等
|
|
1013
|
-
// 非 HITL 帧,那些不在本条收紧的辖域内(file_link 的补偿是它自己的 durable 双写,不是 throw)。
|
|
1014
510
|
const emitApproval = (frame) => {
|
|
1015
511
|
if (res.writableEnded || res.destroyed)
|
|
1016
512
|
throw new Error("live stream ended — approval card undeliverable");
|
|
1017
513
|
emitAsk(frame);
|
|
1018
514
|
};
|
|
1019
|
-
// ── #151 车3 刀 3b:sync 腿的流内审批协议装配 ────────────────────────────────────────────
|
|
1020
|
-
// 上场判据走**单一谓词**(与协调器注入 / `/v1/capabilities` / 回决端点 501 同一个符号)。
|
|
1021
|
-
// 关着 ⇒ 下面三样(呈卡口 / 腿轴真值 / windowZero 分支)全部不接,本腿字节逐字不变(A-1)。
|
|
1022
515
|
const streamApprovalOn = resolveStreamApprovalGate({
|
|
1023
516
|
toolApprovalEnabled: Boolean(deps.toolApproval),
|
|
1024
517
|
streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
|
|
@@ -1026,10 +519,6 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1026
519
|
parkFacility: deps.checkpointStore !== undefined,
|
|
1027
520
|
}).active;
|
|
1028
521
|
const legWalltimeMs = typeof prepared.spec.limits?.maxWalltimeMs === "number" ? prepared.spec.limits.maxWalltimeMs : undefined;
|
|
1029
|
-
// §7.3 + §8.3:本腿的「上不上场 / 窗是不是 0 / leg deadline 真值」走**三腿共用**的那一份判据
|
|
1030
|
-
// (`resolveApprovalLeg` 顶注记着为什么必须是一个函数:第一版只写在本腿上,bg/resume 两腿因此
|
|
1031
|
-
// 在开关关时行为变了、窗=0 也不生效)。取值点 = **本腿开始执行的时刻**(这里就是 `streamBody`
|
|
1032
|
-
// 进入前的最后一站),误差方向偏短=安全侧。
|
|
1033
522
|
const approvalLeg = resolveApprovalLeg({
|
|
1034
523
|
streamApprovalOn,
|
|
1035
524
|
windowMs: deps.config.streamApproval?.windowMs ?? 0,
|
|
@@ -1038,17 +527,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1038
527
|
nowMonotonicMs: performance.now(),
|
|
1039
528
|
});
|
|
1040
529
|
const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
|
|
1041
|
-
// 🔴 ①`forceDurableGate` principal 这一源**本装配点读不到**(`runtimeCapsResolver` 是 core 在
|
|
1042
|
-
// prepare 内部按 principal 异步解析的 deps 级 seam)。按 §14 §12-5 亲裁:不可读则该源不做 ——
|
|
1043
|
-
// core 自己兜得住(`prepare-task.js:3330` 的 `runtimeCaps?.forceDurableGate !== true` 让
|
|
1044
|
-
// suspendAsk 不退位 ⇒ 结构性 park)。代价只是这类 principal 可能先收到一张必被 park 的卡,
|
|
1045
|
-
// 已写进 wire 契约文档。
|
|
1046
530
|
const windowZero = approvalLeg.windowZero;
|
|
1047
|
-
// §4.3(a)+(c):呈卡帧的投递口 —— live SSE **与** detach 车道的 durable 账本**双写**。
|
|
1048
|
-
// 🔴 codex round2 R2-1 修:两个 sink 的成败判定收进**唯一属主** `createApprovalCardEmitter`
|
|
1049
|
-
// (顶注写着为什么「两路都没接下就必须抛」——静默成功会压住 park 路由,把一只没人能回答的 ask
|
|
1050
|
-
// 挂到窗到期)。本处只负责把两个 sink 交出去;`writeLive` 在流已结束时**缺席**(不是 no-op),
|
|
1051
|
-
// 于是「socket 已死 ∧ 账本写失败」如实成为一次投递失败。
|
|
1052
531
|
const emitCard = createApprovalCardEmitter({
|
|
1053
532
|
...(ledgerSink
|
|
1054
533
|
? {
|
|
@@ -1064,20 +543,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1064
543
|
emitAsk(frame);
|
|
1065
544
|
},
|
|
1066
545
|
});
|
|
1067
|
-
// 车6 撤卡帧:**live only**(不进 durable tail —— 收敛器/reaper 那类调用点没有 seq 可分配,
|
|
1068
|
-
// 硬塞进账本会铸一条序号伪造的行;丢帧的结构补偿恒是重连 preamble 的全量对账基准)。
|
|
1069
546
|
const emitRevokeLive = (frame) => {
|
|
1070
547
|
if (!res.writableEnded && !res.destroyed)
|
|
1071
548
|
emitAsk(frame);
|
|
1072
549
|
};
|
|
1073
|
-
// [1535] server 半场([1539] 定谳断点①的修):同一 ctx 双装——ALS 包裹(宿主自身 ask 的
|
|
1074
|
-
// 既有路)+ **spec.onAsk = boundAsk(ctx)**(core 1.294 继承链 parentConstraints 冻结
|
|
1075
|
-
// `spec.onAsk ?? deps.onAsk` 逐 ancestor 传给每个委派子代:bg 子代 lane 无 ALS ctx 时不再
|
|
1076
|
-
// 「unavailable/零帧 fail-closed deny」,权限卡带 sourceTaskId 浮到宿主本流)。suspendAsk
|
|
1077
|
-
// 退位条件的「onAsk 在场」真值不变(deps.onAsk 恒 wire),durable 部署行为零漂移。
|
|
1078
|
-
// #151 车3 刀 3b:`windowZero` ⇒ **不包 ALS**(ALS 腿一并让位,`withApproval` 退化成
|
|
1079
|
-
// `withQuestion`),且下面的 `spec.onAsk` 换成 immediate-unavailable 闭包 —— 两件必须同时做,
|
|
1080
|
-
// 只做一件的话另一条路仍会呈卡(ALS 腿是 `deps.onAsk` 的到达路)。
|
|
1081
550
|
const approvalCtx = deps.toolApproval && !windowZero
|
|
1082
551
|
? {
|
|
1083
552
|
taskId: askTaskId,
|
|
@@ -1085,13 +554,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1085
554
|
emit: emitApproval,
|
|
1086
555
|
abortSignal: ac.signal,
|
|
1087
556
|
...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}),
|
|
1088
|
-
// 协议上场才接新协议族的两个口与两条腿轴;关着时逐字保持既有四键形。
|
|
1089
557
|
...(streamApprovalOn
|
|
1090
558
|
? {
|
|
1091
559
|
emitCard,
|
|
1092
560
|
emitRevoke: emitRevokeLive,
|
|
1093
|
-
// sync 腿恒是**首腿**:它不经 checkpoint token 复活(那条是 resume 腿的形),
|
|
1094
|
-
// 空串是首腿的**真值**而不是「未知」(见 ToolApprovalRunContext.legKey 顶注)。
|
|
1095
561
|
legKey: "",
|
|
1096
562
|
...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
|
|
1097
563
|
}
|
|
@@ -1099,20 +565,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1099
565
|
}
|
|
1100
566
|
: undefined;
|
|
1101
567
|
if (deps.toolApproval) {
|
|
1102
|
-
// 🔴 具名一次再进闭包(自审:`deps.toolApproval!` 的非空断言在这里是**可避免**的 —— 断言只
|
|
1103
|
-
// 在本 if 成立时正确,而闭包是稍后才跑的;拿捕获到的常量才是真的把那条不变量携进去)。
|
|
1104
568
|
const toolApproval = deps.toolApproval;
|
|
1105
|
-
// [1546] HIGH-1:闭包只携身份——emit 经 broker 取「当前」活跃流(宿主重连的新 SSE 在
|
|
1106
|
-
// runWithContext 注册时覆盖指针),internalsSnapshot 冻结的闭包不再携死 response/死 signal。
|
|
1107
|
-
// #151 车3 刀 3b(§8.3 ⑦):`windowZero` ⇒ 换成**立即** `"unavailable"` 的闭包。为什么不是
|
|
1108
|
-
// 「不装 onAsk」:`deps.onAsk` 恒 wire(boot/runner-deps.ts)且 core 取 `spec.onAsk ?? deps.onAsk`,
|
|
1109
|
-
// per-task 的「缺席」结构上不可达;而 core 的 `approverUnavailable === true` 分支
|
|
1110
|
-
// (prepare-task.js:3328-3335)**直接绕过** onAsk 在场判定去铸 checkpoint —— 与「onAsk 缺席」
|
|
1111
|
-
// 在 park 结局上逐字等价,且可 per-task 表达。
|
|
1112
|
-
// #280 R-13 C 臂(d):这条短路的取值走协调器公开的**同一个**政策口
|
|
1113
|
-
// (`unattendedAskOutcome()`)—— `UNATTENDED_APPROVAL_POLICY=deny` 的部署上它是裸 `false`。
|
|
1114
|
-
// 🔴 不在这里读 `deps.config` 自己算一遍:五臂一处施加是本件的硬条款,两处各算必然漂
|
|
1115
|
-
// (协调器与装配层对同一只 ask 给出不同终局)。
|
|
1116
569
|
prepared.spec.onAsk = windowZero
|
|
1117
570
|
? () => Promise.resolve(toolApproval.unattendedAskOutcome())
|
|
1118
571
|
: toolApproval.boundAsk({
|
|
@@ -1123,10 +576,6 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1123
576
|
});
|
|
1124
577
|
}
|
|
1125
578
|
const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
|
|
1126
|
-
// SendUserFile:sync 腿 file_link 走 live out-of-band 帧面(emitAsk)。detach 车道([1840]§五
|
|
1127
|
-
// file_link 修):**同落账**——断连后 live 面写在已死 socket 上被丢,壳换道 events tail 必须
|
|
1128
|
-
// 能拿到文件卡(bg 腿本就 durable append,双腿对齐);live+durable 双写与 text/tool 帧的换道
|
|
1129
|
-
// 语义同栏(壳按换道协议处理重放)。非 detach 车道 ledgerSink 恒 undefined = live-only 字节不变。
|
|
1130
579
|
await (deps.sendUserFile
|
|
1131
580
|
? deps.sendUserFile.runWithContext({
|
|
1132
581
|
taskId: askTaskId,
|
|
@@ -1134,50 +583,33 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1134
583
|
void (await emitAsk(f));
|
|
1135
584
|
if (ledgerSink) {
|
|
1136
585
|
const { type, ...rest } = f;
|
|
1137
|
-
await ledgerSink.append(type, rest).catch(() => undefined);
|
|
586
|
+
await ledgerSink.append(type, rest).catch(() => undefined);
|
|
1138
587
|
}
|
|
1139
588
|
},
|
|
1140
589
|
}, withApproval)
|
|
1141
590
|
: withApproval());
|
|
1142
|
-
// Drive the parked run row to its terminal/suspended state (releases or keeps task_active).
|
|
1143
591
|
if (durableTaskId && deps.runStore) {
|
|
1144
|
-
// R8 decouple defense-in-depth (review LOW): only KEEP the lock (setSuspended/needs_review) when
|
|
1145
|
-
// checkpointStore exists — without it there's no resume ctx, so a suspend (unreachable per durableEnabled,
|
|
1146
|
-
// but fail-safe matters) FALLS THROUGH to setTerminal which RELEASES the claim. Mirrors the sync path.
|
|
1147
592
|
if (finalResult?.status === "suspended" && deps.checkpointStore)
|
|
1148
593
|
await deps.runStore.setSuspended(durableTaskId);
|
|
1149
594
|
else if (finalResult?.status === "needs_review" && deps.checkpointStore)
|
|
1150
|
-
await deps.runStore.setNeedsReview(durableTaskId);
|
|
595
|
+
await deps.runStore.setNeedsReview(durableTaskId);
|
|
1151
596
|
else if (finalResult)
|
|
1152
597
|
await deps.runStore.setTerminal(durableTaskId, finalResult.status, finalResult, finalResult.errorMessage ?? null);
|
|
1153
598
|
else {
|
|
1154
|
-
// No done event observed — the client disconnected (closed → break) and ac.abort() killed the
|
|
1155
|
-
// run. Race guard: the task may have SUSPENDED right before the abort (checkpoint already
|
|
1156
|
-
// persisted, its done event unread) — a pending checkpoint outlives the abort, so park the row
|
|
1157
|
-
// suspended (session stays locked for the operator's /decide). Otherwise record an honest
|
|
1158
|
-
// failure: never "completed" for a run whose terminal state was never seen.
|
|
1159
|
-
// R8: anchor the rewind handle even on disconnect — if the user-message entryId was committed
|
|
1160
|
-
// before the Ctrl+C, the shell (holding the early run_started taskId) can still rewind to this turn.
|
|
1161
599
|
putRewindAnchor();
|
|
1162
600
|
const pending = await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId);
|
|
1163
601
|
if (pending) {
|
|
1164
|
-
// [1.211 codex M1] detach 账本补 park 终局帧(done 未读到但 checkpoint 已在——账本没有
|
|
1165
|
-
// suspended 帧则 events tail 无终局,壳等空;gate 内容未读到 done 无从知,落 gate:null)。
|
|
1166
602
|
if (ledgerSink && !ledgerTerminal) {
|
|
1167
603
|
ledgerTerminal = true;
|
|
1168
604
|
await ledgerSink.flush().catch(() => undefined);
|
|
1169
605
|
await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
|
|
1170
606
|
}
|
|
1171
607
|
await deps.runStore.setSuspended(durableTaskId);
|
|
1172
|
-
settleFleet("suspended");
|
|
608
|
+
settleFleet("suspended");
|
|
1173
609
|
}
|
|
1174
610
|
else {
|
|
1175
|
-
// codex L1 对齐:同一类用户中止(disconnect abort)不再因 iterator 正常 return / throw 而给
|
|
1176
|
-
// 两种终局形——与 throw 路兜底、resume-abort settle 同为 errorCode "cancelled" 的取消终局。
|
|
1177
611
|
const dcErr = "stream client disconnected before completion (run aborted)";
|
|
1178
612
|
const dc = { taskId: durableTaskId, sessionId: prepared.spec.sessionId ?? "", status: "failed", errorCode: "cancelled", errorMessage: dcErr, stats: { turns: 0, tokens: 0 } };
|
|
1179
|
-
// [900]① detach 车道:账本也要有终局帧(bg 腿 cancelled 终局同形;detach 下本分支仅在
|
|
1180
|
-
// generator 无 done 早收/cancel-verb 干净收尾时可达)。best-effort:落账失败不改行既有 settle。
|
|
1181
613
|
if (ledgerSink && !ledgerTerminal) {
|
|
1182
614
|
ledgerTerminal = true;
|
|
1183
615
|
await ledgerSink.flush().catch(() => undefined);
|
|
@@ -1187,67 +619,47 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1187
619
|
settleFleet("failed");
|
|
1188
620
|
}
|
|
1189
621
|
}
|
|
1190
|
-
runRowSettled = true;
|
|
622
|
+
runRowSettled = true;
|
|
1191
623
|
}
|
|
1192
624
|
}
|
|
1193
625
|
finally {
|
|
1194
|
-
syncLegLive = false;
|
|
1195
|
-
unregisterEngineNotice();
|
|
626
|
+
syncLegLive = false;
|
|
627
|
+
unregisterEngineNotice();
|
|
1196
628
|
if (durableHeartbeat)
|
|
1197
629
|
clearInterval(durableHeartbeat);
|
|
1198
|
-
// 1.207: drop the live verb handle — IDENTITY-GUARDED (review LOW): a suspend→fast-resume can
|
|
1199
|
-
// re-register the SAME taskId from the resume leg before this finally runs; an unconditional
|
|
1200
|
-
// delete would clobber the fresh handle (verbs would 409 "another replica" until re-registration).
|
|
1201
630
|
if (durableTaskId && steerableRuns.get(durableTaskId) === liveStreamRef)
|
|
1202
631
|
steerableRuns.delete(durableTaskId);
|
|
1203
|
-
// [888]① same identity guard for the cancel handle (a fast-resume registers its own controller).
|
|
1204
632
|
if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
|
|
1205
633
|
inflightRuns.delete(durableTaskId);
|
|
1206
|
-
cancelledViaVerb.delete(durableTaskId);
|
|
634
|
+
cancelledViaVerb.delete(durableTaskId);
|
|
1207
635
|
}
|
|
1208
636
|
for (const evict of subagentHandleEvictions)
|
|
1209
|
-
evict();
|
|
1210
|
-
// [868]③: the DURABLE twin of leak C — a throw out of the stream (abort mid-iteration / plain
|
|
1211
|
-
// Error) also skipped the run-row settle block, leaving a `running` zombie that only reapStale
|
|
1212
|
-
// would flip (after runStaleSec, as a misleading "run stalled (instance lost?)"). Settle it here:
|
|
1213
|
-
// re-read the row (a concurrent path may have settled it — then this is a no-op), honour a
|
|
1214
|
-
// suspend that raced the abort (pending checkpoint ⇒ park, the disconnect branch's exact guard),
|
|
1215
|
-
// else fail it — errorCode "cancelled" when the leg was aborted (user kill / disconnect), bare
|
|
1216
|
-
// failed for a genuine mid-stream error. Best-effort: the backstop must never mask the real throw.
|
|
1217
|
-
// 🔴 runs BEFORE the leak-C fleet backstop below (codex M3): the run-row verdict here (park vs
|
|
1218
|
-
// fail) must drive the fleet verdict — the old order settled the fleet row "failed" first and a
|
|
1219
|
-
// subsequently PARKED run row contradicted it ("waiting" run under a failed fleet row).
|
|
637
|
+
evict();
|
|
1220
638
|
if (!runRowSettled && durableTaskId && deps.runStore) {
|
|
1221
639
|
try {
|
|
1222
640
|
const row = await deps.runStore.getRun(durableTaskId);
|
|
1223
641
|
if (row?.status === "running") {
|
|
1224
642
|
const pending = prepared.spec.sessionId ? await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId) : undefined;
|
|
1225
643
|
if (pending) {
|
|
1226
|
-
// [1.211 codex M1] detach 账本补 park 终局帧(finally 兜底路同款,gate 未知落 null)。
|
|
1227
644
|
if (ledgerSink && !ledgerTerminal) {
|
|
1228
645
|
ledgerTerminal = true;
|
|
1229
646
|
await ledgerSink.flush().catch(() => undefined);
|
|
1230
647
|
await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
|
|
1231
648
|
}
|
|
1232
649
|
await deps.runStore.setSuspended(durableTaskId);
|
|
1233
|
-
settleFleet("suspended");
|
|
650
|
+
settleFleet("suspended");
|
|
1234
651
|
}
|
|
1235
652
|
else if (finalResult) {
|
|
1236
|
-
// [1.211 codex H1] done 已观测(账本/fleet 可能已按它落)但行仍 running = 首次 setTerminal
|
|
1237
|
-
// 抖动——兜底必须重试「原终局」,绝不写 failed(否则账本 done+fleet completed+行 failed
|
|
1238
|
-
// 三面矛盾;bg 腿 `reached` 重试同款纪律)。账本侧若 onDone 也没落成,这里补一发。
|
|
1239
653
|
if (ledgerSink && !ledgerTerminal) {
|
|
1240
654
|
ledgerTerminal = true;
|
|
1241
655
|
await ledgerSink.onDone(finalResult).catch(() => undefined);
|
|
1242
656
|
}
|
|
1243
657
|
await deps.runStore.setTerminal(durableTaskId, finalResult.status, finalResult, finalResult.errorMessage ?? null);
|
|
1244
|
-
settleFleet(finalResult.status, fleetRunResiduals(finalResult));
|
|
658
|
+
settleFleet(finalResult.status, fleetRunResiduals(finalResult));
|
|
1245
659
|
}
|
|
1246
660
|
else if (ac.signal.aborted) {
|
|
1247
661
|
const err = "stream aborted before completion (run cancelled)";
|
|
1248
662
|
const c = { taskId: durableTaskId, sessionId: prepared.spec.sessionId ?? "", status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
|
|
1249
|
-
// [900]① detach 车道:throw 路兜底同样补账本终局帧(bg 腿 catch 路 append("failed") 同形;
|
|
1250
|
-
// ledgerTerminal 防对已落 done/park 的账本覆写 failed —— bg 腿 `reached` 同款纪律)。
|
|
1251
663
|
if (ledgerSink && !ledgerTerminal) {
|
|
1252
664
|
ledgerTerminal = true;
|
|
1253
665
|
await ledgerSink.flush().catch(() => undefined);
|
|
@@ -1267,28 +679,17 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1267
679
|
}
|
|
1268
680
|
}
|
|
1269
681
|
catch {
|
|
1270
|
-
/* best-effort — reapStale remains the last-resort net; the original throw stays the story */
|
|
1271
682
|
}
|
|
1272
683
|
}
|
|
1273
|
-
// C: a plain Error thrown mid-stream skipped every onTerminal branch above — settle the leaked
|
|
1274
|
-
// "running" row to "failed" before the throw propagates (no-op when any branch — incl. the [868]③
|
|
1275
|
-
// park above — already settled; settleFleet is once-guarded).
|
|
1276
684
|
settleFleet("failed");
|
|
1277
685
|
}
|
|
1278
686
|
return { status: 200, body: finalResult };
|
|
1279
687
|
};
|
|
1280
|
-
// B3 复审(codex HIGH):无 runStore 的部署 durableTaskId 恒缺 → 这条活流从不进 steerableRuns,
|
|
1281
|
-
// inflight() 全程读 0,SIGHUP 空闲窗(以及 SIGTERM drain)会把它当空闲斩掉。整腿显式计数,
|
|
1282
|
-
// 与 steerableRuns 的短暂双计无害(两者同在终局递减,>0 语义只看非零)。
|
|
1283
688
|
counters.uncountedBillableInflight++;
|
|
1284
689
|
try {
|
|
1285
|
-
resp = await idemCache.run(idemKey, runStreamSubmitLeg,
|
|
1286
|
-
// Cache only a clean completion; a failed/aborted stream is evicted so a retry re-runs.
|
|
1287
|
-
(r) => r.body?.status === "completed");
|
|
690
|
+
resp = await idemCache.run(idemKey, runStreamSubmitLeg, (r) => r.body?.status === "completed");
|
|
1288
691
|
}
|
|
1289
692
|
catch (e) {
|
|
1290
|
-
// Log unconditionally (don't silently drop a runner error that races with the client close — council);
|
|
1291
|
-
// re-throw only while the client is still connected so the outer handler can surface it.
|
|
1292
693
|
deps.logger?.error("stream_error", { closed, err: e instanceof Error ? e.message : String(e) });
|
|
1293
694
|
if (!closed)
|
|
1294
695
|
throw e;
|
|
@@ -1296,74 +697,43 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1296
697
|
finally {
|
|
1297
698
|
counters.uncountedBillableInflight--;
|
|
1298
699
|
clearInterval(hb);
|
|
1299
|
-
// E8 leak fence(双腿对齐的对偶):detach 腿注册过的归因键在 leg 收尾必 clear——终局 attach 已
|
|
1300
|
-
// drain 过时这是幂等 no-op;errored/aborted leg 则丢掉未 drain 的累积(bg 腿 finally 同款)。
|
|
1301
700
|
if (usageKey)
|
|
1302
701
|
deps.modelUsage?.clear(usageKey);
|
|
1303
702
|
}
|
|
1304
|
-
// A concurrent caller the cache deduplicated (ranLive=false) received no live events — hand it the terminal.
|
|
1305
703
|
if (!ranLive && resp && !res.writableEnded) {
|
|
1306
704
|
sseData(res, { type: "done", result: resp.body, replay: true });
|
|
1307
705
|
}
|
|
1308
706
|
res.end();
|
|
1309
707
|
}
|
|
1310
708
|
else {
|
|
1311
|
-
// Idempotency-Key dedup (center blocker: exactly-once billing) — concurrent + retried same-key submits
|
|
1312
|
-
// share one run; the whole run+record is wrapped so metrics/cost are counted exactly once on a replay.
|
|
1313
|
-
// ── B3 ── 同上:原文此处是同步腿的 **128 行匿名 async**,提为具名闭包。
|
|
1314
|
-
// [3979]:带 verify/cascade 的 200 体附事件通道指路;plain 形缺席不铸键(负控钉在 audit-fixes-http)。
|
|
1315
|
-
// 谓词包在**全部三处** 200 return 上而不只终局形:今天 verify/cascade 腿的 park 被 core 映射成
|
|
1316
|
-
// failed(nested suspend)走终局,park 两形只可能 plain 抵达——但那是 core 的映射语义,不是本腿
|
|
1317
|
-
// 可假设的不变式;包全三处让「core 让组合腿真 park」的未来变更不产生一个漏 notice 的形。
|
|
1318
709
|
const withDiscoveryNotice = (body) => prepared.verify || prepared.cascade ? { ...body, notice: MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE } : body;
|
|
1319
710
|
const runSyncSubmitLeg = async () => {
|
|
1320
|
-
// Durable F4/ask on the sync path — same two things as the stream path (audit functional gap):
|
|
1321
|
-
// 1. createRun a task_active claim at SUBMIT (+ heartbeat) so reapCtx's task_active guard protects
|
|
1322
|
-
// the ctx for the WHOLE first leg (its updated_at is stamped at submit and never refreshed; a
|
|
1323
|
-
// multi-minute leg would otherwise age it past REAP_RUN_STALE_SEC and get it reaped MID-RUN).
|
|
1324
|
-
// setSuspended on suspend keeps the claim (session locked submit→resume); setTerminal releases it.
|
|
1325
|
-
// 2. putCtx AFTER the claim → a suspend here is resumable on any replica. Claim-first is load-bearing
|
|
1326
|
-
// (parity with /v1/runs): a lost claim is a 409, never a silent second leg on a locked session —
|
|
1327
|
-
// and a rejected submit must not clobber the active run's resume ctx (putCtx is sessionId-keyed).
|
|
1328
711
|
let durableTaskId;
|
|
1329
712
|
let durableHeartbeat;
|
|
1330
|
-
// R8 (CC parity): the run-row claim + `durableTaskId` (the REWIND anchor key + the unique result.taskId
|
|
1331
|
-
// re-stamp) is gated on `runStore` ALONE; the durable-suspend resume ctx (`putCtx`) on `checkpointStore`. These
|
|
1332
|
-
// were bundled under ONE checkpointStore gate, which over-coupled REWIND to DURABLE_APPROVAL — but E18 resume-at
|
|
1333
|
-
// needs NO checkpoint (main.ts:815). Now matches the already-decoupled /v1/runs bg path: rewind works with just
|
|
1334
|
-
// runStore (DB_BACKEND=mysql|pg|local), no approval. Claim-first preserved (a lost claim → 409, never a silent 2nd leg).
|
|
1335
713
|
if (deps.runStore && prepared.spec.sessionId) {
|
|
1336
714
|
const tid = uuidv7();
|
|
1337
715
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
1338
716
|
if (created.ok)
|
|
1339
|
-
clearTurnActivity(tid);
|
|
717
|
+
clearTurnActivity(tid);
|
|
1340
718
|
if (!created.ok)
|
|
1341
|
-
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId) };
|
|
1342
|
-
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
|
|
719
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId) };
|
|
720
|
+
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
|
|
1343
721
|
durableTaskId = tid;
|
|
1344
722
|
}
|
|
1345
723
|
if (deps.checkpointStore && prepared.spec.sessionId) {
|
|
1346
|
-
// putCtx AFTER the claim (resumable). If this throws, the claim row self-heals: no heartbeat yet → reapStale flips it failed + releases.
|
|
1347
724
|
await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
|
|
1348
725
|
}
|
|
1349
|
-
// [888]① interactive live-run cancel, non-stream twin: this leg previously ran with NO abort seam at
|
|
1350
|
-
// all (bare runTask, no spec.signal) — a cancel flipped the durable flag nothing consumed. Wire a
|
|
1351
|
-
// controller through spec.signal (same seam as the stream leg's disconnect-kill) + register it in the
|
|
1352
|
-
// in-flight map so the cancel fast path lands. Children reaped by this abort are a USER stop (parity
|
|
1353
|
-
// with the stream leg's disconnect marker).
|
|
1354
726
|
const syncCancelCtrl = new AbortController();
|
|
1355
727
|
if (prepared.spec.sessionId)
|
|
1356
|
-
markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
|
|
728
|
+
markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
|
|
1357
729
|
if (durableTaskId && deps.runStore) {
|
|
1358
730
|
const rs = deps.runStore;
|
|
1359
731
|
const tid = durableTaskId;
|
|
1360
|
-
const owner = principal ?? null;
|
|
732
|
+
const owner = principal ?? null;
|
|
1361
733
|
const abortFromVerb = () => {
|
|
1362
|
-
cancelledViaVerb.add(tid);
|
|
734
|
+
cancelledViaVerb.add(tid);
|
|
1363
735
|
syncCancelCtrl.abort();
|
|
1364
736
|
};
|
|
1365
|
-
// [1.207 codex H1] heartbeat ALSO polls the durable cancel flag (bg-leg parity) — cross-replica
|
|
1366
|
-
// cancel only sets the flag; this leg previously had no consumer.
|
|
1367
737
|
durableHeartbeat = setInterval(() => {
|
|
1368
738
|
void rs.heartbeat(tid, owner).catch(() => undefined);
|
|
1369
739
|
if (!syncCancelCtrl.signal.aborted) {
|
|
@@ -1373,30 +743,17 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1373
743
|
}, HEARTBEAT_MS);
|
|
1374
744
|
durableHeartbeat.unref?.();
|
|
1375
745
|
inflightRuns.set(tid, syncCancelCtrl);
|
|
1376
|
-
// [1.207 codex H1] registration-window catch-up (flag flipped during the awaited putCtx window).
|
|
1377
746
|
void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !syncCancelCtrl.signal.aborted)
|
|
1378
747
|
abortFromVerb(); }).catch(() => undefined);
|
|
1379
748
|
}
|
|
1380
749
|
else if (durableTaskId) {
|
|
1381
750
|
inflightRuns.set(durableTaskId, syncCancelCtrl);
|
|
1382
751
|
}
|
|
1383
|
-
const specWithSignal = { ...prepared.spec, signal: syncCancelCtrl.signal, ...(durableTaskId ? { taskId: durableTaskId } : {}) };
|
|
1384
|
-
// Developer mode (1.44): verify = adversarial verify→fix gate. Cascade (1.45): cheap→strong ladder,
|
|
1385
|
-
// escalate on a failed gate (default = didn't complete). Both opt-in, mutually exclusive, multi-attempt.
|
|
752
|
+
const specWithSignal = { ...prepared.spec, signal: syncCancelCtrl.signal, ...(durableTaskId ? { taskId: durableTaskId } : {}) };
|
|
1386
753
|
let result;
|
|
1387
|
-
let cancelVerbLabel = false;
|
|
1388
|
-
|
|
1389
|
-
// echo — it does NOT register() with the ModelUsageTracker, so the register-gate makes its brain.call usage a
|
|
1390
|
-
// no-op (NOT a leak; the register-gate prevents the prior unbounded growth). modelUsage is the
|
|
1391
|
-
// async durable /v1/runs feature (cap `modelUsage` is gated on the run store + tracker); a sync caller gets
|
|
1392
|
-
// its result synchronously and reads aggregate cost from stats. (Wiring sync = register + a withModelUsage
|
|
1393
|
-
// attach pass; deferred as low-value for the synchronous path.)
|
|
1394
|
-
counters.uncountedBillableInflight++; // B-1: the sync await is real model work — drain must see it
|
|
754
|
+
let cancelVerbLabel = false;
|
|
755
|
+
counters.uncountedBillableInflight++;
|
|
1395
756
|
try {
|
|
1396
|
-
// ── B3 ── 原文此处是一处三层三元,把三条模型驱动腿挤在五行里。三腿具名化后判别顺序
|
|
1397
|
-
// (verify → cascade → plain)、短路语义、传参**逐字不变**;`runWithVerification` 仍是整对象
|
|
1398
|
-
// 直传(快审 F1:cost 顶不在重建中丢失),`runCascade` 仍从操作员梯子 + 本任务预算现算配置。
|
|
1399
|
-
// #196:三腿一律跑 `prepared.runner`(场景 hands 判别位选出的那只),不再直接摸 deps.runner。
|
|
1400
757
|
const verifyLeg = (v) => runWithVerification(prepared.runner, specWithSignal, v);
|
|
1401
758
|
const cascadeLeg = () => runCascade(prepared.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd));
|
|
1402
759
|
const plainLeg = () => prepared.runner.runTask(specWithSignal);
|
|
@@ -1407,67 +764,40 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1407
764
|
if (durableHeartbeat)
|
|
1408
765
|
clearInterval(durableHeartbeat);
|
|
1409
766
|
if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl) {
|
|
1410
|
-
inflightRuns.delete(durableTaskId);
|
|
1411
|
-
cancelVerbLabel = cancelledViaVerb.has(durableTaskId);
|
|
767
|
+
inflightRuns.delete(durableTaskId);
|
|
768
|
+
cancelVerbLabel = cancelledViaVerb.has(durableTaskId);
|
|
1412
769
|
cancelledViaVerb.delete(durableTaskId);
|
|
1413
770
|
}
|
|
1414
771
|
}
|
|
1415
|
-
// [1.207 codex H2] cancel-verb abort label (non-stream twin of the SSE done-frame projection):
|
|
1416
|
-
// core's external-signal abort yields failed WITHOUT an errorCode — label it so /cancel's outcome is
|
|
1417
|
-
// uniform across legs. Only an abort-shaped FAILED result; a completion that raced the abort stands.
|
|
1418
772
|
if (cancelVerbLabel && result.status === "failed" && !result.errorCode) {
|
|
1419
773
|
result = { ...result, errorCode: "cancelled" };
|
|
1420
774
|
}
|
|
1421
|
-
// E18: a resume-at caller error (a stale/non-message anchor target) comes back as a failed result carrying a
|
|
1422
|
-
// resume_at.* code — a caller mistake that BILLED NOTHING. Handle it BEFORE the suspend/terminal/finalize
|
|
1423
|
-
// logic below: release the submit-time claim (unlock the session) but do NOT finalizeTaskResult (it must not
|
|
1424
|
-
// count as a billed failure / plan-cache miss), and return a 4xx (caller fault) instead of a 200-with-failed
|
|
1425
|
-
// body. NOT idem-cached (the predicate below caches only success-range responses) — a fixed anchor can retry.
|
|
1426
775
|
const resumeAtStatus = resumeAtHttpStatus(result);
|
|
1427
776
|
if (resumeAtStatus) {
|
|
1428
777
|
if (durableTaskId && deps.runStore)
|
|
1429
778
|
await deps.runStore.setTerminal(durableTaskId, "failed", stripCheckpointToken(result), result.errorMessage ?? null);
|
|
1430
|
-
// #148 件4: the transient org-memory refusal carries core's retry hint — forward it as
|
|
1431
|
-
// retryAfterSec (seconds, ceil; family shape = usage.window_exhausted). ONLY on the transient
|
|
1432
|
-
// code: a wait hint on a terminal 4xx would be a directional lie.
|
|
1433
779
|
const retryAfterSec = result.errorCode === "memory.admission_required" && result.retryAfterMs !== undefined
|
|
1434
780
|
? Math.max(1, Math.ceil(result.retryAfterMs / 1000))
|
|
1435
781
|
: undefined;
|
|
1436
782
|
return { status: resumeAtStatus, body: { errorCode: result.errorCode, error: result.errorMessage ?? "resume-at failed", ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) } };
|
|
1437
783
|
}
|
|
1438
|
-
// Durable suspend on the sync path: PARK it as an async run + return a pollable taskId, NEVER the
|
|
1439
|
-
// capability token. The submitter switches to GET /v1/runs/:id. Cost is recorded
|
|
1440
|
-
// by the RESUMED run (mirrors runInBackground, which also parks-without-record on suspend) — not here,
|
|
1441
|
-
// so the pre-suspend tokens aren't double-counted on resume.
|
|
1442
784
|
if (result.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
|
|
1443
|
-
// The submit-time row — guaranteed when runStore is present (a lost claim was 409'd before running).
|
|
1444
785
|
if (durableTaskId)
|
|
1445
786
|
await deps.runStore.setSuspended(durableTaskId);
|
|
1446
787
|
return { status: 200, body: withDiscoveryNotice({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" }) };
|
|
1447
788
|
}
|
|
1448
|
-
// design/80 D-B: a review pause (plan_review / dry_run_review) on the sync path — PARK it like a suspend
|
|
1449
|
-
// (keep task_active) + return a pollable taskId. For a plan_review GATE the /v1/assistant/tasks/:id/plan_review
|
|
1450
|
-
// resume claims it; a needs_review gate (dry-run review) has no resolution endpoint today ([3683]-1).
|
|
1451
|
-
// NEVER the capability token (parity with the suspended branch above).
|
|
1452
789
|
if (result.status === "needs_review" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
|
|
1453
790
|
if (durableTaskId)
|
|
1454
791
|
await deps.runStore.setNeedsReview(durableTaskId);
|
|
1455
792
|
return { status: 200, body: withDiscoveryNotice({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" }) };
|
|
1456
793
|
}
|
|
1457
|
-
// Not suspended → release the submit-time claim so the session unlocks + the ctx becomes reapable.
|
|
1458
794
|
if (durableTaskId && deps.runStore)
|
|
1459
795
|
await deps.runStore.setTerminal(durableTaskId, result.status, stripCheckpointToken(result), result.errorMessage ?? null);
|
|
1460
|
-
// A completed OR failed result both BILLED → cache it so a retry replays the outcome (no double-bill);
|
|
1461
|
-
// a thrown error / transient conflict is evicted by the cache so a genuine retry re-runs. (The async
|
|
1462
|
-
// /v1/runs caches only its 202 receipt — a different object, queried separately — hence the asymmetry.)
|
|
1463
796
|
finalizeTaskResult(result, principal, prepared.spec.objective, prepared.spec.sessionId);
|
|
1464
|
-
// A verify/cascade NESTED suspend is mapped by core to status:"failed" while still carrying the
|
|
1465
|
-
// capability token (the genuine top-level suspend is parked above and never reaches here). Strip it so
|
|
1466
|
-
// the resume credential never goes in the response body OR the idempotency cache.
|
|
1467
797
|
return { status: 200, body: withDiscoveryNotice(stripCheckpointToken(result)) };
|
|
1468
798
|
};
|
|
1469
|
-
const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400);
|
|
1470
|
-
sendSubmitResult(res, resp);
|
|
799
|
+
const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400);
|
|
800
|
+
sendSubmitResult(res, resp);
|
|
1471
801
|
}
|
|
1472
802
|
return;
|
|
1473
803
|
}
|