@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.
Files changed (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -3,35 +3,21 @@ import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from ".
3
3
  import { redactSecrets } from "../../trace/redact.js";
4
4
  import { sendJson, sendError, sseHeaders } from "../send.js";
5
5
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
6
- const WORKFLOWS_RE = /^\/v1\/workflows(?:\/([^/]+)(\/stream|\/journal)?)?$/; // S8 self-orchestration runs: list / detail / live SSE
7
- // SVC-5 (design/97 CORE-5 #6): steer a STILL-RUNNING workflow agent (ctx.agentStream) by runId + label. POST-only,
8
- // so it never collides with the GET WORKFLOWS_RE above. The label is a non-slash segment (core mints `agent-N`
9
- // for an unlabelled agent, else the caller's opts.label) — URL-decoded by the handler.
6
+ const WORKFLOWS_RE = /^\/v1\/workflows(?:\/([^/]+)(\/stream|\/journal)?)?$/;
10
7
  const WORKFLOW_AGENT_STEER_RE = /^\/v1\/workflows\/([^/]+)\/agents\/([^/]+)\/steer$/;
11
8
  export async function handleWorkflows(req, res, url, ctx) {
12
9
  const miss = { fell: false };
13
10
  await handleWorkflowsReadBody(req, res, url, ctx, miss);
14
11
  return !miss.fell;
15
12
  }
16
- /** SVC-5 的 steer 路——`handle()` 里位置在 tasks/runs 之后(鉴权门之后),故是**独立**的域入口。 */
17
13
  export async function handleWorkflowAgentSteer(req, res, url, ctx) {
18
14
  const miss = { fell: false };
19
15
  await handleWorkflowAgentSteerBody(req, res, url, ctx, miss);
20
16
  return !miss.fell;
21
17
  }
22
- /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
23
- * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
24
18
  async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
25
19
  const { deps } = ctx;
26
20
  const { runSessionAcceptOk } = ctx.helpers;
27
- // S8 self-orchestration runs (design/97/98): GET /v1/workflows (list) · /v1/workflows/:id (detail) ·
28
- // /v1/workflows/:id/stream (live SSE). OWNER-GATED by scope (= the creating principal, prepare-task.js
29
- // scope: spec.principal). list/detail need the run store; live stream is in-process (subscribeWorkflow).
30
- // 🔐 Identity from gatedPrincipal (direct-door-secure single point — crypto-verified principal JWT when
31
- // directDoorActive, transparently `principalFrom` otherwise), NEVER the spoofable x-agent-principal header:
32
- // a direct-door worker MUST NOT trust the raw header for a per-tenant owner gate (same bypass class —
33
- // parity with the trace owner-gate, the workflow steer POST, and the elicit route). This block returns BEFORE
34
- // the service-credential gate below, so the per-tenant gate here is the sole tenant boundary for these reads.
35
21
  const wfMatch = req.method === "GET" ? WORKFLOWS_RE.exec(url) : null;
36
22
  if (wfMatch) {
37
23
  const principal = gatedPrincipal(req, deps.config);
@@ -43,29 +29,19 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
43
29
  sendError(res, 501, "capability.self_orchestration_required", "workflow runs require self-orchestration (SELF_ORCHESTRATION_ENABLED)");
44
30
  return;
45
31
  }
46
- const scope = principal ?? "default"; // matches core's run_workflow scope = spec.principal ?? "default"
32
+ const scope = principal ?? "default";
47
33
  const wfId = wfMatch[1];
48
34
  const isStream = wfMatch[2] === "/stream";
49
35
  if (!wfId) {
50
36
  const q = new URL(req.url ?? "", "http://x").searchParams;
51
37
  const statusFilter = q.get("status");
52
38
  const limit = Math.min(100, Math.max(1, Number(q.get("limit") ?? 50) || 50));
53
- // [1512]§二 交付件接线(1.247 留的 list 缺口):`?session=` 过滤臂 —— present 即按
54
- // `originatingSessionId` 严格等值筛。语义是**筛选**非 per-id 的接受拍:无字段行(pre-core-1.210)
55
- // 不属于任何会话,筛选下剔除(per-id 面的 absent 放行是访问控制防误伤,两面语义刻意不同);
56
- // 空串=无行可匹配=空列表;absent = 全量,字节不变。1.250:筛下推 store 侧(core 1.368 [1516]②
57
- // `listByScope.opts.session`,limit **前**应用)——1.249 的「页内筛」升「全量筛」,忙 scope 下
58
- // 比 newest-limit 窗口更老的会话行不再漏。直调 store(listWorkflowRuns 是纯透传,其 opts 类型
59
- // 未收录 session)。
60
39
  const sessionFilter = q.get("session");
61
40
  const sessionRuns = await deps.workflowRunStore.listByScope(scope, {
62
41
  ...(statusFilter ? { status: statusFilter } : {}),
63
42
  limit,
64
43
  ...(sessionFilter !== null ? { session: sessionFilter } : {}),
65
44
  });
66
- // K-2c LIST side (core 1.155 `WorkflowRunSummary` now carries name?/description?/currentPhase? from the
67
- // script `meta` — flows via the pin bump): redact the LLM-authored label surfaces for parity with the DETAIL
68
- // route (summarizeWorkflowDetail) + the fleet rows. The rest of the summary (status/counts/stats) rides as-is.
69
45
  const redacted = sessionRuns.map((r) => ({
70
46
  ...r,
71
47
  ...(r.name !== undefined ? { name: redactSecrets(r.name) } : {}),
@@ -75,48 +51,24 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
75
51
  sendJson(res, 200, { workflows: redacted });
76
52
  return;
77
53
  }
78
- // per-id: owner-gate via the persisted run's scope (no existence oracle for non-owners → 404). 1.146:
79
- // getWorkflowRun is scope-filtered IN CORE (returns null unless run.scope === scope) — defense-in-depth on
80
- // top of our route gate; the `run.scope !== scope` below is now redundant but kept (belt-and-suspenders).
81
- // The SSE branch streams ONLY after this gate, so the live stream is owner-gated too (core's subscribeWorkflow
82
- // does not self-filter — this gate is its tenant boundary).
83
54
  const run = await getWorkflowRun(deps.workflowRunStore, wfId, scope);
84
55
  if (!run || run.scope !== scope) {
85
56
  sendError(res, 404, "not_found.workflow", "workflow not found");
86
57
  return;
87
58
  }
88
- // [1508] 1.247 — session ACCEPT phase on the workflow per-id faces ([1493]-class sibling: these deliver
89
- // conversation-derived content — step prompts/outputs/live events — but were principal-gated only, while
90
- // the durable WorkflowRun carries `originatingSessionId` since core 1.210). Same soft phase + helper as
91
- // the run faces (present-mismatch → 404; absent → warn-once). The LIST face is NOT gated this拍:
92
- // WorkflowRunSummary lacks the field (core ask, γ-batch alignment) — its rows stay redacted summaries.
93
59
  if (!runSessionAcceptOk(req, res, { sessionId: run.originatingSessionId ?? null }, wfMatch[2] === "/journal" ? "workflow.journal" : isStream ? "workflow.stream" : "workflow.detail", "workflow not found"))
94
60
  return;
95
61
  if (wfMatch[2] === "/journal") {
96
- // [1402]/[1400]④:per-agent journal 读面(云本地同构——三后端店同契约)。owner 门已过(上方 run
97
- // scope 校验);投影=bounded+redact(result 是 LLM 产物,注入面纪律同 fleet 行),ordinal 序稳定。
98
62
  if (!deps.workflowJournalStore) {
99
63
  sendError(res, 501, "capability.workflow_store_required", "workflow journal requires the journal store (SELF_ORCHESTRATION_ENABLED + a store backend)");
100
64
  return;
101
65
  }
102
- // codex R1-H2:hard bounds——limit 默认 20 cap 50(?limit=)、?offset= 游标;SQL 店走 loadPage
103
- // (行数 LIMIT/OFFSET + 单行 64KiB LENGTH 门在库端,峰值 ≤50×64KiB);file/in-memory 店(单机形)
104
- // 回落全量 load 后同界投影(内存峰值=店内容,记档取舍)。
105
66
  const jq = new URL(req.url ?? "", "http://x").searchParams;
106
67
  const jLimit = Math.min(50, Math.max(1, Number(jq.get("limit") ?? 20) || 20));
107
68
  const jOffset = Math.max(0, Number(jq.get("offset") ?? 0) || 0);
108
69
  const MAX_ROW_BYTES = 64 * 1024;
109
70
  const firstLine = (t) => (t ? redactSecrets(t.split("\n")[0].slice(0, 300)) : undefined);
110
71
  const MAX_RESULT_CHARS = 2000;
111
- // A-002.6(披露双标,P1):>64KiB 的行诚实标 `truncated`/`resultBytes`(下方两臂),而 2000 字符~
112
- // 64KiB 之间的行此前被 `slice(0, 2000)` **静默**截断 —— 同一个响应数组里一行说得出「我被截了」、
113
- // 另一行说不出,消费者也无从区分「结果正好 2000 字」与「结果被截到 2000 字」。
114
- // 🔴 键刻意**不复用** `truncated`:兄弟臂的 `truncated:true` 语义是「整行没内容,只有尺寸存根」,
115
- // 盖到一行有内容的行上就是把兄弟臂的判别位改义(按它分支的消费者会把有内容的行当空存根)。
116
- // `resultTruncated`/`resultChars` 描述的是 `result` **这个字段**,无歧义。原始长度量的是
117
- // redact 之前的字符数(与兄弟臂 `resultBytes` 同精神,基准不同故不同名)。
118
- // ⚠️ wire 记账:SDK spec 的 `WorkflowJournalEntry` 是 `additionalProperties: false`,两枚 additive
119
- // 键需要 sema-sdk spec/openapi.yaml 同源补声明(SDK 件另走)。
120
72
  const projectResult = (callKey, r) => ({
121
73
  callKey,
122
74
  ordinal: callKeyOrdinal(callKey),
@@ -129,8 +81,6 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
129
81
  }
130
82
  : {}),
131
83
  ...(r.stats ? { tokens: r.stats.tokens, turns: r.stats.turns } : {}),
132
- // core 5.51 #210:status:"completed" 但停在人拒的唯一判别位——白名单不投它,journal 消费者
133
- // 会把「用户拒了、在等指示」读成「自然跑完」(契约明写的误读形)。additive:自然结束不挂键。
134
84
  ...(r.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}),
135
85
  });
136
86
  const js = deps.workflowJournalStore;
@@ -139,7 +89,6 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
139
89
  const page = await js.loadPage(wfId, scope, { offset: jOffset, limit: jLimit, maxResultBytes: MAX_ROW_BYTES });
140
90
  projected = page.map((row) => {
141
91
  if (row.resultJson === null) {
142
- // 超单行门的条目:诚实标 truncated,不把 64KiB+ 的 TaskResult 拉进进程
143
92
  return { callKey: row.callKey, ordinal: callKeyOrdinal(row.callKey), truncated: true, resultBytes: row.resultBytes };
144
93
  }
145
94
  try {
@@ -151,9 +100,6 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
151
100
  });
152
101
  }
153
102
  else {
154
- // fallback(file/in-memory,单机 TOC 形——多租户 durable 恒 SQL 店=loadPage 覆盖):load 全量的
155
- // 内存/事件环成本=用户读自己盘的单机取舍(ARCHITECTURE.md 记档);投影侧与 SQL 店**同一诚实
156
- // 语义**——超 64KiB 的行标 truncated/resultBytes,不做静默 2000 字投影(跨后端 API 形一致)。
157
103
  const entries = await deps.workflowJournalStore.load(wfId, scope);
158
104
  projected = entries
159
105
  .sort((a, b) => callKeyOrdinal(a.callKey) - callKeyOrdinal(b.callKey))
@@ -169,57 +115,35 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
169
115
  return;
170
116
  }
171
117
  if (isStream)
172
- await streamWorkflowRun(req, res, wfId, scope); // core 1.366 B-3: scope-doored live stream(异 scope=空流,与不存在同形)
118
+ await streamWorkflowRun(req, res, wfId, scope);
173
119
  else
174
- sendJson(res, 200, summarizeWorkflowDetail(run)); // SVC-4: structured drill-down (Phases panel + agent list + groups tree)
120
+ sendJson(res, 200, summarizeWorkflowDetail(run));
175
121
  return;
176
122
  }
177
123
  miss.fell = true;
178
124
  }
179
- /** 同上纪律(SVC-5 steer 段)。 */
180
125
  async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
181
126
  const { deps } = ctx;
182
127
  const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
183
- // SVC-5 (design/97 CORE-5 #6): POST /v1/workflows/:id/agents/:label/steer — STEER a still-running workflow
184
- // agent (`ctx.agentStream`) mid-flight. Unlike RUN steer (a whole task run), this targets ONE running agent
185
- // inside a workflow, addressed by runId + label. Delivery is replica-local (the steerable handle is an
186
- // in-memory bridge to the running TaskStream, like `steerableRuns`):
187
- // • the handle is live on THIS replica → `handle.steer(redacted)` injects + returns the marker → 200.
188
- // • not here but the workflow run is `running` (another replica) → 409 steering.not_running (cross-replica
189
- // live-steer is the same fast-follow seam as RUN steer — single-replica-per-worker today; honest, never
190
- // a silent drop). • terminal / no handle → 409 steering.not_running.
191
- // 🔴 HANDLE-VISIBILITY SEAM: core returns a WorkflowAgentHandle ONLY from `WorkflowRunContext.agentStream`,
192
- // and that host ctx structurally never enters the script runner (the LLM workflow sees the sterile
193
- // `WorkflowPrimitives`, which has `agent` but not `agentStream`). The service runs workflows ONLY via core's
194
- // `run_workflow` tool → primitives, so it never holds a handle to register. The registry/redaction/auth here
195
- // are production-real (the populated path uses them unchanged); until core hands the deployment the handle
196
- // (e.g. an onAgentStream lifecycle sink on run_workflow), the registry is genuinely empty and the route
197
- // returns steering.not_running. See workflow-agent-steer.ts. Raised to core (SVC-5).
198
128
  const wfSteerMatch = req.method === "POST" ? WORKFLOW_AGENT_STEER_RE.exec(url) : null;
199
129
  if (wfSteerMatch) {
200
130
  if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
201
- return; // mutating + steers a running model agent
131
+ return;
202
132
  if (!deps.workflowRunStore) {
203
133
  sendError(res, 501, "capability.self_orchestration_required", "workflow runs require self-orchestration (SELF_ORCHESTRATION_ENABLED)");
204
134
  return;
205
135
  }
206
- // Identity from gatedPrincipal (direct-door-secure single point), NEVER the spoofable header — `trusted`
207
- // carries operator authority (a supervisor steering any tenant's agent), so deriving it from a header would
208
- // be a privilege escalation (parity with RUN steer / preempt; HANDOFF 铁律 + core §3 inv #1).
209
136
  const principal = gatedPrincipal(req, deps.config);
210
137
  if (deps.config.requirePrincipal && principal === undefined) {
211
138
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
212
139
  return;
213
140
  }
214
- // safeDecode (not bare decodeURIComponent): a malformed %-sequence is the CALLER's error → 400, not a 500
215
- // (parity with the approvals + session-sync routes' established handling).
216
141
  const wfRunId = safeDecode(wfSteerMatch[1]);
217
142
  const label = safeDecode(wfSteerMatch[2]);
218
143
  if (wfRunId === null || label === null) {
219
144
  sendError(res, 400, "request.path_malformed", "malformed workflow path (invalid percent-encoding)");
220
145
  return;
221
146
  }
222
- // Parse + validate the body BEFORE the run lookup (a bad body is the caller's error regardless of state).
223
147
  let body;
224
148
  try {
225
149
  body = (await readJson(req));
@@ -232,43 +156,21 @@ async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
232
156
  sendError(res, 400, "request.body_shape", "body must be { content: string (non-empty) }");
233
157
  return;
234
158
  }
235
- // 入参上限(见 `STEER_IN_MAX_REQUEST_CHARS` 旁注):脱敏门是**同步**的,烧的是事件循环 ⇒
236
- // 一次超大 steer 卡住的是**整个副本**。**拒**而不是服务端先截 —— 先截会让引擎的
237
- // `[+N chars]` 披露低报,而"诚实标记不得低报"是本仓另一条钉守着的性质。
238
159
  if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
239
160
  sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
240
161
  return;
241
162
  }
242
- // trusted = explicit operator (NOT bare isOperator — its empty-list "true-for-all" would let any caller
243
- // steer any tenant's agent). A normal owner steers only their own workflow.
244
163
  const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
245
- const ownerScope = principal ?? "default"; // matches run_workflow scope = spec.principal ?? "default"
246
- // Owner-gate via the persisted run, now scope-filtered IN CORE (1.146 getWorkflowRun(store, id, scope) →
247
- // null unless run.scope === scope). For a NON-operator: a missing/other-tenant run is null → 404 (no
248
- // existence oracle). An explicit OPERATOR (supervisor) may steer ANY tenant's workflow — but core's
249
- // scope-filtered read can't fetch another tenant's run by the operator's own scope (→ null), so the operator
250
- // is NOT gated on `wfRun` existence: its authority is the operator allowlist, and the steer target's
251
- // existence+liveness is proven by the registry handle below (no handle → 409, never a wrong-tenant steer).
164
+ const ownerScope = principal ?? "default";
252
165
  const wfRun = await getWorkflowRun(deps.workflowRunStore, wfRunId, ownerScope);
253
166
  if (!trusted && !wfRun) {
254
167
  sendError(res, 404, "not_found.workflow", "workflow not found");
255
168
  return;
256
169
  }
257
- // [1508] 1.247 — same session accept-phase as the read faces (a steer ACTS on the conversation's
258
- // workflow). Gated on the fetched run only: the operator leg (wfRun null by scope-filtered read) keeps
259
- // its registry-handle-proven authority below, and runSessionAcceptOk's operator branch covers a
260
- // same-scope operator anyway.
261
170
  if (wfRun && !runSessionAcceptOk(req, res, { sessionId: wfRun.originatingSessionId ?? null }, "workflow.agent-steer", "workflow not found"))
262
171
  return;
263
172
  const sendNotRunningWf = (error) => sendError(res, 409, "steering.not_running", error);
264
- // STEER-IN REDACTION GATE (SVC-5 信任门): treat the human/leader content as UNTRUSTED — redact host leaks +
265
- // size-bound + fence — BEFORE it reaches the worker. core's `handle.steer` fences AGAIN inside its trusted
266
- // marker framing (defense in depth). Applied regardless of `trusted`: an operator's copy-pasted steer can
267
- // still leak a host path/token, and an authoritative-looking steer must not pose as system instructions.
268
173
  const redacted = redactSteerIn(body.content, label);
269
- // Same-replica live path: the steerable handle is registered here → inject now, return the marker.
270
- // core does not enforce label uniqueness within a run, so resolve() reports the count: >1 = two live agents
271
- // share this label → the target is ambiguous, refuse (409) rather than steer an arbitrary one.
272
174
  const resolution = deps.workflowAgentRegistry?.resolve(wfRunId, label);
273
175
  if (resolution && resolution.count > 1) {
274
176
  sendError(res, 409, "steering.ambiguous_label", `${resolution.count} live agents share label '${label}' in this run — steer target is ambiguous`);
@@ -283,18 +185,13 @@ async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
283
185
  }
284
186
  catch (e) {
285
187
  const code = e.code;
286
- // The agent settled between the registry lookup and steer() → core rejects steering.not_running.
287
188
  if (code === "steering.not_running") {
288
189
  sendNotRunningWf("agent just finished — no longer accepting steers");
289
190
  return;
290
191
  }
291
- throw e; // unexpected → outer handler 500
192
+ throw e;
292
193
  }
293
194
  }
294
- // No live handle here. The workflow run is either terminal, or `running` on another replica (cross-replica
295
- // live-steer = the same fast-follow seam as RUN steer; a workflow agent is in-memory steerable only on its
296
- // running replica). Either way: honest 409, never a silent drop. `wfRun` may be null for an OPERATOR steering
297
- // a cross-tenant run (core's scope-filtered read returned null) → a generic message (status unknowable here).
298
195
  sendNotRunningWf(!wfRun
299
196
  ? "workflow agent is not running on this replica (no live handle)"
300
197
  : wfRun.status === "running"
@@ -304,37 +201,14 @@ async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
304
201
  }
305
202
  miss.fell = true;
306
203
  }
307
- /** SVC-4 v1 — the STRUCTURED drill-down view of one persisted {@link WorkflowRun} (the historical detail the
308
- * Phases panel + agent list + groups tree render, parity with CC's Workflow tool). Pure projection over the
309
- * persisted run — no new source of truth, no gate input (the route already owner-gated by scope before this).
310
- * Three derived shapes:
311
- * - `phases`: each `run.phases` entry + a done/total of the agents that ran under that `phase` title (done =
312
- * terminal status). Plus an `unphased` bucket counting agents with no phase.
313
- * - `agents`: each agent flattened to `{ label, status, callKey, groupId, phase, tokens, turns, durationMs,
314
- * replayed, prompt, output }`. `prompt`/`output` are core-redacted + size-bounded; this is a HUMAN view so
315
- * they are surfaced AT REST WITHOUT a fence (a consumer that re-feeds them to an LLM must fence itself —
316
- * core's `WorkflowAgentRun` doc). `tokens`/`turns`/`costMicroUsd` come from the agent's own `stats`.
317
- * - `groups`: the nested tree rebuilt from `run.groups` (parentGroupId) — each node carries its direct agent
318
- * callKeys so a UI can hang the agent list off the tree. Empty when the script used no `ctx.workflow` nesting.
319
- * `stats` (own + nested, kept separate per core R-5) and the run-level fields pass through unchanged. */
320
204
  function summarizeWorkflowDetail(run) {
321
205
  const isDone = (s) => s === "completed" || s === "failed";
322
- // MF-W queued (core 1.150): a WorkflowAgentRun's `startedAt` is now OPTIONAL — ABSENT while an agent is QUEUED
323
- // (waiting on a concurrency slot) or if it was aborted/finalized before it ran. So duration is undefined unless
324
- // BOTH ends are present (a queued agent has no running duration — the DoR fix). Phases still always carry startedAt.
325
206
  const durationOf = (a) => a.startedAt === undefined || a.endedAt === undefined ? undefined : a.endedAt - a.startedAt;
326
- // A real persisted run always carries these arrays; default defensively so a malformed/partial stored row
327
- // projects cleanly instead of 500-ing the detail route (the view is read-only — never a gate input).
328
207
  const runPhases = run.phases ?? [];
329
208
  const runAgents = run.agents ?? [];
330
209
  const runGroups = run.groups ?? [];
331
- // [review] label-parity: an LLM-authored run_workflow script controls phase titles + agent labels + the per-agent
332
- // phase tag (core stores them raw, unbounded), so redact them on EMISSION for the same reason as name/description
333
- // (a secret the agent read could land in a label). Filter/bucket on the RAW values (below) so grouping is unchanged;
334
- // redactSecrets is deterministic → a secret-bearing title and the matching agent.phase scrub to the SAME string,
335
- // so the shell's phase grouping still lines up. (prompt/output are already core-redacted; model is catalog-sourced.)
336
210
  const phases = runPhases.map((p) => {
337
- const inPhase = runAgents.filter((a) => a.phase === p.title); // RAW compare — bucketing key, not emitted
211
+ const inPhase = runAgents.filter((a) => a.phase === p.title);
338
212
  return {
339
213
  title: redactSecrets(p.title),
340
214
  status: p.status,
@@ -345,50 +219,31 @@ function summarizeWorkflowDetail(run) {
345
219
  total: inPhase.length,
346
220
  };
347
221
  });
348
- // Agents with no phase title, OR a phase title that matches no registered `run.phases` entry (a stray
349
- // `opts.phase` with no enclosing `phase()` block) — surfaced so the panel's counts are EXHAUSTIVE (an agent is
350
- // in exactly one bucket: its registered phase, or `unphased`). Without the `titleSet` guard a stray-phase agent
351
- // would fall into NO bucket (under-count). Empty-string phase is also unphased.
352
222
  const phaseTitleSet = new Set(runPhases.map((p) => p.title));
353
223
  const unphased = runAgents.filter((a) => a.phase === undefined || a.phase === "" || !phaseTitleSet.has(a.phase));
354
224
  const unphasedView = unphased.length > 0
355
225
  ? { done: unphased.filter((a) => isDone(a.status)).length, total: unphased.length }
356
226
  : undefined;
357
227
  const agents = runAgents.map((a) => ({
358
- label: redactSecrets(a.label), // [review] label-parity (LLM-authored, see phases comment)
228
+ label: redactSecrets(a.label),
359
229
  status: a.status,
360
- // MF-W (1.149 CORE-shared): the canonical display status (running|queued|done|failed|interrupted) via core's
361
- // deriveAgentDisplayStatus — the SAME pure fn the shell renders, so the agent-state glyph can't drift between
362
- // shells (one source of truth in core). `interrupted` = a non-terminal agent under a terminal run. The live
363
- // `idle` overlay (a running agent with no recent activity) stays a SHELL wall-clock concern — since core 1.295
364
- // ToolActivity carries `at`, this view exposes the raw timestamps (activity[].at + lastActivityAt below) and
365
- // the shell derives idleness against its own clock (a served idleMs would drift on a historical view).
366
230
  displayStatus: deriveAgentDisplayStatus(a, run.status),
367
231
  taskStatus: a.taskStatus,
368
232
  callKey: a.callKey,
369
233
  groupId: a.groupId,
370
- phase: a.phase !== undefined ? redactSecrets(a.phase) : undefined, // [review] label-parity; undefined = unphased (kept)
371
- model: a.model, // SVC-4 v2 / CORE-8 ①: per-agent model display label ("Opus 4.8 (1M context)" row)
234
+ phase: a.phase !== undefined ? redactSecrets(a.phase) : undefined,
235
+ model: a.model,
372
236
  tokens: a.stats?.tokens,
373
237
  turns: a.stats?.turns,
374
- toolCalls: a.toolCalls, // SVC-4 v2 / CORE-8 ②: total tool calls ("M" in "last N of M tool calls")
375
- // SVC-4 v2 / CORE-8 ③: the LAST-N tool-call beats (structural — phase/toolName/ids, no args/output). The live
376
- // detail SSE (subscribeWorkflow → streamWorkflowRun) already forwards the per-callKey `agent_activity` events
377
- // verbatim for a running drill-down; this is the persisted tail a historical view renders.
378
- // [843]④a (core 1.295): each beat now carries `at` (epoch ms, stamped on BOTH start and end) — verbatim
379
- // passthrough here, so idle derives shell-side as the FRAME GAP (end.at → next start.at), the [879] ruling.
238
+ toolCalls: a.toolCalls,
380
239
  activity: a.activity,
381
- // [843]④a convenience: the tail beat's timestamp, so a shell can grey a stale agent without walking the
382
- // array. TIMESTAMP not a duration on purpose — this detail is a HISTORICAL view, so a served `idleMs`
383
- // (Date.now()-derived) would drift on every re-read; the shell owns the clock. Absent for pre-1.295 runs
384
- // whose beats carry no `at` (undefined → key omitted by JSON, same back-compat shape as agentFailures).
385
240
  lastActivityAt: a.activity && a.activity.length > 0 ? a.activity[a.activity.length - 1].at : undefined,
386
241
  durationMs: durationOf(a),
387
- queuedAt: a.queuedAt, // MF-W queued (1.150): when the agent was ENQUEUED (always set) — a queued agent has queuedAt but no startedAt
388
- startedAt: a.startedAt, // now optional (absent while queued / aborted-before-run) — displayStatus already reflects "queued"
242
+ queuedAt: a.queuedAt,
243
+ startedAt: a.startedAt,
389
244
  endedAt: a.endedAt,
390
245
  replayed: a.replayed,
391
- prompt: a.prompt, // core-redacted + bounded; human view → no fence at rest
246
+ prompt: a.prompt,
392
247
  output: a.output,
393
248
  }));
394
249
  const byId = new Map();
@@ -404,9 +259,6 @@ function summarizeWorkflowDetail(run) {
404
259
  children: [],
405
260
  });
406
261
  }
407
- // Walk up `parentGroupId` from `node`; true if the chain LOOPS (self-parent, or a multi-node cycle A→B→A)
408
- // rather than terminating at a top-level (`undefined`) or dangling (absent) parent. Bounded by the seen-set, so
409
- // a malformed run can neither hang nor build a circular object that `JSON.stringify` would throw 500 on.
410
262
  const inCycle = (start) => {
411
263
  const seen = new Set();
412
264
  let cur = start;
@@ -421,27 +273,18 @@ function summarizeWorkflowDetail(run) {
421
273
  const roots = [];
422
274
  for (const node of byId.values()) {
423
275
  const parent = node.parentGroupId !== undefined ? byId.get(node.parentGroupId) : undefined;
424
- // Link to parent ONLY when that can't form a cycle — a self/multi-node cycle member grafts onto root, so the
425
- // tree is always acyclic (JSON-safe) and no node is ever dropped.
426
276
  if (parent && parent !== node && !inCycle(node))
427
277
  parent.children.push(node);
428
278
  else
429
- roots.push(node); // top-level, dangling parent, or cycle member → graft onto root (never dropped)
279
+ roots.push(node);
430
280
  }
431
281
  return {
432
282
  id: run.id,
433
283
  scope: run.scope,
434
284
  status: run.status,
435
- // K-2c (shell §K): the workflow header label + subtitle the MF-W detail view needs — from the script's
436
- // `export const meta = {name, description}` (core WorkflowRun.name/description). REDACTED for label parity with
437
- // the fleet run/subagent names (an LLM-authored meta is task-controlled → could carry a secret shape). Per-phase
438
- // title is already on each `phases[].title`. Omitted when absent (back-compat: older runs have neither).
439
285
  ...(run.name !== undefined ? { name: redactSecrets(run.name) } : {}),
440
286
  ...(run.description !== undefined ? { description: redactSecrets(run.description) } : {}),
441
287
  stats: run.stats,
442
- // core 1.293: run-level failed-agent count (agents whose status ended "failed"), stamped at run end only
443
- // when > 0. A plain number (not task-controlled text) → no redaction. Omitted when absent (back-compat:
444
- // pre-1.293 runs / zero failures) — [843]④a: the detail view had no failure count to render.
445
288
  ...(run.agentFailures !== undefined ? { agentFailures: run.agentFailures } : {}),
446
289
  startedAt: run.startedAt,
447
290
  endedAt: run.endedAt,
@@ -449,10 +292,6 @@ function summarizeWorkflowDetail(run) {
449
292
  durationMs: durationOf(run),
450
293
  rev: run.rev,
451
294
  error: run.error,
452
- // core 1.234: the script's RETURN VALUE — bounded + redacted AT THE SOURCE (completion-time,
453
- // same egress discipline as the notifier), but re-redacted here for defence-in-depth parity with
454
- // name/description (this projection's whitelist predates the field — live-dig T8 caught the omission:
455
- // TaskOutput carried the result while this detail view silently dropped it). Absent on failed/pre-1.234 runs.
456
295
  ...(run.result !== undefined ? { result: redactSecrets(run.result) } : {}),
457
296
  phases,
458
297
  ...(unphasedView ? { unphased: unphasedView } : {}),
@@ -460,27 +299,14 @@ function summarizeWorkflowDetail(run) {
460
299
  groups: roots,
461
300
  };
462
301
  }
463
- /** SSE the live progress of a RUNNING workflow (S8). subscribeWorkflow is IN-PROCESS (replica-local): the
464
- * iterable ends when the run's channel closes at run_end, or when the client disconnects. A run with no live
465
- * channel (already finished, or on another replica) yields nothing → the stream ends immediately (the
466
- * historical view is GET /v1/workflows/:id). Owner-gating (run.scope === principal) is done by the caller;
467
- * core 1.366 (B-3) additionally doors the subscription itself on scope — a mismatched scope yields an empty
468
- * stream (same shape as not-found), so the caller passes the SAME scope it owner-gated with. */
469
302
  async function streamWorkflowRun(req, res, runId, scope) {
470
303
  sseHeaders(res);
471
- // `type: "meta"` 与 `event: meta` 同名双发(tasks/trace-usage/approvals 三流同款):部分代理只转发
472
- // `data:` 行,按 data.type 分派的消费端否则认不出首帧。additive,老消费端忽略未知键。
473
304
  res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, runId })}\n\n`);
474
305
  let closed = false;
475
- // subscribeWorkflow is a PUSH iterator: a parked `next()` on an idle-but-alive workflow does NOT observe a
476
- // `closed` flag, so a disconnected client would leave the subscriber queue registered in core's channels and
477
- // publishWorkflowEvent would buffer events for the dead peer forever (a leak). Drive the iterator explicitly so
478
- // disconnect can call `it.return()` — which unwinds the parked next() AND runs core's finally to prune the queue.
479
306
  const it = subscribeWorkflow(runId, scope)[Symbol.asyncIterator]();
480
- // Heartbeat so an idle proxy/mesh doesn't drop the connection (and a dead TCP peer is detected → "close").
481
307
  const hb = setInterval(() => {
482
308
  if (!res.writableEnded)
483
- res.write(`event: heartbeat\ndata: {}\n\n`); // real frame (not an SSE comment) — a per-frame-parsing BFF drops comments, so downstream saw a zero-frame window; EventSource clients without a heartbeat listener ignore it (zero break)
309
+ res.write(`event: heartbeat\ndata: {}\n\n`);
484
310
  }, 15_000);
485
311
  if (typeof hb.unref === "function")
486
312
  hb.unref();
@@ -489,12 +315,12 @@ async function streamWorkflowRun(req, res, runId, scope) {
489
315
  return;
490
316
  closed = true;
491
317
  clearInterval(hb);
492
- void it.return?.(undefined); // prune core's subscriber queue (workflow-observe finally) — stop buffering for a dead peer
318
+ void it.return?.(undefined);
493
319
  if (!res.writableEnded)
494
320
  res.end();
495
321
  };
496
322
  req.on("close", shutdown);
497
- res.on("close", shutdown); // res close (peer gone) too, not just req — either side ending must prune the queue
323
+ res.on("close", shutdown);
498
324
  try {
499
325
  for (let next = await it.next(); !next.done; next = await it.next()) {
500
326
  if (closed || res.writableEnded)
@@ -504,8 +330,6 @@ async function streamWorkflowRun(req, res, runId, scope) {
504
330
  }
505
331
  }
506
332
  catch {
507
- // A1/A6/B:归一 error 帧形(`type` + 机器码)。此前只发人话 message ⇒ 消费端要么锚文案、要么把
508
- // "流断了"和"流正常结束"混为一谈;`workflow.stream_error` 是本帧的铸码(附录 A `*.stream_error` 族)。
509
333
  if (!closed && !res.writableEnded)
510
334
  res.write(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: "workflow.stream_error", message: "workflow stream error" })}\n\n`);
511
335
  }
@@ -1,18 +1,12 @@
1
1
  import { redactSecrets } from "../trace/redact.js";
2
- /** The denormalized run-row metadata captured at submit: correlation (jobId), credential-derived source,
3
- * and a redacted objective glance for lists. One place so the three submit paths can't drift. */
4
2
  export function runMeta(prepared, source) {
5
3
  const obj = prepared.spec.objective ?? "";
6
4
  return {
7
5
  jobId: prepared.jobId ?? null,
8
6
  source,
9
- // redactSecrets is best-effort (known key/token shapes only — NOT exhaustive; see redact.ts). Acceptable
10
- // here: the preview rides the fleet-token-gated trace surface, not an end-user-readable channel (council #1).
11
7
  objectivePreview: obj ? redactSecrets(obj).slice(0, 120) : null,
12
8
  };
13
9
  }
14
- /** Build the cascade config from the operator ladder. Default gate = `status==="completed"` (escalate
15
- * on a cheap-rung failure); cost-ceiling from the task's budget bounds the cold-rerun tax. */
16
10
  export function cascadeConfig(ladder, maxCostUsd) {
17
11
  return {
18
12
  ladder: ladder.map((model) => ({ model })),
package/dist/http/send.js CHANGED
@@ -6,47 +6,15 @@ export function sseHeaders(res, extra) {
6
6
  ...extra,
7
7
  });
8
8
  }
9
- /**
10
- * 一条 SSE 连接的墙钟上限。到点发一帧 `error`/`STREAM_MAX_DURATION`(**不是**静默关闭 —— 客户端分不出
11
- * 「到帽了请重连」和「跑完了」)然后收流。
12
- *
13
- * 🔴 为什么共享(2026-08-08,#176 二轮扫描 finding):这个数与下面的心跳间隔此前在**三处**各手抄一份
14
- * —— `http/sse-log.ts`(task_run + image_bake 共用的那台泵)、`http/routes/trace-usage.ts`、
15
- * `http/routes/approvals-assistant.ts`(它的顶注还自称「parity with streamTaskTrace」)。三处各自有测试
16
- * 钉着**自己那一份**,没有任何跨副本的一致性钉 ⇒ 单边调帽后「parity」只剩注释里那句话。
17
- *
18
- * ⚠️ 改这个数请连**文案**一起改:三处的 `STREAM_MAX_DURATION` 消息里把「15-minute」写死在散文里
19
- * (文案面另有 freeze 门,故此处不做插值——散文与常量的同步靠这行告诫,不靠机器)。
20
- */
21
9
  export const SSE_MAX_STREAM_MS = 15 * 60 * 1000;
22
- /**
23
- * 长时间无事件时的心跳间隔(防中继的 idle timeout 掐流)。与 {@link SSE_MAX_STREAM_MS} 同批收口。
24
- *
25
- * ⚠️ 收口的只有**间隔**,**不含帧形**:`sse-log` / `trace-usage` 发 `data: {}`,`approvals-assistant`
26
- * 发 `data: {"type":"heartbeat"}`。这是真分歧不是笔误 —— trace-usage 自己的 meta 帧还引着 [2373]C-12
27
- * 「每个 data payload 都带 type」的约定,而它的心跳帧没有 type(probe-sse-1 的注释把这条记成「按
28
- * data.type 分派的客户端会拿到 undefined」)。统一帧形是**行为面**改动(要么给两处加 type=多发一个键,
29
- * 要么给 approvals 减 type=消费端破坏),属候裁,不在本批的零行为码范围内。
30
- */
31
10
  export const SSE_HEARTBEAT_IDLE_MS = 15_000;
32
11
  export function sendJson(res, status, body) {
33
12
  res.writeHead(status, { "content-type": "application/json" });
34
13
  res.end(JSON.stringify(body));
35
14
  }
36
- /** A6(design/158 lens1 §C3/lens3 §C3a): canonical error-response sender for sites that already carry a
37
- * machine `errorCode` — `errorCode` is a required positional arg (not optional-bag) so a call site
38
- * cannot regress to the "code but no errorCode" shape the error-code-key-gate forbids. Body shape is
39
- * `{ error: message, errorCode, ...extra }`; a site that used to ALSO emit a legacy `code` key keeps it
40
- * by passing `{ code: ... }` inside `extra` — this is a pure call-site migration (zero wire-contract
41
- * change), not a new response shape. Sites where the two machine-code channels genuinely disagree, or
42
- * where `error`/`errorCode` aren't cleanly separable from the rest of the body, are left on raw
43
- * `sendJson` (see the A6 migration notes for the callers still doing that on purpose). */
44
15
  export function sendError(res, status, errorCode, message, extra) {
45
16
  sendJson(res, status, { error: message, errorCode, ...extra });
46
17
  }
47
- /** B7(design/158 lens1 §C3):`HttpError` 的兜底 catch-all 站点没有自己的语义分支——`err.code` 在 42 个
48
- * throw 点里只有 3 个带。为了让「errorCode 恒在」在这些站点也成立(而不是靠豁免表放行),按**状态码**
49
- * 给一个诚实的粗码:它只声明「这类拒绝」,不冒充比 throw 点更细的语义。带 `code` 的 throw 点原样优先。 */
50
18
  export function httpErrorCode(status, code) {
51
19
  if (code)
52
20
  return code;