@sema-agent/server 7.56.0 → 7.57.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 (110) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +2 -2
  3. package/USAGE.md +101 -9
  4. package/dist/approval-ask-audit-store.d.ts +100 -1
  5. package/dist/approval-ask-audit-store.js +103 -2
  6. package/dist/approval-card.d.ts +148 -13
  7. package/dist/approval-card.js +82 -13
  8. package/dist/approval.d.ts +31 -0
  9. package/dist/approval.js +18 -0
  10. package/dist/auto-mode-face.d.ts +111 -0
  11. package/dist/auto-mode-face.js +99 -0
  12. package/dist/bench/s1/arms.js +5 -5
  13. package/dist/bench/s1/live-deps.js +11 -11
  14. package/dist/bench/s1/run-firm.js +3 -0
  15. package/dist/bench/s1/runner-ctx.d.ts +4 -0
  16. package/dist/bench/s1/runner-ctx.js +7 -0
  17. package/dist/boot/config-center.js +23 -1
  18. package/dist/boot/coordinators.js +13 -1
  19. package/dist/boot/parked-revive-gate.d.ts +7 -2
  20. package/dist/boot/parked-revive-gate.js +12 -1
  21. package/dist/boot/resolve-spec.d.ts +3 -0
  22. package/dist/boot/resolve-spec.js +3 -1
  23. package/dist/boot/runner-deps.d.ts +12 -0
  24. package/dist/boot/runner-deps.js +28 -3
  25. package/dist/boot/runtime-caps.d.ts +19 -9
  26. package/dist/boot/runtime-caps.js +49 -21
  27. package/dist/boot/session-shell-gate-registry.d.ts +39 -0
  28. package/dist/boot/session-shell-gate-registry.js +21 -0
  29. package/dist/config-catalog.js +14 -5
  30. package/dist/config-types.d.ts +41 -10
  31. package/dist/config.d.ts +21 -0
  32. package/dist/config.js +40 -5
  33. package/dist/http/route-ctx.d.ts +75 -0
  34. package/dist/http/routes/a2a-serve.js +5 -3
  35. package/dist/http/routes/approvals-assistant.js +19 -6
  36. package/dist/http/routes/capabilities.js +21 -4
  37. package/dist/http/routes/memory-origin.d.ts +2 -2
  38. package/dist/http/routes/rules.js +3 -2
  39. package/dist/http/routes/runs.d.ts +0 -14
  40. package/dist/http/routes/runs.js +20 -8
  41. package/dist/http/routes/tasks.js +34 -8
  42. package/dist/http/routes/workflows.js +19 -4
  43. package/dist/http/server.d.ts +11 -1
  44. package/dist/http/server.js +215 -43
  45. package/dist/http/wire-types.d.ts +9 -4
  46. package/dist/leader/diffout.js +2 -1
  47. package/dist/leader/endpoint.js +45 -12
  48. package/dist/leader/fanout.js +6 -5
  49. package/dist/leader/leader.js +17 -14
  50. package/dist/leader/merge.js +17 -12
  51. package/dist/leader/planner.js +3 -2
  52. package/dist/leader/repair-oracle.js +6 -4
  53. package/dist/leader/repair-wire.js +5 -3
  54. package/dist/leader/wire.d.ts +30 -1
  55. package/dist/leader/wire.js +36 -19
  56. package/dist/main.js +43 -7
  57. package/dist/observability/err-text.d.ts +6 -0
  58. package/dist/observability/err-text.js +10 -0
  59. package/dist/observability/fail-open.d.ts +40 -0
  60. package/dist/observability/fail-open.js +19 -0
  61. package/dist/observability/metrics.js +1 -1
  62. package/dist/observability/run-terminal-log.d.ts +120 -0
  63. package/dist/observability/run-terminal-log.js +360 -0
  64. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  65. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  66. package/dist/permission-rule-vocab.d.ts +40 -0
  67. package/dist/permission-rule-vocab.js +17 -0
  68. package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
  69. package/dist/plugins/checkpoint-store-sql.js +19 -3
  70. package/dist/plugins/file-run-store.d.ts +3 -34
  71. package/dist/plugins/file-run-store.js +19 -0
  72. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  73. package/dist/plugins/local-checkpoint-store.js +3 -0
  74. package/dist/plugins/memory-run-store.d.ts +3 -15
  75. package/dist/plugins/memory-run-store.js +19 -0
  76. package/dist/plugins/permission-rule-store-file.js +8 -3
  77. package/dist/plugins/permission-rule-store-sql.js +16 -10
  78. package/dist/plugins/remote-scratchpad.js +3 -2
  79. package/dist/plugins/run-store-sql.d.ts +3 -42
  80. package/dist/plugins/run-store-sql.js +30 -1
  81. package/dist/plugins/store-backend.d.ts +1 -1
  82. package/dist/plugins/store-contracts.d.ts +66 -1
  83. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  84. package/dist/plugins/workflow-run-store-sql.js +10 -2
  85. package/dist/rules-consent.js +20 -13
  86. package/dist/run-cancel-context.d.ts +13 -0
  87. package/dist/run-cancel-context.js +15 -0
  88. package/dist/run-local.js +2 -2
  89. package/dist/runs.d.ts +4 -1
  90. package/dist/runs.js +41 -10
  91. package/dist/runtime-caps-resolver.d.ts +133 -17
  92. package/dist/runtime-caps-resolver.js +41 -3
  93. package/dist/task-settings.d.ts +57 -3
  94. package/dist/task-settings.js +78 -5
  95. package/dist/task-workflow.d.ts +32 -2
  96. package/dist/task-workflow.js +8 -3
  97. package/dist/tool-approval.d.ts +26 -3
  98. package/dist/tool-approval.js +96 -13
  99. package/dist/trace/core-keyset-guard.d.ts +2 -2
  100. package/dist/trace/ledger-sink.d.ts +13 -4
  101. package/dist/trace/ledger-sink.js +14 -6
  102. package/dist/trace/project.d.ts +13 -0
  103. package/dist/trace/project.js +10 -1
  104. package/dist/trace/redact.d.ts +22 -11
  105. package/dist/trace/redact.js +655 -20
  106. package/dist/trace/sema-provenance.d.ts +26 -0
  107. package/dist/trace/sema-provenance.js +11 -0
  108. package/dist/turn-activity.d.ts +32 -2
  109. package/dist/turn-activity.js +29 -4
  110. package/package.json +2 -2
@@ -107,6 +107,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
107
107
  : (principal ?? "__none__");
108
108
  sendJson(res, 200, {
109
109
  pending: (await cs.listPending(scope)).map((r) => projectPendingForWire(r, deps.config)),
110
+ serverNowMs: Date.now(),
110
111
  ...(deps.toolApproval ? { livePending: deps.toolApproval.listLivePending(scope) } : {}),
111
112
  ...(deps.approvalAskAudit ? { crashConverged: deps.approvalAskAudit.listCrashConverged(scope) } : {}),
112
113
  });
@@ -520,9 +521,20 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
520
521
  const APPROVALS_STREAM_POLL_MS = 3000;
521
522
  function redactPendingDisclosures(row) {
522
523
  const rd = row.riskDescriptor;
523
- if (rd === null || rd === undefined || typeof rd.shadowedRule !== "string")
524
+ if (rd === null || rd === undefined)
524
525
  return row;
525
- return { ...row, riskDescriptor: { ...rd, shadowedRule: redactSecrets(rd.shadowedRule) } };
526
+ const shadowedRule = typeof rd.shadowedRule === "string" ? redactSecrets(rd.shadowedRule) : undefined;
527
+ const summary = typeof rd.summary === "string" ? redactSecrets(rd.summary) : undefined;
528
+ if (shadowedRule === undefined && summary === undefined)
529
+ return row;
530
+ return {
531
+ ...row,
532
+ riskDescriptor: {
533
+ ...rd,
534
+ ...(shadowedRule !== undefined ? { shadowedRule } : {}),
535
+ ...(summary !== undefined ? { summary } : {}),
536
+ },
537
+ };
526
538
  }
527
539
  function projectPendingForWire(row, governance) {
528
540
  const redacted = redactPendingDisclosures(row);
@@ -553,19 +565,20 @@ export async function streamApprovals(req, res, cs, scope, governance, pollMs =
553
565
  }
554
566
  const cur = new Map(pending.map((p) => [keyOf(p), p]));
555
567
  const curFrames = new Map([...cur].map(([k, p]) => [k, JSON.stringify({ type: "pending", ...p })]));
568
+ const pendingFrame = (k) => JSON.stringify({ type: "pending", ...cur.get(k), serverNowMs: Date.now() });
556
569
  if (first) {
557
- for (const frame of curFrames.values())
558
- res.write(`event: pending\ndata: ${frame}\n\n`);
570
+ for (const k of curFrames.keys())
571
+ res.write(`event: pending\ndata: ${pendingFrame(k)}\n\n`);
559
572
  res.write(`event: synced\ndata: ${JSON.stringify({ type: "synced", count: pending.length })}\n\n`);
560
573
  first = false;
561
574
  }
562
575
  else {
563
576
  for (const [k, frame] of curFrames)
564
577
  if (prevFrames.get(k) !== frame)
565
- res.write(`event: pending\ndata: ${frame}\n\n`);
578
+ res.write(`event: pending\ndata: ${pendingFrame(k)}\n\n`);
566
579
  for (const [k, p] of prev)
567
580
  if (!cur.has(k))
568
- res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`);
581
+ res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId, serverNowMs: Date.now() })}\n\n`);
569
582
  }
570
583
  prev = cur;
571
584
  prevFrames = curFrames;
@@ -9,8 +9,10 @@ import { mcpInjectionHonored } from "../../task-mcp.js";
9
9
  import { sendJson, sendError } from "../send.js";
10
10
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
11
11
  import { projectMemoryEngineCapability } from "../../memory-posture.js";
12
- import { workflowModelAllowlistFor } from "../../task-workflow.js";
12
+ import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
13
13
  import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
14
+ import { judgeAutoModeArming } from "../../auto-mode-face.js";
15
+ import { coercePermissionModeStrict } from "../../task-settings.js";
14
16
  import { gatedPrincipal } from "../principal-gate.js";
15
17
  function routePairingOf(deps, name, m) {
16
18
  if (isSealedKeyPoison(deps.config.modelApiKeys?.[name]))
@@ -43,6 +45,18 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
43
45
  return;
44
46
  }
45
47
  if (req.method === "GET" && url === "/v1/capabilities") {
48
+ const workflowsEngineCan = deps.workflowsCapable ?? Boolean(deps.workflowRunStore);
49
+ const workflowsDenial = selfOrchestrationDenial({ selfOrchestrationEnabled: deps.config.selfOrchestrationEnabled, requirePrincipal: deps.config.requirePrincipal }, centerEntitlementSourceWired(deps.config));
50
+ const requestedModeRaw = new URL(req.url ?? "", "http://x").searchParams.get("permissionMode");
51
+ const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
52
+ if (requestedModeRaw !== null && requestedMode === undefined) {
53
+ sendError(res, 400, "request.field_invalid", 'permissionMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
54
+ return;
55
+ }
56
+ const entitlementSource = centerEntitlementSourceWired(deps.config);
57
+ const { fault: autoModeFault, ...autoMode } = await judgeAutoModeArming(deps.autoModeFace, { requestedMode, principal: gatedPrincipal(req, deps.config), entitlementSource, localDeny: deps.config.permissionsDisableAutoMode });
58
+ if (autoModeFault !== undefined)
59
+ deps.logger?.warn("auto_mode_face_resolver_fault", { fault: autoModeFault });
46
60
  sendJson(res, 200, {
47
61
  ...deps.capabilities,
48
62
  asyncRuns: Boolean(deps.runStore),
@@ -92,8 +106,10 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
92
106
  modeShellGateTranslation: true,
93
107
  permissionModeAuto: {
94
108
  accepted: true,
95
- classifierSeat: true,
96
- entitlementSource: centerEntitlementSourceWired(deps.config),
109
+ classifierSeat: deps.autoModeFace?.seatMounted === true,
110
+ entitlementSource,
111
+ intentArming: deps.autoModeFace?.engineArmsOnIntent === true,
112
+ ...autoMode,
97
113
  },
98
114
  readFace: deps.config.readFace ?? null,
99
115
  appendSystemPrompt: true,
@@ -141,8 +157,9 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
141
157
  sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
142
158
  s3PublicEndpoint: deps.config.sendUserFile?.publicEndpoint ?? null,
143
159
  sendUserFileLedger: Boolean(deps.sendFileLedger),
144
- workflows: deps.workflowsCapable ?? Boolean(deps.workflowRunStore),
160
+ workflows: workflowsEngineCan && workflowsDenial === null,
145
161
  workflowsList: Boolean(deps.workflowRunStore),
162
+ workflowsGate: { engineCan: workflowsEngineCan, denial: workflowsDenial },
146
163
  workflowModels: (() => {
147
164
  const allow = deps.workflowModelAllowlist
148
165
  ?? workflowModelAllowlistFor({
@@ -99,7 +99,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
99
99
  detail?: string | undefined;
100
100
  eventId: string;
101
101
  at: number;
102
- to: "failed" | "done";
102
+ to: "done" | "failed";
103
103
  requestId: string;
104
104
  }[];
105
105
  custodyBytes: number;
@@ -107,7 +107,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
107
107
  requestId: string;
108
108
  reason: string;
109
109
  at: number;
110
- status: "pending" | "failed" | "done";
110
+ status: "pending" | "done" | "failed";
111
111
  originUnknownKeys?: string[] | undefined;
112
112
  clearanceId: string;
113
113
  entryId: string;
@@ -6,6 +6,7 @@ import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, seri
6
6
  import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
7
7
  import { HttpError } from "../../security.js";
8
8
  import { MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
9
+ import { PERSISTED_RULE_MATCHES, PERSISTED_RULE_TOOLS } from "../../permission-rule-vocab.js";
9
10
  const logger = createLogger();
10
11
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
11
12
  export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
@@ -45,8 +46,8 @@ const LocalImportRowSchema = z
45
46
  .object({
46
47
  rule: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
47
48
  scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
48
- tool: z.literal("Bash"),
49
- match: z.enum(["exact", "prefix"]),
49
+ tool: z.enum(PERSISTED_RULE_TOOLS),
50
+ match: z.enum(PERSISTED_RULE_MATCHES),
50
51
  command: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
51
52
  adds: z.array(LocalImportAddSchema).max(64),
52
53
  })
@@ -1,17 +1,3 @@
1
- /**
2
- * [ref] A9 域模块 —— 异步 run 面与它的全部动词。
3
- *
4
- * 路由:`POST /v1/runs`(异步提交)· `GET /v1/runs/:id[/events]`(轮询 / 可恢复 SSE)· 运行期动词
5
- * `cancel` / `steer` / `compact` / `detach` · 子代理面 `subagents/:target/{steer,resume,output,stream}` ·
6
- * 通用任务句柄 `tasks/:handle/{output,stop}` · 三条 HITL 应答口 `elicitations|questions|tool-approvals/:id/respond`。
7
- *
8
- * 两段搬运:提交+run 级动词一段,子代理/任务句柄/HITL 应答一段——它们在 `handle()` 里原本被 SVC-5 的
9
- * workflow steer 隔开,拆分保持各自位置(本文件里的先后只是同域归并)。`POST /v1/runs` 与 tasks 域共用
10
- * `ctx.legs.prepareSpec`(同一条读体/校验/建 spec 的前段)。
11
- *
12
- * 可变状态全部来自跨域 registry(`idemCache` 幂等、`inflightRuns`/`preemptableRuns` 中止句柄、
13
- * `cancelledViaVerb` first-writer-wins 判别、`steerableRuns` 活流句柄、`wakeParkMints` park 铸造闩)。
14
- */
15
1
  import type { IncomingMessage, ServerResponse } from "node:http";
16
2
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
17
3
  import type { RouteCtx, PreparedTaskSubmission } from "../route-ctx.js";
@@ -1,3 +1,4 @@
1
+ import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventData, redactTerminalResult } from "../../observability/run-terminal-log.js";
1
2
  import { z } from "zod";
2
3
  import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer } from "@sema-agent/core";
3
4
  import { isTerminalRunStatus, isParkedRunStatus } from "../../plugins/store-contracts.js";
@@ -22,10 +23,12 @@ import { normalizeRunEventType } from "../../trace/project.js";
22
23
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
23
24
  import { buildActiveRunConflict } from "../active-run-conflict.js";
24
25
  import { clearTurnActivity, readTurnActivityMs } from "../../turn-activity.js";
26
+ import { buildCancelContext } from "../../run-cancel-context.js";
25
27
  import { buildCrossSliceUsage, readResourceWindow, recordResourceWindow } from "../../resource-window.js";
26
28
  import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
27
29
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
28
30
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
31
+ import { withPrincipal } from "../../observability/principal-context.js";
29
32
  export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
30
33
  export const RUN_CANCEL_RE = /^\/v1\/runs\/([^/]+)\/cancel$/;
31
34
  export const RUN_STEER_RE = /^\/v1\/runs\/([^/]+)\/steer$/;
@@ -136,7 +139,10 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
136
139
  retainedFrom: (id) => runStore.retainedFrom(id),
137
140
  formatEvent: (ev) => {
138
141
  const type = normalizeRunEventType(ev.type);
139
- return { id: ev.seq, event: type, data: { type, ...(ev.data ?? {}) } };
142
+ const data = type === "done" && ev.data && typeof ev.data === "object" && ev.data.result && typeof ev.data.result === "object"
143
+ ? { ...ev.data, result: memoRedactTerminalResult("ev.done.result", ev.data.result) }
144
+ : type === "failed" ? memoRedactedRead("ev.failed", ev.data, (d) => redactLedgerEventData(type, d)) : redactLedgerEventData(type, ev.data);
145
+ return { id: ev.seq, event: type, data: { type, ...(data ?? {}) } };
140
146
  },
141
147
  staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
142
148
  }, taskId, staleMs);
@@ -251,11 +257,11 @@ async function handleRunsBody(req, res, url, ctx, miss) {
251
257
  status: stale ? "failed" : run.status,
252
258
  ...(lastActivityAt !== undefined ? { msSinceLastActivity: Math.max(0, Date.now() - lastActivityAt) } : {}),
253
259
  ...(crossSliceUsage !== undefined ? { crossSliceUsage } : {}),
254
- result: run.result ?? undefined,
260
+ result: isTerminalRunStatus(run.status) ? memoRedactTerminalResult("run.result", run.result ?? undefined) : redactTerminalResult(run.result ?? undefined),
255
261
  supervisorCost,
256
262
  suggestions,
257
263
  errorCode: stale ? undefined : (run.result?.errorCode ?? undefined),
258
- error: stale ? "run stalled (instance lost?)" : run.error ?? undefined,
264
+ error: stale ? "run stalled (instance lost?)" : isTerminalRunStatus(run.status) ? memoRedactedRead("run.error", run.error ?? undefined, redactErrorMessage) : redactErrorMessage(run.error ?? undefined),
259
265
  jobId: run.jobId ?? undefined,
260
266
  source: run.source ?? undefined,
261
267
  });
@@ -324,7 +330,8 @@ async function handleRunsBody(req, res, url, ctx, miss) {
324
330
  }
325
331
  const err = note;
326
332
  try {
327
- const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
333
+ const cancelContext = buildCancelContext(taskId, Date.parse(run.createdAt));
334
+ const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 }, ...(cancelContext ? { cancelContext } : {}) };
328
335
  let lastErr;
329
336
  for (let attempt = 0;; attempt++) {
330
337
  try {
@@ -870,7 +877,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
870
877
  deps.logger?.[level]?.("manual_compact_outcome", { taskId, outcome, note: outcome === "compacted" ? "a compacted{trigger:'manual'} event rode the run stream" : `no event (see compaction.${outcome === "mooted" ? "mooted" : outcome} trace / task terminal events)` });
871
878
  }, (e) => {
872
879
  if (e.code !== "steering.not_running") {
873
- deps.logger?.warn?.("manual_compact_failed", { taskId, err: e instanceof Error ? e.message : String(e) });
880
+ deps.logger?.warn?.("manual_compact_failed", { taskId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
874
881
  }
875
882
  else {
876
883
  deps.logger?.info?.("manual_compact_not_running", { taskId, note: "stream settled between lookup and compact() — benign unless systematic" });
@@ -934,7 +941,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
934
941
  }
935
942
  async function handleRunVerbsBody(req, res, url, ctx, miss) {
936
943
  const { deps } = ctx;
937
- const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
944
+ const { readJson, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, safeDecode, runSessionAcceptOk } = ctx.helpers;
938
945
  const subOutputMatch = req.method === "GET" ? RUN_SUBAGENT_OUTPUT_RE.exec(url) : null;
939
946
  if (subOutputMatch) {
940
947
  if (!deps.runStore || !deps.subagentTaskOutput) {
@@ -1159,7 +1166,12 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1159
1166
  send409("resume.retain_off", "the parent run did not retain sub-agent sessions (set retainSubagentSessions on the run to enable revival)");
1160
1167
  return;
1161
1168
  }
1162
- const marker = await handle.resume(redacted);
1169
+ if (ownerQuotaDenied(res, run.owner ?? undefined))
1170
+ return;
1171
+ if (await ownerLeaseDenied(res, run.owner ?? undefined))
1172
+ return;
1173
+ const resumeFn = handle.resume;
1174
+ const marker = await withPrincipal(run.owner ?? undefined, () => resumeFn(redacted));
1163
1175
  sendJson(res, 200, { taskId: runId, target, status: "running", delivery: "applied", marker, note: `Message queued for delivery to ${handle.agentName ?? target}; it will continue in the background.` });
1164
1176
  return;
1165
1177
  }
@@ -1593,7 +1605,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1593
1605
  backend: deps.backend,
1594
1606
  parkFacility: deps.checkpointStore !== undefined,
1595
1607
  }).active,
1596
- windowMs: deps.config.streamApproval?.windowMs ?? 0,
1608
+ windowMs: deps.config.streamApproval?.windowMs,
1597
1609
  windowMarginMs: deps.config.streamAskWindowMarginMs,
1598
1610
  }
1599
1611
  : undefined, deps.checkpointStore);
@@ -1,3 +1,4 @@
1
+ import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal-log.js";
1
2
  import { runWithVerification, runCascade, uuidv7 } from "@sema-agent/core";
2
3
  import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, resumeAtHttpStatus, stripCheckpointToken, HEARTBEAT_MS } from "../../runs.js";
3
4
  import { withPrincipal } from "../../observability/principal-context.js";
@@ -7,6 +8,7 @@ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotific
7
8
  import { createLedgerSink } from "../../trace/ledger-sink.js";
8
9
  import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
9
10
  import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
11
+ import { buildCancelContext } from "../../run-cancel-context.js";
10
12
  import { recordResourceWindow } from "../../resource-window.js";
11
13
  import { redactSecrets } from "../../trace/redact.js";
12
14
  import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
@@ -47,6 +49,13 @@ async function handleTasksBody(req, res, url, ctx, miss) {
47
49
  const { prepareSpec, finalizeTaskResult } = ctx.legs;
48
50
  const reqState = ctx.req;
49
51
  const source = ctx.req.source;
52
+ const noteRunOutcome = (r) => {
53
+ if (!r?.status)
54
+ return;
55
+ reqState.runStatus = r.status;
56
+ if (r.errorCode)
57
+ reqState.runErrorCode = r.errorCode;
58
+ };
50
59
  if (req.method === "POST" && (url === "/v1/tasks" || url === "/v1/tasks/stream")) {
51
60
  const rawIdem = headerStr(req.headers["idempotency-key"]);
52
61
  const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, gatedPrincipal(req, deps.config)) : undefined;
@@ -88,6 +97,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
88
97
  return;
89
98
  }
90
99
  const earlyDurableTid = deps.runStore && prepared.spec.sessionId ? uuidv7() : undefined;
100
+ const streamLegStartedAt = Date.now();
91
101
  const detachOnDisconnect = headerStr(req.headers["x-detach-on-disconnect"]) === "true";
92
102
  if (detachOnDisconnect && earlyDurableTid === undefined) {
93
103
  sendError(res, 400, "request.precondition_unmet", "x-detach-on-disconnect requires a durable run (a run store + sessionId) — without one the detached result would be unqueryable");
@@ -178,6 +188,19 @@ async function handleTasksBody(req, res, url, ctx, miss) {
178
188
  let runRowSettled = false;
179
189
  let durableTaskId;
180
190
  let durableHeartbeat;
191
+ const mintCancelledResult = (errorMessage) => {
192
+ const ctx = durableTaskId === undefined ? undefined : buildCancelContext(durableTaskId, streamLegStartedAt);
193
+ return {
194
+ taskId: durableTaskId ?? "",
195
+ sessionId: prepared.spec.sessionId ?? "",
196
+ status: "failed",
197
+ result: "",
198
+ errorCode: "cancelled",
199
+ errorMessage,
200
+ stats: { turns: 0, tokens: 0 },
201
+ ...(ctx ? { cancelContext: ctx } : {}),
202
+ };
203
+ };
181
204
  let userMsgEntryId;
182
205
  let anchorPut = false;
183
206
  const putRewindAnchor = () => {
@@ -211,7 +234,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
211
234
  if (detachOnDisconnect && durableTaskId && deps.runStore) {
212
235
  const rs = deps.runStore;
213
236
  const tid = durableTaskId;
214
- ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: () => recordTurnActivity(tid) });
237
+ ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData) });
215
238
  usageKey = durableTaskId;
216
239
  if (usageKey)
217
240
  deps.modelUsage?.register(usageKey);
@@ -380,13 +403,14 @@ async function handleTasksBody(req, res, url, ctx, miss) {
380
403
  if (ev.type === "message_committed" && ev.role === "user")
381
404
  userMsgEntryId = ev.entryId;
382
405
  if (ev.type === "done") {
383
- finalResult = stripCheckpointToken(await graftParkToolCallId(ev.result, deps.checkpointStore));
406
+ finalResult = redactTerminalResult(stripCheckpointToken(await graftParkToolCallId(ev.result, deps.checkpointStore)));
384
407
  if (durableTaskId && finalResult)
385
408
  finalResult = { ...finalResult, taskId: durableTaskId };
386
409
  if (durableTaskId && cancelledViaVerb.has(durableTaskId) && finalResult?.status === "failed" && !finalResult.errorCode) {
387
410
  finalResult = { ...finalResult, errorCode: "cancelled" };
388
411
  }
389
412
  finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
413
+ noteRunOutcome(finalResult);
390
414
  putRewindAnchor();
391
415
  settleFleet(finalResult?.status ?? "completed", fleetRunResiduals(finalResult));
392
416
  if (ledgerSink && finalResult) {
@@ -530,7 +554,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
530
554
  const legWalltimeMs = typeof prepared.spec.limits?.maxWalltimeMs === "number" ? prepared.spec.limits.maxWalltimeMs : undefined;
531
555
  const approvalLeg = resolveApprovalLeg({
532
556
  streamApprovalOn,
533
- windowMs: deps.config.streamApproval?.windowMs ?? 0,
557
+ windowMs: deps.config.streamApproval?.windowMs,
534
558
  windowMarginMs: deps.config.streamAskWindowMarginMs,
535
559
  ...(legWalltimeMs !== undefined ? { legWalltimeMs } : {}),
536
560
  nowMonotonicMs: performance.now(),
@@ -618,13 +642,14 @@ async function handleTasksBody(req, res, url, ctx, miss) {
618
642
  }
619
643
  else {
620
644
  const dcErr = "stream client disconnected before completion (run aborted)";
621
- const dc = { taskId: durableTaskId, sessionId: prepared.spec.sessionId ?? "", status: "failed", errorCode: "cancelled", errorMessage: dcErr, stats: { turns: 0, tokens: 0 } };
645
+ const dc = mintCancelledResult(dcErr);
622
646
  if (ledgerSink && !ledgerTerminal) {
623
647
  ledgerTerminal = true;
624
648
  await ledgerSink.flush().catch(() => undefined);
625
649
  await ledgerSink.append("failed", { errorMessage: dcErr, errorCode: "cancelled" }).catch(() => undefined);
626
650
  }
627
651
  await deps.runStore.setTerminal(durableTaskId, "failed", dc, dcErr);
652
+ noteRunOutcome(dc);
628
653
  settleFleet("failed");
629
654
  }
630
655
  }
@@ -668,7 +693,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
668
693
  }
669
694
  else if (ac.signal.aborted) {
670
695
  const err = "stream aborted before completion (run cancelled)";
671
- const c = { taskId: durableTaskId, sessionId: prepared.spec.sessionId ?? "", status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
696
+ const c = mintCancelledResult(err);
672
697
  if (ledgerSink && !ledgerTerminal) {
673
698
  ledgerTerminal = true;
674
699
  await ledgerSink.flush().catch(() => undefined);
@@ -699,7 +724,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
699
724
  resp = await idemCache.run(idemKey, runStreamSubmitLeg, (r) => r.body?.status === "completed");
700
725
  }
701
726
  catch (e) {
702
- deps.logger?.error("stream_error", { closed, err: e instanceof Error ? e.message : String(e) });
727
+ deps.logger?.error("stream_error", { closed, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
703
728
  if (!closed)
704
729
  throw e;
705
730
  }
@@ -790,7 +815,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
790
815
  const retryAfterSec = result.errorCode === "memory.admission_required" && result.retryAfterMs !== undefined
791
816
  ? Math.max(1, Math.ceil(result.retryAfterMs / 1000))
792
817
  : undefined;
793
- return { status: resumeAtStatus, body: { errorCode: result.errorCode, error: result.errorMessage ?? "resume-at failed", ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) } };
818
+ return { status: resumeAtStatus, body: { errorCode: result.errorCode, error: redactErrorMessage(result.errorMessage) ?? "resume-at failed", ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) } };
794
819
  }
795
820
  if (result.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
796
821
  if (durableTaskId)
@@ -805,7 +830,8 @@ async function handleTasksBody(req, res, url, ctx, miss) {
805
830
  if (durableTaskId && deps.runStore)
806
831
  await deps.runStore.setTerminal(durableTaskId, result.status, stripCheckpointToken(result), result.errorMessage ?? null);
807
832
  finalizeTaskResult(result, principal, prepared.spec.objective, prepared.spec.sessionId);
808
- return { status: 200, body: withDiscoveryNotice(stripCheckpointToken(result)) };
833
+ noteRunOutcome(result);
834
+ return { status: 200, body: withDiscoveryNotice(redactTerminalResult(stripCheckpointToken(result))) };
809
835
  };
810
836
  const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400);
811
837
  sendSubmitResult(res, resp);
@@ -2,6 +2,10 @@ import { z } from "zod";
2
2
  import { getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus, callKeyOrdinal } from "@sema-agent/core";
3
3
  import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
4
4
  import { redactSecrets } from "../../trace/redact.js";
5
+ import { createHash } from "node:crypto";
6
+ import { redactErrorMessage, redactHead } from "../../observability/run-terminal-log.js";
7
+ const JOURNAL_PROJECTION_MEMO_CAP = 1024;
8
+ const journalProjectionMemo = new Map();
5
9
  import { sendJson, sendError, sseHeaders } from "../send.js";
6
10
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
7
11
  const WORKFLOWS_RE = /^\/v1\/workflows(?:\/([^/]+)(\/stream|\/journal)?)?$/;
@@ -68,16 +72,27 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
68
72
  const jLimit = Math.min(50, Math.max(1, Number(jq.get("limit") ?? 20) || 20));
69
73
  const jOffset = Math.max(0, Number(jq.get("offset") ?? 0) || 0);
70
74
  const MAX_ROW_BYTES = 64 * 1024;
71
- const firstLine = (t) => (t ? redactSecrets(t.split("\n")[0].slice(0, 300)) : undefined);
75
+ const firstLine = (t) => (t ? redactHead(t.split("\n")[0], 300) : undefined);
72
76
  const MAX_RESULT_CHARS = 2000;
73
- const projectResult = (callKey, r) => ({
77
+ const projectResult = (callKey, r) => {
78
+ const memoKey = `${wfId}\u0000${callKey}\u0000${createHash("sha256").update(JSON.stringify(r)).digest("hex")}`;
79
+ const hit = journalProjectionMemo.get(memoKey);
80
+ if (hit)
81
+ return hit;
82
+ const out = projectResultUncached(callKey, r);
83
+ if (journalProjectionMemo.size >= JOURNAL_PROJECTION_MEMO_CAP)
84
+ journalProjectionMemo.delete(journalProjectionMemo.keys().next().value);
85
+ journalProjectionMemo.set(memoKey, out);
86
+ return out;
87
+ };
88
+ const projectResultUncached = (callKey, r) => ({
74
89
  callKey,
75
90
  ordinal: callKeyOrdinal(callKey),
76
91
  status: r.status,
77
92
  ...(r.errorMessage ? { error: firstLine(r.errorMessage) } : {}),
78
93
  ...(r.result
79
94
  ? {
80
- result: redactSecrets(r.result.slice(0, MAX_RESULT_CHARS)),
95
+ result: redactHead(r.result, MAX_RESULT_CHARS),
81
96
  ...(r.result.length > MAX_RESULT_CHARS ? { resultTruncated: true, resultChars: r.result.length } : {}),
82
97
  }
83
98
  : {}),
@@ -310,7 +325,7 @@ function summarizeWorkflowDetail(run) {
310
325
  createdAt: run.createdAt,
311
326
  durationMs: durationOf(run),
312
327
  rev: run.rev,
313
- error: run.error,
328
+ ...(run.error !== undefined ? { error: redactErrorMessage(run.error) ?? "" } : {}),
314
329
  ...(run.result !== undefined ? { result: redactSecrets(run.result) } : {}),
315
330
  phases,
316
331
  ...(unphasedView ? { unphased: unphasedView } : {}),
@@ -18,7 +18,7 @@ import { type FleetEventBus } from "../fleet/fleet-bus.js";
18
18
  import type { ElicitationCoordinator } from "../elicitation.js";
19
19
  import type { QuestionCoordinator } from "../question.js";
20
20
  import { type ParkedAskRedeem, type ToolApprovalCoordinator } from "../tool-approval.js";
21
- import type { ApprovalAskAuditReadFace } from "../approval-ask-audit-store.js";
21
+ import type { ApprovalAskAuditReadFace, ApprovalLateSettleSink } from "../approval-ask-audit-store.js";
22
22
  import type { PlanCacheProbe } from "../plan-cache-probe.js";
23
23
  import type { Logger } from "../observability/logger.js";
24
24
  import type { Metrics } from "../observability/metrics.js";
@@ -210,6 +210,11 @@ export interface ServiceCoordinatorDeps {
210
210
  * SQL 车道恒缺席(那里有真 ask 店,[ref])—— 键缺席 = 本部署无此面,`[]` = 有面且无崩溃残账,
211
211
  * 两义与 `livePending` 同律可判别。 */
212
212
  approvalAskAudit?: ApprovalAskAuditReadFace;
213
+ /** [ref] 迟到人工结算的**写面**(与 {@link approvalAskAudit} 同一个店实例,两把窄口:读面喂
214
+ * `GET /v1/approvals` 的 `crashConverged`,写面只有 `recordLateSettle` 一个动词 —— 铸造/决议/腿闭
215
+ * 三点的属主是协调器,路由层拿到它们只会长出第二个写者)。在场判据与读面逐字同源(local 车道 ∧
216
+ * 审批面开着);缺席 ⇒ 两条 decide 腿**不写、不抛**,行为与修前逐字相同(SQL 车道恒缺席)。 */
217
+ approvalLateSettle?: ApprovalLateSettleSink;
213
218
  /** SVC-5 ([ref] CORE-5 #6): the process-local registry of STEERABLE workflow-agent handles
214
219
  * (`ctx.agentStream`) live on THIS replica. Enables POST /v1/workflows/:id/agents/:label/steer. See
215
220
  * workflow-agent-steer.ts for the handle-visibility seam status (the registry is real + ready; core does not
@@ -539,6 +544,11 @@ export interface ServiceGovernanceDeps {
539
544
  * pre-admission 读窗用(主力 /v1/runs 是 202 异步,core 进场拒发生在 202 之后永远变不成 429;
540
545
  * 这里是异步车道唯一的 429 出路)。enforcement 真源在 core(RunnerDeps 同店),此门是快速座。 */
541
546
  usageWindowStore?: import("@sema-agent/core").UsageWindowStore;
547
+ /** [ref] ④([ref]/[ref],core [ref] 合修):auto 模式自查读面的三件装配事实(席位在不在 / 合成后的
548
+ * per-principal caps resolver / 分类器配置路由)。`/v1/capabilities.permissionModeAuto.{armed,reason,model}`
549
+ * 只消费它,不自算 —— 席位与 resolver 都必须是递给 `RunnerDeps` 的**同一只**(main.ts 装配),否则又是
550
+ * 「诊断说亮、位说暗」。缺席(桩/一次性 CLI)⇒ 读面如实答 `deployment_incapable`。 */
551
+ autoModeFace?: import("../auto-mode-face.js").AutoModeFaceDeps;
542
552
  }
543
553
  /** **部署自述面**:只被 /health、/v1/capabilities 与提交前置门读的部署事实(多为 live getter,
544
554
  * 不是快照——/health 必须回答「此刻」)。 */