@sema-agent/server 4.3.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +16 -16
  4. package/dist/approval-hmac.js +1 -1
  5. package/dist/approval.d.ts +17 -37
  6. package/dist/approval.js +26 -112
  7. package/dist/auth-bridge.d.ts +0 -1
  8. package/dist/auth-bridge.js +0 -1
  9. package/dist/auth-keys.d.ts +1 -1
  10. package/dist/auth-keys.js +1 -1
  11. package/dist/bench/s1/run-firm.js +1 -1
  12. package/dist/boot/config-center.d.ts +1 -1
  13. package/dist/boot/config-center.js +4 -3
  14. package/dist/boot/reapers.d.ts +1 -2
  15. package/dist/boot/reapers.js +4 -5
  16. package/dist/boot/resolve-spec.d.ts +1 -3
  17. package/dist/boot/resolve-spec.js +21 -35
  18. package/dist/boot/stores.js +1 -1
  19. package/dist/brain.js +3 -1
  20. package/dist/capabilities/center-prompts.d.ts +2 -3
  21. package/dist/capabilities/center-prompts.js +2 -3
  22. package/dist/capabilities/sandbox-file-send.d.ts +0 -2
  23. package/dist/capabilities/sandbox-file-send.js +2 -5
  24. package/dist/capabilities/scenarios.d.ts +2 -13
  25. package/dist/capabilities/scenarios.js +3 -22
  26. package/dist/capabilities/send-user-file-tool.js +3 -3
  27. package/dist/config-center/apply-effective.js +51 -13
  28. package/dist/config-center/types.d.ts +10 -3
  29. package/dist/config-provider.d.ts +1 -1
  30. package/dist/config-provider.js +6 -11
  31. package/dist/config-types.d.ts +15 -29
  32. package/dist/config.d.ts +28 -0
  33. package/dist/config.js +122 -68
  34. package/dist/fleet/fleet-bus.d.ts +1 -1
  35. package/dist/fleet/fleet-bus.js +25 -33
  36. package/dist/fleet-client.js +1 -1
  37. package/dist/http/active-run-conflict.d.ts +2 -0
  38. package/dist/http/active-run-conflict.js +1 -0
  39. package/dist/http/routes/approvals-assistant.d.ts +2 -1
  40. package/dist/http/routes/approvals-assistant.js +10 -96
  41. package/dist/http/routes/capabilities.js +6 -5
  42. package/dist/http/routes/fleet.js +7 -2
  43. package/dist/http/routes/images.js +10 -4
  44. package/dist/http/routes/runs.js +42 -65
  45. package/dist/http/routes/session-sync.d.ts +1 -1
  46. package/dist/http/routes/session-sync.js +2 -1
  47. package/dist/http/routes/tasks.js +7 -7
  48. package/dist/http/routes/trace-usage.js +1 -1
  49. package/dist/http/routes/workflows.js +3 -1
  50. package/dist/http/server.d.ts +1 -6
  51. package/dist/http/server.js +15 -10
  52. package/dist/http/verify-rounds.d.ts +2 -2
  53. package/dist/http/wire-types.d.ts +1 -1
  54. package/dist/images/bake-validate.d.ts +0 -22
  55. package/dist/images/bake-validate.js +0 -4
  56. package/dist/index.d.ts +4 -4
  57. package/dist/index.js +2 -5
  58. package/dist/main.js +9 -10
  59. package/dist/memory-scope.d.ts +1 -2
  60. package/dist/memory-scope.js +8 -4
  61. package/dist/orchestration/workflow-completion-inbox.d.ts +6 -4
  62. package/dist/orchestration/workflow-completion-inbox.js +6 -4
  63. package/dist/plugins/breaker-state-sql.d.ts +2 -3
  64. package/dist/plugins/file-run-store.d.ts +1 -1
  65. package/dist/plugins/file-run-store.js +1 -1
  66. package/dist/plugins/image-index-sql.d.ts +0 -1
  67. package/dist/plugins/image-index-sql.js +0 -1
  68. package/dist/plugins/local-checkpoint-store.d.ts +1 -1
  69. package/dist/plugins/local-checkpoint-store.js +1 -1
  70. package/dist/plugins/memory-engine-pg.js +2 -1
  71. package/dist/plugins/memory-engine-tidb.js +1 -1
  72. package/dist/plugins/memory-engine-vector-util.d.ts +2 -3
  73. package/dist/plugins/memory-engine-vector-util.js +2 -3
  74. package/dist/plugins/memory-run-store.d.ts +2 -2
  75. package/dist/plugins/memory-run-store.js +1 -1
  76. package/dist/plugins/memory-session-policy-store.d.ts +1 -1
  77. package/dist/plugins/memory-session-policy-store.js +1 -1
  78. package/dist/plugins/memory-sync-store-pg.js +1 -1
  79. package/dist/plugins/pg-pool.js +8 -10
  80. package/dist/plugins/pg-rate-limiter.d.ts +1 -4
  81. package/dist/plugins/pg-rate-limiter.js +0 -1
  82. package/dist/plugins/roster-store-sql.d.ts +1 -3
  83. package/dist/plugins/roster-store-sql.js +3 -5
  84. package/dist/plugins/store-backend.d.ts +7 -17
  85. package/dist/plugins/store-backend.js +14 -30
  86. package/dist/plugins/tidb-pool.js +6 -30
  87. package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
  88. package/dist/plugins/workflow-journal-store-sql.js +1 -1
  89. package/dist/principal-jwt.js +2 -2
  90. package/dist/prompts-domain-validate.d.ts +4 -3
  91. package/dist/prompts-domain-validate.js +7 -8
  92. package/dist/router/route-orchestration.js +3 -3
  93. package/dist/run-local.js +13 -10
  94. package/dist/runs.js +8 -6
  95. package/dist/runtime-caps-resolver.d.ts +3 -2
  96. package/dist/runtime-caps-resolver.js +4 -4
  97. package/dist/runtime-governance.d.ts +4 -7
  98. package/dist/runtime-governance.js +12 -17
  99. package/dist/sealed-key.d.ts +1 -1
  100. package/dist/sealed-key.js +1 -1
  101. package/dist/security.d.ts +0 -5
  102. package/dist/security.js +8 -12
  103. package/dist/session-sync.d.ts +0 -2
  104. package/dist/session-sync.js +10 -17
  105. package/dist/spec-fields.js +6 -6
  106. package/dist/task-settings.d.ts +0 -27
  107. package/dist/task-settings.js +42 -11
  108. package/dist/tool-approval.d.ts +3 -3
  109. package/dist/tool-approval.js +10 -10
  110. package/dist/trace/artifacts.d.ts +11 -0
  111. package/dist/trace/artifacts.js +8 -19
  112. package/dist/trace/ledger-sink.js +7 -1
  113. package/dist/trace/project.d.ts +17 -0
  114. package/dist/trace/project.js +58 -5
  115. package/package.json +3 -3
  116. package/dist/capabilities/oa-tools.d.ts +0 -15
  117. package/dist/capabilities/oa-tools.js +0 -54
  118. package/dist/plugins/approval-store-sql.d.ts +0 -116
  119. package/dist/plugins/approval-store-sql.js +0 -150
  120. package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
  121. package/dist/plugins/file-workflow-journal-store.js +0 -12
  122. package/dist/plugins/pg-approval-store.d.ts +0 -9
  123. package/dist/plugins/pg-approval-store.js +0 -9
  124. package/dist/plugins/pg-breaker-state.d.ts +0 -8
  125. package/dist/plugins/pg-breaker-state.js +0 -8
  126. package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
  127. package/dist/plugins/pg-checkpoint-store.js +0 -10
  128. package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
  129. package/dist/plugins/pg-file-snapshot-store.js +0 -8
  130. package/dist/plugins/pg-image-bake.d.ts +0 -12
  131. package/dist/plugins/pg-image-bake.js +0 -11
  132. package/dist/plugins/pg-image-index.d.ts +0 -12
  133. package/dist/plugins/pg-image-index.js +0 -11
  134. package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
  135. package/dist/plugins/pg-outcome-ledger.js +0 -11
  136. package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
  137. package/dist/plugins/pg-resume-anchor-store.js +0 -7
  138. package/dist/plugins/pg-run-store.d.ts +0 -9
  139. package/dist/plugins/pg-run-store.js +0 -9
  140. package/dist/plugins/pg-session-policy-store.d.ts +0 -7
  141. package/dist/plugins/pg-session-policy-store.js +0 -7
  142. package/dist/plugins/pg-session-store.d.ts +0 -12
  143. package/dist/plugins/pg-session-store.js +0 -12
  144. package/dist/plugins/pg-tool-result-store.d.ts +0 -9
  145. package/dist/plugins/pg-tool-result-store.js +0 -9
  146. package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
  147. package/dist/plugins/pg-workflow-journal-store.js +0 -9
  148. package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
  149. package/dist/plugins/pg-workflow-run-store.js +0 -9
  150. package/dist/plugins/tidb-approval-store.d.ts +0 -8
  151. package/dist/plugins/tidb-approval-store.js +0 -8
  152. package/dist/plugins/tidb-breaker-state.d.ts +0 -7
  153. package/dist/plugins/tidb-breaker-state.js +0 -7
  154. package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
  155. package/dist/plugins/tidb-checkpoint-store.js +0 -9
  156. package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
  157. package/dist/plugins/tidb-file-snapshot-store.js +0 -8
  158. package/dist/plugins/tidb-image-bake.d.ts +0 -12
  159. package/dist/plugins/tidb-image-bake.js +0 -11
  160. package/dist/plugins/tidb-image-index.d.ts +0 -12
  161. package/dist/plugins/tidb-image-index.js +0 -11
  162. package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
  163. package/dist/plugins/tidb-outcome-ledger.js +0 -12
  164. package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
  165. package/dist/plugins/tidb-resume-anchor-store.js +0 -7
  166. package/dist/plugins/tidb-run-store.d.ts +0 -10
  167. package/dist/plugins/tidb-run-store.js +0 -9
  168. package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
  169. package/dist/plugins/tidb-session-policy-store.js +0 -7
  170. package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
  171. package/dist/plugins/tidb-tool-result-store.js +0 -10
  172. package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
  173. package/dist/plugins/tidb-workflow-journal-store.js +0 -9
  174. package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
  175. package/dist/plugins/tidb-workflow-run-store.js +0 -10
  176. package/dist/plugins/workflow-journal-limits.d.ts +0 -12
  177. package/dist/plugins/workflow-journal-limits.js +0 -12
@@ -1,5 +1,6 @@
1
- import { canonicalToolName } from "@sema-agent/core";
2
- import { principalFrom, verifyDirectDoorProof, MAX_APPROVAL_REASON_CHARS, decodeCheckpointScope } from "../../security.js";
1
+ import { principalFrom, decodeCheckpointScope } from "../../security.js";
2
+ import { verifyDirectDoorProof } from "../../principal-jwt.js";
3
+ import { MAX_APPROVAL_REASON_CHARS } from "../../approval-hmac.js";
3
4
  import { redactedPreview } from "../../trace/redact.js";
4
5
  import { fleetRunLabels } from "../../fleet/fleet-bus.js"; // [2069]④ §3 行展示名与 fleet 行同源(见用处的注)
5
6
  import { sleep } from "../sse-log.js";
@@ -123,7 +124,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
123
124
  sendError(res, 400, "request.route_unsupported", "DELETE needs /exemptions/:toolName");
124
125
  return;
125
126
  }
126
- const revoked = await deps.approvalExemptionStore.revoke(exSessionId, canonicalToolName(exToolName));
127
+ const revoked = await deps.approvalExemptionStore.revoke(exSessionId, exToolName); // RAW(5.0.0):豁免键空间=CC canonical 名,live/存量行同名
127
128
  sendJson(res, revoked ? 200 : 404, revoked ? { revoked: true } : { error: "not found", errorCode: "not_found.exemption" });
128
129
  return;
129
130
  }
@@ -445,7 +446,9 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
445
446
  sendJson(res, out.status, out.body);
446
447
  return;
447
448
  }
448
- const m = /^\/v1\/approvals\/([^/]+?)(?:\/decide)?$/.exec(url);
449
+ // [2373]C-4(5.0.0):裸 `POST /v1/approvals/:id` 装饰臂删——/decide 是唯一决裁形(裸形曾与
450
+ // `GET /v1/approvals/:id` 撞 URL 空间,并把限流/计费/wire 注三处记账劈成两条)。
451
+ const m = /^\/v1\/approvals\/([^/]+)\/decide$/.exec(url);
449
452
  if (m && req.method === "POST") {
450
453
  if (rateLimited(req, res) || quotaExceeded(req, res))
451
454
  return; // 🔴 复审 C2:lease admitted in driveResumeIntoRunLog (owner principal). resume hits TiDB — rate-limit (council)
@@ -622,7 +625,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
622
625
  ? async (overrideSessionId) => {
623
626
  const grantSessionId = overrideSessionId ?? sessionId;
624
627
  try {
625
- const canonical = canonicalToolName(rememberToolName);
628
+ const canonical = rememberToolName; // RAW(5.0.0):行 toolName 由 core 铸,恒 canonical
626
629
  await deps.approvalExemptionStore.grant(grantSessionId, canonical, deciderPrincipal ?? null);
627
630
  rememberApplied = true;
628
631
  deps.logger?.info?.("approval_exemption_granted", { sessionId: grantSessionId, toolName: canonical, grantedBy: deciderPrincipal ?? null });
@@ -643,97 +646,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
643
646
  sendError(res, 404, "not_found.route", "not found");
644
647
  return;
645
648
  }
646
- // Human-in-the-loop approval gate (F4). Operators list/decide; the running task polls the row.
647
- if (deps.approvalStore && url.startsWith("/v1/approvals")) {
648
- const store = deps.approvalStore;
649
- // 🔴 D-G belt (defense-in-depth): this LEGACY block authorizes /approvals via the trusted x-agent-principal
650
- // header (no crypto). On a direct-door worker that path must NEVER be live — the boot invariant already forces
651
- // DURABLE_APPROVAL (so checkpointStore owns /approvals above and we never reach here), but if a direct-door
652
- // worker ever fell through to here, fail closed rather than expose the header-trusting path.
653
- if (deps.config.directApprovalDoor) {
654
- sendError(res, 503, "approval_door_unconfigured", "approvals are served by the direct door on this worker");
655
- return;
656
- }
657
- if (req.method === "GET" && url === "/v1/approvals") {
658
- const principal = principalFrom(req, deps.config);
659
- if (isOperator(principal, deps.config.operatorPrincipals)) {
660
- // operator: may filter by any ?owner (or list all). scope is owner-sourced, so the operator's ?owner
661
- // filter IS the scope filter; an unfiltered operator list is cross-tenant BY DESIGN (operator queue).
662
- const owner = new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined;
663
- sendJson(res, 200, { pending: owner ? await store.listPending(owner, owner) : await store.listPendingAll() });
664
- }
665
- else {
666
- // non-operator: only ITS OWN pending (ignore any ?owner spoof) — never others' tool-call args. scope =
667
- // the caller's principal (owner-sourced), so the single-DB read guard isolates it at the SQL layer too.
668
- sendJson(res, 200, { pending: principal ? await store.listPending(principal, principal) : [] });
669
- }
670
- return;
671
- }
672
- const m = /^\/v1\/approvals\/([^/]+)$/.exec(url);
673
- if (m) {
674
- const id = m[1];
675
- if (req.method === "GET") {
676
- // The row carries the full tool-call args (commands/paths/contents of a high-risk op). The list
677
- // endpoint deliberately scopes non-operators to their OWN pending ("never others' tool-call args");
678
- // a by-id GET must enforce the same boundary, else any token-holder who learns an id reads another
679
- // tenant's args. Operators see all; everyone else only their own. 404 (not 403) to non-owners so
680
- // existence isn't revealed — symmetric with runOwnerOk.
681
- const principal = principalFrom(req, deps.config);
682
- if (deps.config.requirePrincipal && !principal) {
683
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
684
- return;
685
- }
686
- const operator = isOperator(principal, deps.config.operatorPrincipals);
687
- // Operators read across tenants (scope-agnostic getById); a non-operator reads ONLY its own scope so the
688
- // single-DB read guard fires at the SQL layer (no cross-tenant args even if it learns an id).
689
- const row = operator ? await store.getById(id) : principal != null ? await store.get(id, principal) : undefined;
690
- if (!row || (!operator && row.owner !== null && row.owner !== principal)) {
691
- sendError(res, 404, "not_found.approval", "approval not found");
692
- return;
693
- }
694
- sendJson(res, 200, row);
695
- return;
696
- }
697
- if (req.method === "POST") {
698
- // Deciding (approve/deny) is OPERATOR-ONLY — a requester approving its own high-risk op would
699
- // defeat the F4 gate. (Legacy/unset OPERATOR_PRINCIPALS → the service token is the boundary — but
700
- // ONLY on a single-tenant deployment: under REQUIRE_PRINCIPAL the empty-list true-for-all would let
701
- // ANY tenant decide ANOTHER tenant's approval [the durable /decide got
702
- // this hardening earlier and this legacy leg never did] — multi-tenant demands an EXPLICIT
703
- // operator list, same posture as the durable twin.)
704
- const principal = principalFrom(req, deps.config);
705
- const operatorOk = deps.config.requirePrincipal
706
- ? explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals)
707
- : isOperator(principal, deps.config.operatorPrincipals);
708
- if (!operatorOk) {
709
- sendError(res, 403, "auth.operator_only", "only an operator may decide an approval (set OPERATOR_PRINCIPALS)");
710
- return;
711
- }
712
- const body = (await readJson(req));
713
- const decision = body?.decision === "approve" ? "approved" : body?.decision === "deny" ? "denied" : undefined;
714
- if (!decision) {
715
- sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason? }");
716
- return;
717
- }
718
- // The operator decides ACROSS tenants by design (the F4 operator queue), so we bind the decide CAS to the
719
- // row's OWN scope (read scope-agnostically first) rather than the operator's principal — the scope<=>?
720
- // clause then becomes an exact, consistent guard instead of a cross-tenant bypass. Unknown id ⇒ 409.
721
- const target = await store.getById(id);
722
- if (!target) {
723
- sendError(res, 409, "conflict.approval_settled", "approval already decided or not found");
724
- return;
725
- }
726
- const applied = await store.decide(id, target.scope, decision, body.reason ?? null, principal ?? null);
727
- if (applied)
728
- sendJson(res, 200, { id, status: decision });
729
- else
730
- sendError(res, 409, "conflict.approval_settled", "approval already decided or not found");
731
- return;
732
- }
733
- }
734
- sendError(res, 404, "not_found.route", "not found");
735
- return;
736
- }
649
+ // [2354]/[2356] A-5(5.0.0):legacy F4 轮询腿(approvalStore list/by-id/decide,trusted-header 决裁)
650
+ // 整体退役。无 checkpointStore 的部署打 /v1/approvals 一律落到本域尾部的未匹配路径(404 诚实缺席)
737
651
  miss.fell = true;
738
652
  }
739
653
  /** Server-side poll cadence for the native-push approvals stream. The CLIENT subscribes ONCE and never polls;
@@ -40,8 +40,8 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
40
40
  artifacts: Boolean(deps.runStore),
41
41
  // TRUE when durable HITL can FIRE = checkpointStore present (DURABLE_APPROVAL=true): it mounts
42
42
  // AskUserQuestion-suspend + the approval center + resumable checkpoints, independent of whether any
43
- // TOOL is gated (APPROVAL_REQUIRE = the additive F4 layer). Legacy poll-gate approvalStore does NOT
44
- // count: alone it can't suspend (the P2-preflight lie).
43
+ // TOOL is gated (APPROVAL_REQUIRE = the additive F4 layer). ([2354] A-5:legacy 轮询门已退役,
44
+ // durable 是唯一门形——本谓词自然成为 approvals 面唯一的挂载条件。)
45
45
  //
46
46
  // 🔴 **辖域比名字宽**(2026-08-01,web [C33]③.3 踩出来的):这个谓词同时是 `/v1/assistant/*`
47
47
  // 整条车道的挂载条件 —— `routes/approvals-assistant.ts` 的分支头逐字是
@@ -54,7 +54,8 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
54
54
  approvals: Boolean(deps.checkpointStore),
55
55
  sessions: Boolean(deps.sessionAudit),
56
56
  // [1196] session 级 SSE 订阅面:与 /v1/sessions/:id/events 的 501 谓词同源(sessionWatch 只在
57
- // sessionStorage 具备 getLeafId/ownerOf 时由 main.ts 构建)——web 据此换轨,false=回落 /head 轮询。
57
+ // sessionStorage 具备 getLeafId/ownerOf 时由 main.ts 构建)。[2373]A-5:截至 2026-08-02 无消费端接线
58
+ // (web 未按此换轨)——本位是预备位;「web 据此换轨」是当年的预期,不是已核实的现状。
58
59
  sessionEvents: Boolean(deps.sessionWatch && deps.sessionStorage?.getLeafId && deps.sessionStorage?.ownerOf),
59
60
  // #13([1871] web asks,clay 排批 2026-07-27):fleet 面能力族——一个诚实对象或 false(无 fleetBus=
60
61
  // 无 /v1/fleet/stream,路由 501 同谓词)。`resume:"snapshot"` 是刻意的语义广告而非搪塞:总线是
@@ -253,8 +254,8 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
253
254
  // 缺席时 issue()/prepareDirectUpload() 一律 fail-loud,工具挂着也必失败)。"says yes ⟺ 调用真能
254
255
  // 成功":只看 config.sendUserFile(签发面已配)会在工具缺席的形态(adb/local-docker lane、多租户
255
256
  // host/ssh)假 yes(配置自查 agent 抓获 2026-07-14);只看 emitter 会在无公网端点时假 yes。
256
- // s3PublicEndpoint=web 渲染/拼接要的公网 base(bootstrap 单一真源判决——server 透出,web 不猜、
257
- // 不走 registry env 双源;工具挂不挂载它都照透——纯感知面,web 可提前接线)。
257
+ // s3PublicEndpoint=公网 base 的单一真源透出(server 透出,消费端不猜、不走 registry env 双源;
258
+ // 纯感知面)。[2373]A-5:截至 2026-08-02 无消费端读它——预备位,「web 渲染要用」是预期非现状。
258
259
  sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
259
260
  s3PublicEndpoint: deps.config.sendUserFile?.publicEndpoint ?? null,
260
261
  // SendUserFile 账本(多租户治理面):true ⇔ 签发面已配 + store backend 在(记账写点活 + GET
@@ -62,7 +62,9 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
62
62
  // a new-generation shell can drop its frame-level own/foreign checks entirely (incl. the cross-shell
63
63
  // restart edge) while keeping them against older servers whose meta lacks the field. Additive (version
64
64
  // stays 1): older shells ignore unknown fields.
65
- res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, scoped: callerScope !== null, sessionScoped: callerSession !== null, bgNotifyFailClosed: true })}\n\n`);
65
+ // `type: "meta"` `event: meta` 同名双发(tasks/trace-usage/approvals 三流同款):部分代理只转发
66
+ // `data:` 行,按 data.type 分派的消费端否则认不出首帧。additive,老消费端忽略未知键。
67
+ res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, scoped: callerScope !== null, sessionScoped: callerSession !== null, bgNotifyFailClosed: true })}\n\n`);
66
68
  const seenTasks = new Set();
67
69
  const seenWf = new Set();
68
70
  // #3 fleet per-session scope (clay 2026-06-30): a row is visible iff it matches the caller's principal scope AND
@@ -95,8 +97,11 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
95
97
  }
96
98
  return rest;
97
99
  };
100
+ // [2373]C-12:data 里补 `type`(=event 名)双发——代理剥 `event:` 行/onmessage 忽略具名事件时,
101
+ // 按 data.type 分派的客户端在本流拿到的是 undefined(approvals 流注释里的真实约束,同理适用)。
102
+ // 单点注入:调用方已带 type 的帧(hook_notice/bg_notification)保持原值。
98
103
  const send = (event, data) => { if (!res.writableEnded)
99
- res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); };
104
+ res.write(`event: ${event}\ndata: ${JSON.stringify({ type: event, ...data })}\n\n`); };
100
105
  // connect-time snapshot (filtered + scope-stripped) — the visible rows become this connection's "seen" set.
101
106
  const snap = bus.snapshot();
102
107
  const tasks = snap.tasks.filter(visT);
@@ -23,6 +23,12 @@ export function coarseStatusForState(state) {
23
23
  return "failed";
24
24
  return "running";
25
25
  }
26
+ /** [2373]C-7:202 受理体的**唯一铸点**——此前四份逐字构造散在 attach/竞态重读/新建/dryRun 四臂,
27
+ * 下一个字段必然只加到其中两处(:213/:217 与 :193/:206 已有分叉风险)。status/state 双发保留
28
+ * (status=state 的粗化像,省客户端抄映射表)。 */
29
+ function bakeAcceptedBody(b) {
30
+ return { bakeId: b.bakeId, eventsUrl: `/v1/images/bakes/${b.bakeId}/events`, status: b.status, state: b.state };
31
+ }
26
32
  /** build.sh's structured exit code → the center band-composer UI's terminal `errorCode` (§P2.14 #2). null for an
27
33
  * uncategorized failure (then `error` free-text + `exitCode` drive the UI). 10=dup-tag, 124=timeout, 507=disk. */
28
34
  export function errorCodeForExit(exitCode, cancelled) {
@@ -168,7 +174,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
168
174
  // (a) an IDENTICAL-argv in-flight bake ⇒ attach (idempotent re-submit, 202) — no second build.
169
175
  const sameArgv = await bakes.findActiveByArgv(argv);
170
176
  if (sameArgv) {
171
- sendJson(res, 202, { bakeId: sameArgv.bakeId, eventsUrl: `/v1/images/bakes/${sameArgv.bakeId}/events`, status: sameArgv.status, state: sameArgv.state });
177
+ sendJson(res, 202, bakeAcceptedBody(sameArgv));
172
178
  return;
173
179
  }
174
180
  // (b) M4: ATOMIC admission — the busy-check and the create are ONE DB statement (`INSERT … SELECT …
@@ -181,18 +187,18 @@ async function handleImagesBody(req, res, url, ctx, miss) {
181
187
  if (!r.created) {
182
188
  const sameNow = await bakes.findActiveByArgv(argv);
183
189
  if (sameNow) {
184
- sendJson(res, 202, { bakeId: sameNow.bakeId, eventsUrl: `/v1/images/bakes/${sameNow.bakeId}/events`, status: sameNow.status, state: sameNow.state });
190
+ sendJson(res, 202, bakeAcceptedBody(sameNow));
185
191
  return;
186
192
  }
187
193
  const busy = await bakes.findActiveAny();
188
194
  sendError(res, 409, "conflict.bake_in_progress", "a bake is already in progress", { ...(busy ? { activeBakeId: busy.bakeId, eventsUrl: `/v1/images/bakes/${busy.bakeId}/events` } : {}) });
189
195
  return;
190
196
  }
191
- sendJson(res, 202, { bakeId: r.bake.bakeId, eventsUrl: `/v1/images/bakes/${r.bake.bakeId}/events`, status: r.bake.status, state: r.bake.state });
197
+ sendJson(res, 202, bakeAcceptedBody(r.bake));
192
198
  return;
193
199
  }
194
200
  const { bake } = await bakes.createBake(createInput);
195
- sendJson(res, 202, { bakeId: bake.bakeId, eventsUrl: `/v1/images/bakes/${bake.bakeId}/events`, status: bake.status, state: bake.state });
201
+ sendJson(res, 202, bakeAcceptedBody(bake));
196
202
  return;
197
203
  }
198
204
  // GET /v1/images/bakes/:bakeId/events — resumable SSE (the live "baking…" view), run-trace-SSE-shaped (§P2.2).
@@ -54,6 +54,29 @@ export const RUN_TASK_OUTPUT_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/output$
54
54
  export const RUN_TASK_STOP_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/stop$/;
55
55
  /** The task_run SSE — a thin wrapper over `streamSseLog` that PRESERVES the existing wire shape exactly
56
56
  * (`data: {type, ...data}`, `event: failed` stale frame). Behavior-identical to the pre-refactor reader. */
57
+ /** [2373]C-5:subagent/task-handle 两读写面的**共享 authz 门**(principal→owner→session 三级,
58
+ * [1493] 模型)。此前两面逐字重写——[1493] 类修复要改两次的真风险。返回 run 行;`null`=已应答
59
+ * (401/404,honest 404 无存在性 oracle)。语义逐字保留:operator 双跳过(owner+session)、
60
+ * session-unbound run 仅 principal 门、fail-closed。 */
61
+ async function subagentReadGate(req, res, deps, runId) {
62
+ const principal = gatedPrincipal(req, deps.config);
63
+ if (deps.config.requirePrincipal && principal === undefined) {
64
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
65
+ return null;
66
+ }
67
+ const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
68
+ const run = await deps.runStore.getRun(runId);
69
+ if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
70
+ sendError(res, 404, "not_found.run", "run not found");
71
+ return null;
72
+ }
73
+ const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
74
+ if (!trusted && run.sessionId && callerSession !== run.sessionId) {
75
+ sendError(res, 404, "not_found.run", "run not found");
76
+ return null;
77
+ }
78
+ return { run, trusted };
79
+ }
57
80
  async function streamRunEvents(req, res, runStore, taskId, staleMs) {
58
81
  await streamSseLog(req, res, {
59
82
  statusOf: async (id) => {
@@ -900,44 +923,18 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
900
923
  sendError(res, 501, "capability.run_store_required", "subagent output reads require the run store");
901
924
  return;
902
925
  }
903
- const principal = gatedPrincipal(req, deps.config);
904
- if (deps.config.requirePrincipal && principal === undefined) {
905
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
906
- return;
907
- }
908
926
  const runId = safeDecode(subOutputMatch[1]);
909
927
  const target = safeDecode(subOutputMatch[2]);
910
928
  if (runId === null || target === null) {
911
929
  sendError(res, 400, "request.path_malformed", "malformed subagent path (invalid percent-encoding)");
912
930
  return;
913
931
  }
914
- const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
915
- const run = await deps.runStore.getRun(runId);
916
- if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
917
- sendError(res, 404, "not_found.run", "run not found");
918
- return;
919
- }
920
- // [1493] SESSION-scoped content read. principal answers "is this your data at all" (whose); session
921
- // answers "which conversation" (which). This verb returns CONVERSATION CONTENT (a child's final body), so
922
- // the principal owner-gate above is necessary but NOT sufficient: under a SHARED principal (single-user
923
- // multi-shell — the default local posture, and any tenant that shares an identity) it passes for every
924
- // co-principal shell, so a SIBLING session could read another conversation's child (clay's dual-shell
925
- // repro). The run belongs to a conversation (run.sessionId); a non-operator caller must PROVE it owns that
926
- // conversation by asserting the matching `?session=`. Threat model is ACCIDENTAL cross-read within one
927
- // user's own shells (no adversary — the shell honestly reports its own session), so an honest
928
- // caller-asserted session is correct AND sufficient here (no unguessable capability needed). fable5 R1
929
- // (deliberate boundary, do not "harden" this into a capability): yes, a co-principal caller COULD forge the
930
- // target's session — but co-principal means the SAME authz identity, i.e. the same user reading their own
931
- // data through another window; session is a content-ROUTING axis inside one principal, not an authz wall.
932
- // Deployments with genuinely different humans MUST give them distinct principals (requirePrincipal) — the
933
- // principal owner-gate above is the security boundary, and it runs first. An
934
- // operator/trace caller bypasses (as it does the owner-gate). A run with no sessionId is not
935
- // conversation-bound → principal gate only. Fail-CLOSED: a session-bound run with no/mismatched assertion → 404.
936
- const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
937
- if (!trusted && run.sessionId && callerSession !== run.sessionId) {
938
- sendError(res, 404, "not_found.run", "run not found");
932
+ const gated = await subagentReadGate(req, res, deps, runId); // [2373]C-5 共享门(principal→owner→session)
933
+ if (!gated)
939
934
  return;
940
- }
935
+ const { run } = gated;
936
+ // [1493] SESSION 边界的完整论证(threat model/为何 caller-asserted session 足够/为何不做 capability)
937
+ // 已随共享门挪至 subagentReadGate 注;本面语义逐字不变。
941
938
  const out = await deps.subagentTaskOutput(target, { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) });
942
939
  const details = out.details;
943
940
  // codex R1 (type gate): pollTask is the GENERIC registry face — same-tuple b*/m*/w* handles (bash
@@ -968,29 +965,19 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
968
965
  sendError(res, 501, "capability.run_store_required", "subagent streams require the run store");
969
966
  return;
970
967
  }
971
- const principal = gatedPrincipal(req, deps.config);
972
- if (deps.config.requirePrincipal && principal === undefined) {
973
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
974
- return;
975
- }
976
968
  const runId = safeDecode(subStreamMatch[1]);
977
969
  const target = safeDecode(subStreamMatch[2]);
978
970
  if (runId === null || target === null) {
979
971
  sendError(res, 400, "request.path_malformed", "malformed subagent path (invalid percent-encoding)");
980
972
  return;
981
973
  }
982
- const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
983
- const run = await deps.runStore.getRun(runId);
984
- if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
985
- sendError(res, 404, "not_found.run", "run not found");
974
+ // [2373]C-5 共享门(principal→owner→session)——本腿此前逐字手写同三层([1493] 语义),与 output/stop
975
+ // 两腿并列;收编到同一函数后一次修复处处生效。判定顺序以共享门为准(与已收编的两腿现行为一致:
976
+ // 路径解码 400 先于 principal 401,门内三级次序不变)
977
+ const gatedTail = await subagentReadGate(req, res, deps, runId);
978
+ if (!gatedTail)
986
979
  return;
987
- }
988
- // [1493] session 门(output 面同注,出生即强制——新面零消费者无软拍需要)。
989
- const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
990
- if (!trusted && run.sessionId && callerSession !== run.sessionId) {
991
- sendError(res, 404, "not_found.run", "run not found");
992
- return;
993
- }
980
+ const { run } = gatedTail;
994
981
  // 🔴 cli[1524] HIGH-2:**subscribe 先行,probe 在后**——原序(probe await→meta→subscribe)在
995
982
  // probe 与 subscribe 之间留竞态窗:窗内子代 settle → task_settled 发布时无订阅者被丢(总线无
996
983
  // replay/sticky)→ 订阅者只收 heartbeat 永挂。先建订阅,窗内 settle 自然落进队列;probe 判 404
@@ -1006,12 +993,16 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1006
993
  return;
1007
994
  }
1008
995
  sseHeaders(res);
996
+ // [2373]C-11(5.0.0 BREAKING,提货单点名):meta 帧的 run 实体键改 `taskId`——同 verb 家族的
997
+ // output 面(:936/:1162)一直叫 taskId,meta 独叫 runId 是家族内第三键名;5.0.0 收一。
1009
998
  // meta 声明:live 帧 replica-local + replay 指引;status=探测快照(终态=不再有帧,发 meta 即收尾,
1010
999
  // cli 拿到「走 replay 面」的信号而不是空挂)。seq=stop-cycle 世代号(settle 铸;core 1.377 起
1011
1000
  // **全域投影**——进程内 live 臂投当前复活周期号 [[1533] ask 兑现,[1548]①],durable 终态 serve
1012
1001
  // 同键同义;老 core 只在 durable 臂在场,缺省容忍)。1=spawn 周期,≥2=复活周期(retained revive
1013
1002
  // 或 tier-3 均 bump——codex 快审 #11 纠,勿窄读成仅 tier-3)。
1014
- res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, runId, target, status: probeDetails.status ?? "running", ...(typeof probeDetails.seq === "number" ? { seq: probeDetails.seq } : {}), live: "replica-local", replayFace: "GET /v1/runs/:id/subagents/:handle/output" })}\n\n`);
1003
+ // `type: "meta"` `event: meta` 同名双发(tasks/trace-usage/approvals 三流同款):部分代理只转发
1004
+ // `data:` 行,按 data.type 分派的消费端否则认不出首帧。additive,老消费端忽略未知键。
1005
+ res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, taskId: runId, target, status: probeDetails.status ?? "running", ...(typeof probeDetails.seq === "number" ? { seq: probeDetails.seq } : {}), live: "replica-local", replayFace: "GET /v1/runs/:id/subagents/:handle/output" })}\n\n`);
1015
1006
  // core 1.382([1561] 提货单③):"parked" 非活非终态(durably suspended pending approval)——跟
1016
1007
  // running/pending 同归"还没完",tail 该继续等而不是收尾指去 replay 面(那是给真终态用的)。子代获批
1017
1008
  // 恢复后会在**同一条**活流上继续产帧,提前收尾会让客户端误判"已经结束",错过恢复后的后续内容。
@@ -1068,31 +1059,17 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1068
1059
  }
1069
1060
  if (stopVerb && rateLimited(req, res))
1070
1061
  return; // mutating, runs no model (parity with detach — rateLimited only, no quota/lease gate)
1071
- const principal = gatedPrincipal(req, deps.config);
1072
- if (deps.config.requirePrincipal && principal === undefined) {
1073
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
1074
- return;
1075
- }
1076
1062
  const runId = safeDecode(taskVerbMatch[1]);
1077
1063
  const target = safeDecode(taskVerbMatch[2]);
1078
1064
  if (runId === null || target === null) {
1079
1065
  sendError(res, 400, "request.path_malformed", "malformed task path (invalid percent-encoding)");
1080
1066
  return;
1081
1067
  }
1082
- const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
1083
- const run = await deps.runStore.getRun(runId);
1084
- if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
1085
- sendError(res, 404, "not_found.run", "run not found");
1068
+ const gatedB = await subagentReadGate(req, res, deps, runId); // [2373]C-5 共享门([1493] 模型,论证见门注)
1069
+ if (!gatedB)
1086
1070
  return;
1087
- }
1088
- // SESSION-scoped content/action face ([1493] model): enforced fail-closed — same boundary rationale as
1089
- // subagentOutput (principal = whose data, session = which conversation; honest caller-asserted session is
1090
- // correct AND sufficient inside one principal; operators bypass; a session-unbound run is principal-only).
1071
+ const { run } = gatedB;
1091
1072
  const q = new URL(req.url ?? "", "http://x").searchParams;
1092
- if (!trusted && run.sessionId && q.get("session") !== run.sessionId) {
1093
- sendError(res, 404, "not_found.run", "run not found");
1094
- return;
1095
- }
1096
1073
  const access = { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) };
1097
1074
  // ?filter= is REFUSED on BOTH verbs, not ignored (fail-loud 400; the face shipped unreleased, zero
1098
1075
  // consumers — and codex R2: a GET-only refusal would leave the stop verb silently accepting it). The
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { IncomingMessage, ServerResponse } from "node:http";
11
11
  import { type SessionRulesRecord } from "@sema-agent/core";
12
- import { type StagingHandle } from "../../session-sync.js";
12
+ import { type StagingHandle } from "../../session-sync-kernel.js";
13
13
  import type { RouteCtx } from "../route-ctx.js";
14
14
  export type PendingStaging = {
15
15
  handle: StagingHandle;
@@ -2,7 +2,8 @@ import { once } from "node:events";
2
2
  import { randomBytes, createHash } from "node:crypto";
3
3
  import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, SessionError, SessionPolicyError } from "@sema-agent/core";
4
4
  import { StringDecoder } from "node:string_decoder";
5
- import { exportSessionManifest, classifySyncRelationshipByIds, SyncConflictError, fileSnapshotImportFace } from "../../session-sync.js";
5
+ import { exportSessionManifest, fileSnapshotImportFace } from "../../session-sync.js";
6
+ import { classifySyncRelationshipByIds, SyncConflictError } from "../../session-sync-kernel.js";
6
7
  import { isUuidShape } from "../../security.js";
7
8
  import { sendJson, sendError, msg } from "../send.js";
8
9
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
@@ -6,7 +6,7 @@ import { defaultSubagentTailBus, projectTailFrame } from "../../fleet/subagent-t
6
6
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
7
7
  import { createLedgerSink } from "../../trace/ledger-sink.js";
8
8
  import { redactSecrets } from "../../trace/redact.js";
9
- import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, workspaceChangedEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
9
+ import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
10
10
  import { cascadeConfig, runMeta } from "../run-meta.js";
11
11
  import { scopedIdempotencyKey } from "../idempotency.js";
12
12
  import { sendJson, sendError, sseHeaders } from "../send.js";
@@ -371,8 +371,9 @@ async function handleTasksBody(req, res, url, ctx, miss) {
371
371
  // parentToolCallId-gated), so this sink is the ONLY path a tick reaches the top-run consumers.
372
372
  // 1.78 (review HIGH): fan it out to the LIVE SSE too (shared whitelist+redact builder, carrying
373
373
  // the E2 identity the shell binds concurrent ticks by) — previously it fed ONLY the fleet bus,
374
- // so the conversation lane never saw a real tick and the in-loop task_progress branch below is
375
- // forward-compat only (dead against core ≤1.208). core swallow-guards a throwing sink.
374
+ // so the conversation lane never saw a real tick and the in-loop task_progress branch below was
375
+ // forward-compat then [2373]B-4:floor ^4.0.0 下它是热路径(腐注方向已翻转,勿按旧注当死支)。
376
+ // core swallow-guards a throwing sink.
376
377
  const fwdInternals = {
377
378
  onForwardEvent: (e) => {
378
379
  fleetPub?.onForwardEvent(e);
@@ -652,10 +653,9 @@ async function handleTasksBody(req, res, url, ctx, miss) {
652
653
  res.write(`data: ${JSON.stringify({ type: "workspace_changed", ...workspaceChangedEventData(ev) })}\n\n`);
653
654
  }
654
655
  else if (ev.type === "compaction_outcome") {
655
- // core 2.5.0([2105]② 披露修):压缩非 compacted 结局上流帧——白名单投影:outcome/trigger
656
- // 闭枚举 verbatim;reason 是引擎说明文本(可能引用配置值)→ redactSecrets;identity 两键同款。
657
- const e = ev;
658
- res.write(`data: ${JSON.stringify({ type: "compaction_outcome", outcome: e.outcome, trigger: e.trigger, ...(e.reason !== undefined ? { reason: redactSecrets(e.reason) } : {}), ...(e.eventId !== undefined ? { eventId: e.eventId } : {}), ...(e.parentToolCallId !== undefined ? { parentToolCallId: e.parentToolCallId } : {}) })}\n\n`);
656
+ // core 2.5.0([2105]②)压缩非 compacted 结局帧。[2373]C-2:内联挑键收进共享 builder,
657
+ // 与两条 durable 腿同源(它曾是唯一不走 builder 的内容帧=三方普查共同盲区)。
658
+ res.write(`data: ${JSON.stringify({ type: "compaction_outcome", ...compactionOutcomeEventData(ev) })}\n\n`);
659
659
  }
660
660
  else if (ev.type === "text_delta" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
661
661
  // 裸 catch-all 白名单化收官(probe-wire-1 表征钉的根治半场):最后四个已知臂离开
@@ -242,7 +242,7 @@ async function streamTaskTrace(req, res, runStore, taskId, staleMs) {
242
242
  sseHeaders(res);
243
243
  // First event declares the stream contract: mode + a version (council — lets the relay detect a worker whose
244
244
  // SSE shape changed, e.g. when P1 adds token-delta granularity). resumeFrom echoes the honored Last-Event-ID.
245
- res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, mode: "delta", resumeFrom: from })}\n\n`);
245
+ res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, mode: "delta", resumeFrom: from })}\n\n`); // [2373]C-12:data.type 双发与内容帧一致
246
246
  let closed = false;
247
247
  req.on("close", () => {
248
248
  closed = true;
@@ -451,7 +451,9 @@ function summarizeWorkflowDetail(run) {
451
451
  * stream (same shape as not-found), so the caller passes the SAME scope it owner-gated with. */
452
452
  async function streamWorkflowRun(req, res, runId, scope) {
453
453
  sseHeaders(res);
454
- res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, runId })}\n\n`);
454
+ // `type: "meta"` `event: meta` 同名双发(tasks/trace-usage/approvals 三流同款):部分代理只转发
455
+ // `data:` 行,按 data.type 分派的消费端否则认不出首帧。additive,老消费端忽略未知键。
456
+ res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, runId })}\n\n`);
455
457
  let closed = false;
456
458
  // subscribeWorkflow is a PUSH iterator: a parked `next()` on an idle-but-alive workflow does NOT observe a
457
459
  // `closed` flag, so a disconnected client would leave the subscriber queue registered in core's channels and
@@ -5,7 +5,7 @@ import type { TaskRequestBody } from "./wire-types.js";
5
5
  import type { ServiceConfig } from "../config-types.js";
6
6
  import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
7
7
  import { type OwnerAwareSessionStore } from "../security.js";
8
- import type { RunStore, ApprovalStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ServiceSessionPolicyStore, ServiceFileSnapshotStore, StoreBackend } from "../plugins/store-backend.js";
8
+ import type { RunStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ServiceSessionPolicyStore, ServiceFileSnapshotStore, StoreBackend } from "../plugins/store-backend.js";
9
9
  import { type MemorySyncRequest, type MemorySyncResponse } from "../memory-sync.js";
10
10
  import type { TaskAttachmentStore } from "../plugins/task-attachment-store.js";
11
11
  import type { LeaderEndpoint } from "../leader/endpoint.js";
@@ -25,8 +25,6 @@ import type { QuotaTracker } from "../observability/cost-quota.js";
25
25
  import type { ModelUsageTracker, PromptManifestTracker } from "../budget.js";
26
26
  import { scopedIdempotencyKey } from "./idempotency.js";
27
27
  export { scopedIdempotencyKey };
28
- import { clampVerifyRounds } from "./verify-rounds.js";
29
- export { clampVerifyRounds };
30
28
  import { streamApprovals, isQuestionAnswer } from "./routes/approvals-assistant.js";
31
29
  export { streamApprovals, isQuestionAnswer };
32
30
  import { cascadeConfig } from "./run-meta.js";
@@ -88,8 +86,6 @@ export interface ServiceStoreDeps {
88
86
  /** E18 resume-at: the eventId→entryId anchor map (tidb/pg/local). Present whenever a backend is — resolveSpec
89
87
  * resolves body.resumeAt(eventId)→entryId, the run lifecycle captures per-turn anchors, E21 purges them. */
90
88
  resumeAnchorStore?: ResumeAnchorStore;
91
- /** Human-in-the-loop approval store (F4 poll gate). Enables /v1/approvals when present. */
92
- approvalStore?: ApprovalStore;
93
89
  /** Per-session approval exemption store — enables decide body `remember:"session"` + the
94
90
  * exemptions list/revoke surfaces on /v1/approvals. The ask-gate probe is wired separately (main.ts). */
95
91
  approvalExemptionStore?: ApprovalExemptionStore;
@@ -439,7 +435,6 @@ export type FlatServiceDeps = ServiceCoreDeps & Partial<ServiceStoreDeps & Servi
439
435
  export type ServiceDeps = FlatServiceDeps & ServiceDepGroups;
440
436
  /** 分组入参 → 平铺视图(createHttpServer 的第一件事)。一组都没给 ⇒ 原样返回(存量调用零开销、零形变)。 */
441
437
  export declare function flattenServiceDeps(deps: ServiceDeps): FlatServiceDeps;
442
- export type { TaskRequestBody } from "./wire-types.js";
443
438
  /** Personalization caps: server-pinned hard limits — a UI may be stricter, never looser. */
444
439
  export declare const MAX_USER_SKILLS = 10;
445
440
  /** = core `SKILL_CONTENT_MAX_CHARS`(1MB 加载门,core 1.293 起 invoke 全文零截断、超 1MB 整拒;常量在