@sema-agent/server 7.56.0 → 7.58.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 (121) 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/boot/stores.js +12 -5
  30. package/dist/config-catalog.js +14 -5
  31. package/dist/config-center/http-client.js +7 -11
  32. package/dist/config-center/read-warnings.d.ts +20 -0
  33. package/dist/config-center/read-warnings.js +36 -0
  34. package/dist/config-provider.d.ts +3 -0
  35. package/dist/config-provider.js +4 -8
  36. package/dist/config-types.d.ts +41 -10
  37. package/dist/config.d.ts +21 -0
  38. package/dist/config.js +40 -5
  39. package/dist/hooks/hook-runner.js +8 -8
  40. package/dist/http/route-ctx.d.ts +75 -0
  41. package/dist/http/routes/a2a-serve.js +5 -3
  42. package/dist/http/routes/approvals-assistant.js +19 -6
  43. package/dist/http/routes/capabilities.js +21 -4
  44. package/dist/http/routes/memory-origin.d.ts +2 -2
  45. package/dist/http/routes/rules.js +3 -2
  46. package/dist/http/routes/runs.d.ts +0 -14
  47. package/dist/http/routes/runs.js +20 -8
  48. package/dist/http/routes/tasks.js +34 -8
  49. package/dist/http/routes/workflows.js +19 -4
  50. package/dist/http/server.d.ts +11 -1
  51. package/dist/http/server.js +215 -43
  52. package/dist/http/wire-types.d.ts +9 -4
  53. package/dist/leader/diffout.js +2 -1
  54. package/dist/leader/endpoint.js +45 -12
  55. package/dist/leader/fanout.js +6 -5
  56. package/dist/leader/leader.js +17 -14
  57. package/dist/leader/merge.js +17 -12
  58. package/dist/leader/planner.js +3 -2
  59. package/dist/leader/repair-oracle.js +6 -4
  60. package/dist/leader/repair-wire.js +5 -3
  61. package/dist/leader/wire.d.ts +30 -1
  62. package/dist/leader/wire.js +36 -19
  63. package/dist/main.js +43 -7
  64. package/dist/observability/err-text.d.ts +6 -0
  65. package/dist/observability/err-text.js +10 -0
  66. package/dist/observability/fail-open.d.ts +40 -0
  67. package/dist/observability/fail-open.js +19 -0
  68. package/dist/observability/metrics.js +2 -2
  69. package/dist/observability/run-terminal-log.d.ts +120 -0
  70. package/dist/observability/run-terminal-log.js +360 -0
  71. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  72. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  73. package/dist/permission-rule-vocab.d.ts +40 -0
  74. package/dist/permission-rule-vocab.js +17 -0
  75. package/dist/plugins/checkpoint-store-sql.d.ts +14 -0
  76. package/dist/plugins/checkpoint-store-sql.js +19 -3
  77. package/dist/plugins/file-run-store.d.ts +3 -34
  78. package/dist/plugins/file-run-store.js +19 -0
  79. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  80. package/dist/plugins/local-checkpoint-store.js +3 -0
  81. package/dist/plugins/mailbox-store-sql.d.ts +28 -9
  82. package/dist/plugins/mailbox-store-sql.js +72 -10
  83. package/dist/plugins/memory-run-store.d.ts +3 -15
  84. package/dist/plugins/memory-run-store.js +19 -0
  85. package/dist/plugins/permission-rule-store-file.js +8 -3
  86. package/dist/plugins/permission-rule-store-sql.js +16 -10
  87. package/dist/plugins/remote-scratchpad.js +3 -2
  88. package/dist/plugins/run-store-sql.d.ts +3 -42
  89. package/dist/plugins/run-store-sql.js +30 -1
  90. package/dist/plugins/store-backend.d.ts +1 -1
  91. package/dist/plugins/store-contracts.d.ts +66 -1
  92. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  93. package/dist/plugins/workflow-run-store-sql.js +10 -2
  94. package/dist/rules-consent.js +20 -13
  95. package/dist/run-cancel-context.d.ts +13 -0
  96. package/dist/run-cancel-context.js +15 -0
  97. package/dist/run-local.js +2 -2
  98. package/dist/runs.d.ts +4 -1
  99. package/dist/runs.js +41 -10
  100. package/dist/runtime-caps-resolver.d.ts +133 -17
  101. package/dist/runtime-caps-resolver.js +41 -3
  102. package/dist/task-settings.d.ts +57 -3
  103. package/dist/task-settings.js +78 -5
  104. package/dist/task-workflow.d.ts +32 -2
  105. package/dist/task-workflow.js +8 -3
  106. package/dist/tool-approval.d.ts +26 -3
  107. package/dist/tool-approval.js +96 -13
  108. package/dist/trace/core-keyset-guard.d.ts +7 -8
  109. package/dist/trace/engine-notice-wire.d.ts +1 -1
  110. package/dist/trace/engine-notice-wire.js +2 -0
  111. package/dist/trace/ledger-sink.d.ts +13 -4
  112. package/dist/trace/ledger-sink.js +14 -6
  113. package/dist/trace/project.d.ts +58 -3
  114. package/dist/trace/project.js +33 -1
  115. package/dist/trace/redact.d.ts +22 -11
  116. package/dist/trace/redact.js +660 -21
  117. package/dist/trace/sema-provenance.d.ts +35 -0
  118. package/dist/trace/sema-provenance.js +14 -0
  119. package/dist/turn-activity.d.ts +32 -2
  120. package/dist/turn-activity.js +29 -4
  121. package/package.json +3 -3
@@ -1,7 +1,9 @@
1
+ import { redactErrorMessage, redactLedgerEventData, redactTerminalResult } from "../observability/run-terminal-log.js";
1
2
  import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
2
3
  import { escapeLike } from "./sql-escape.js";
3
4
  import { pgSanitizeText } from "./pg-safe-json.js";
4
5
  import { parseJsonLenient as parseJson, toIso as iso } from "./sql-row-helpers.js";
6
+ import { notifyRunTerminal, runTerminalLogLevel } from "../observability/run-terminal-log.js";
5
7
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
6
8
  import { isDupKeyError } from "./sql-errors.js";
7
9
  export class SqlRunStore {
@@ -105,6 +107,7 @@ export class SqlRunStore {
105
107
  await this.db.query(this.q("UPDATE task_run SET updated_at = ? WHERE task_id = ? AND owner <=> ? AND status = 'running'", "UPDATE task_run SET updated_at = $1 WHERE task_id = $2 AND owner IS NOT DISTINCT FROM $3 AND status = 'running'"), [new Date(), taskId, owner]);
106
108
  }
107
109
  async appendEvent(taskId, seq, type, data) {
110
+ data = redactLedgerEventData(type, data);
108
111
  await this.db.query(this.q("INSERT INTO task_event (task_id, seq, type, data, ts) VALUES (?,?,?,?,?)", "INSERT INTO task_event (task_id, seq, type, data, ts) VALUES ($1,$2,$3,$4::jsonb,$5)"), [taskId, seq, type, data == null ? null : this.json(data), new Date()]);
109
112
  }
110
113
  async maxSeq(taskId) {
@@ -113,9 +116,12 @@ export class SqlRunStore {
113
116
  return m == null ? 0 : Number(m);
114
117
  }
115
118
  async setTerminal(taskId, status, result, error) {
119
+ result = redactTerminalResult(result);
120
+ error = error === null ? null : (redactErrorMessage(error) ?? null);
116
121
  const errorCode = result?.errorCode ?? null;
122
+ let observed;
117
123
  await this.tx(async (conn) => {
118
- await conn.query(this.q("UPDATE task_run SET status = ?, result = ?, error = ?, error_code = ?, updated_at = ? WHERE task_id = ? AND status IN ('running','suspended','needs_review')", "UPDATE task_run SET status = $1, result = $2::jsonb, error = $3, error_code = $4, updated_at = $5 WHERE task_id = $6 AND status IN ('running','suspended','needs_review')"), [
124
+ const w = await conn.query(this.q("UPDATE task_run SET status = ?, result = ?, error = ?, error_code = ?, updated_at = ? WHERE task_id = ? AND status IN ('running','suspended','needs_review')", "UPDATE task_run SET status = $1, result = $2::jsonb, error = $3, error_code = $4, updated_at = $5 WHERE task_id = $6 AND status IN ('running','suspended','needs_review')"), [
119
125
  status,
120
126
  result == null ? null : this.json(result),
121
127
  this.db.dialect === "tidb" ? error : error == null ? null : pgSanitizeText(error),
@@ -123,8 +129,31 @@ export class SqlRunStore {
123
129
  new Date(),
124
130
  taskId,
125
131
  ]);
132
+ if (w.affected > 0 && runTerminalLogLevel(status, errorCode ?? undefined) !== undefined) {
133
+ const { rows } = await conn.query(this.q("SELECT owner, session_id, created_at FROM task_run WHERE task_id = ?", "SELECT owner, session_id, created_at FROM task_run WHERE task_id = $1"), [taskId]);
134
+ const row = rows[0];
135
+ const createdAt = row ? Date.parse(iso(row.created_at)) : NaN;
136
+ observed = {
137
+ owner: row?.owner ?? null,
138
+ sessionId: String(row?.session_id ?? result?.sessionId ?? ""),
139
+ createdAt: Number.isFinite(createdAt) ? createdAt : undefined,
140
+ };
141
+ }
126
142
  await conn.query(this.q("DELETE FROM task_active WHERE task_id = ?", "DELETE FROM task_active WHERE task_id = $1"), [taskId]);
127
143
  });
144
+ if (observed) {
145
+ notifyRunTerminal({
146
+ runId: taskId,
147
+ sessionId: observed.sessionId,
148
+ owner: observed.owner,
149
+ status,
150
+ resultTaskId: result?.taskId,
151
+ model: result?.model,
152
+ errorCode: errorCode ?? undefined,
153
+ errorMessage: result?.errorMessage ?? error ?? undefined,
154
+ elapsedMs: observed.createdAt === undefined ? undefined : Math.max(0, Date.now() - observed.createdAt),
155
+ });
156
+ }
128
157
  }
129
158
  async setSuspended(taskId) {
130
159
  await this.db.query(this.q("UPDATE task_run SET status = 'suspended', updated_at = ? WHERE task_id = ? AND status IN ('running','suspended')", "UPDATE task_run SET status = 'suspended', updated_at = $1 WHERE task_id = $2 AND status IN ('running','suspended')"), [new Date(), taskId]);
@@ -95,7 +95,7 @@ export type ImageBake = TiDBImageBake | PgImageBake;
95
95
  /** [ref] 车4 件1:durable leader-run 登记表的双方言孪生(union 同族——TS 私有字段让具体类名义上不同)。 */
96
96
  export type LeaderRunStore = TiDBLeaderRunStore | PgLeaderRunStore;
97
97
  /** The FULL checkpoint store (core CheckpointStore + the service operator-queue/ctx methods listPending/
98
- * listByScope/findPendingTokenBySession/peekPendingScope/putCtx/getCtx/reapCtx) — the SQL twins carry them,
98
+ * listByScope/findPendingTokenBySession/peekPendingScope/peekScopeByToken/putCtx/getCtx/reapCtx) — the SQL twins carry them,
99
99
  * and the LOCAL lane now does too (core FileCheckpointStore + the service half — TOC plan-mode /
100
100
  * durable HITL work on one box). */
101
101
  export type CheckpointStoreFull = TiDBCheckpointStore | PgCheckpointStore | LocalCheckpointStore;
@@ -15,12 +15,77 @@
15
15
  * code should import from this file.
16
16
  */
17
17
  import type { TaskResult, TaskStatus } from "@sema-agent/core";
18
+ /**
19
+ * 取消便签里的**大脑相位**快照 —— core `BrainStatus` 帧经 `brainStatusEventData` 投影后的五键子集。
20
+ *
21
+ * 刻意**不含** `detail`(provider 自由文本,宽面)与 `retryInMs`/`retryInSec`(相对量,过期即失真;
22
+ * 绝对量 `retryAtMs` 才是可跨进程读的那个,core 顶注已裁)。`phase` 是这只快照的存在理由:没有它
23
+ * 就没有快照(不铸半只)。
24
+ */
25
+ export interface RunBrainStatusSnapshot {
26
+ phase: string;
27
+ attempt?: number;
28
+ maxRetries?: number;
29
+ /** 等待结束的**墙钟**时刻(发端进程钟域,非单调)—— 只当近似,别与本地单调计时器比。 */
30
+ retryAtMs?: number;
31
+ errClass?: string;
32
+ }
33
+ /**
34
+ * [ref] 件3(cli [ref] sema-bug4 (d) / server [ref]):**用户取消时的现场快照**。
35
+ *
36
+ * 病(取证 `audits/evidence/2026-09-03-model-failure-visibility/`):模型挂起 7 分 26 秒后用户放弃,
37
+ * 账本把这条 run 记成 `failed` + `errorCode:"cancelled"` + 「stream client disconnected before
38
+ * completion」—— 与「用户看了一眼不想跑了,秒按 Esc」**同形**。历史 314 条里 96 条 failed、其中 63 条
39
+ * 是 `user_disconnect`,「其中有多少条其实是模型挂死」在数据面**不可考**。
40
+ *
41
+ * 本结构是那一维判别材料:取消发生时,距离**本 run 上一条引擎事件**过了多久。
42
+ *
43
+ * 【语义边界(勿漂)】
44
+ * - 它**不**描述模型调用相位(首字节/重试在 core 的 stream-engine/with-retry 里,server 结构上看不见
45
+ * ——那半场是 [ref] 的 core 钩,别在这里编造)。它只说「server 这一侧多久没收到这条 run 的动静了」。
46
+ * - `lastEventKind`/`lastEventAgeMs` 的数据源是**同副本进程内**的 turn 活性登记(`turn-activity.ts`)。
47
+ * 跨副本 / 本副本重启后读不到 ⇒ 两键**诚实缺席**(缺席 = 「证不出」,不是「0ms 前刚活过」)。
48
+ * - `elapsedMs` 恒在场:run 起跑到取消的墙钟毫秒。
49
+ * - 分诊材料,不是状态机输入:任何门 / CAS / resume 判定都不许读它。
50
+ *
51
+ * 落点 = 终局 `result` blob 里的一个 additive 可缺席键({@link PersistedTaskResult}),**零新列**
52
+ * ——run 行的 `result` 本来就是整只 JSON 存(SQL 双方言同形),所以两个孪生都不需要 DDL 变更。
53
+ */
54
+ export interface RunCancelContext {
55
+ /** 最后一条进本 run durable 账本的事件类型(`text` / `tool_start` / `status` / …)。
56
+ * 🔴 它比 {@link RunCancelContext.lastEventAgeMs} **更容易缺席**(codex 交叉复审 R1-[medium],验真后
57
+ * 改的正是这条契约):打点方里有几席拿不到事件类型(core 的 `onActivity` 第五席、resume 腿的手动
58
+ * 打点),它们只推进「时刻」不带「词」。所以两键**不是**同在同缺 —— 有 age 无 kind 是合法读数,
59
+ * 意思是「本副本知道那一刻,但说不出那是条什么事件」。 */
60
+ lastEventKind?: string;
61
+ /** 距最后一条 durable 事件过了多久(ms,≥0)。同副本有活性登记就在场。 */
62
+ lastEventAgeMs?: number;
63
+ /** 本 run 账本尾**最后一条** `status`(BrainStatus)帧的五键快照([ref]/[ref])——「已重试几次 /
64
+ * 在等什么 / 什么错类」。这条帧 server 早就在透传与落账(`trace/project.ts` 的 `brainStatusEventData`),
65
+ * 取消时把最后一条摘下来当便签,**不需要 core 新钩**。sticky:后续 text/tool 事件不清它。
66
+ * 这条 run 从没出过 status 帧(或跨副本读不到)⇒ 缺席,恒不铸 null。 */
67
+ lastBrainStatus?: RunBrainStatusSnapshot;
68
+ /** run 起跑到本次取消的墙钟毫秒(≥0)。常态在场;行的 `createdAt` 坏到解析不出时缺席
69
+ * ——**绝不折成 0**(0 会被读成「刚起跑就取消」,与本结构要判别的那件事正好相反)。 */
70
+ elapsedMs?: number;
71
+ }
72
+ /**
73
+ * 落库/上 wire 的终局结果 = core 的 `TaskResult` **加上** server 自铸的可缺席侧记键。
74
+ *
75
+ * 今天只有一个成员({@link RunCancelContext});additive 且可缺席,所以旧行、旧消费端逐字不受影响
76
+ * (`TaskResult` 仍可原样赋给本型)。⚠️ 这不是给「往 core 结果里塞私货」开的口子:每加一个键都要能
77
+ * 回答「为什么它属于 run 账本而不属于 core 的任务结果」——`cancelContext` 的答案是它描述的是
78
+ * **server 侧的取消现场**,core 那一侧根本不知道有人按了取消。
79
+ */
80
+ export type PersistedTaskResult = TaskResult & {
81
+ cancelContext?: RunCancelContext;
82
+ };
18
83
  export interface RunRecord {
19
84
  taskId: string;
20
85
  sessionId: string;
21
86
  owner: string | null;
22
87
  status: "running" | TaskStatus;
23
- result: TaskResult | null;
88
+ result: PersistedTaskResult | null;
24
89
  error: string | null;
25
90
  /** Structured failure code (1.36/1.37) denormalized from the result for SQL queryability. */
26
91
  errorCode: string | null;
@@ -75,6 +75,11 @@ export type InboxWarn = typeof onWarnType;
75
75
  * with an explicit marker. Returns null only when even the skeleton is oversize (physics — caller keeps the
76
76
  * prior revision, matching the old behavior for that corner). Dialect-neutral (pure JS, no SQL) — shared
77
77
  * verbatim by both twins. */
78
+ /** S-107:workflow_run 行的 `error`(脚本抛出的 `err.message`,core 源头不脱)在**两条写腿**(put / update 常态 + oversize 降级)
79
+ * 同一口脱;`result` 由 core 完成时源头脱。幂等,不抛(超大 ⇒ 占位)。 */
80
+ export declare function withRedactedError<T extends {
81
+ error?: string | undefined;
82
+ }>(run: T): T;
78
83
  export declare function slimOversizeRun(run: WorkflowRun & {
79
84
  id: string;
80
85
  scope: string;
@@ -1,16 +1,23 @@
1
+ import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
1
2
  import { summarizeWorkflowRun, isTerminalWorkflowStatus, WorkflowRunStoreError } from "@sema-agent/core";
2
3
  import { foldKeyFamily, MAX_PENDING_PER_SESSION, PURGE_FENCE_MS, SERVED_FENCE_MS, } from "../orchestration/workflow-completion-inbox.js";
3
4
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
4
5
  import { isDupKeyError } from "./sql-errors.js";
5
6
  export const MAX_RUN_BLOB_BYTES = 4 * 1024 * 1024;
6
7
  const onWarnType = (msg, meta) => void [msg, meta];
8
+ export function withRedactedError(run) {
9
+ if (typeof run.error !== "string")
10
+ return run;
11
+ const error = redactErrorMessage(run.error) ?? "";
12
+ return error === run.error ? run : { ...run, error };
13
+ }
7
14
  export function slimOversizeRun(run, maxBytes) {
8
15
  const MARK = "…[truncated: run blob exceeded the store cap]";
9
16
  const cut = (s) => s.slice(0, 4000) + MARK;
10
17
  const step1 = {
11
18
  ...run,
12
19
  ...(typeof run.result === "string" && run.result.length > 4000 ? { result: cut(run.result) } : {}),
13
- ...(typeof run.error === "string" && run.error.length > 4000 ? { error: cut(run.error) } : {}),
20
+ ...(typeof run.error === "string" && run.error.length > 4000 ? { error: redactHead(run.error, 4000) + MARK } : {}),
14
21
  };
15
22
  let blob = JSON.stringify(step1);
16
23
  if (Buffer.byteLength(blob) <= maxBytes)
@@ -30,7 +37,7 @@ export class SqlWorkflowRunStore {
30
37
  return this.db.dialect === "tidb" ? tidb : pg;
31
38
  }
32
39
  async put(id, run) {
33
- const stored = { ...run, id, rev: run.rev ?? 0 };
40
+ const stored = { ...withRedactedError(run), id, rev: run.rev ?? 0 };
34
41
  try {
35
42
  await this.db.query(this.q("INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at_ms) VALUES (?,?,?,?,?,?,?)", "INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at_ms) VALUES ($1,$2,$3,$4,$5,$6,$7)"), [id, run.scope, run.status, JSON.stringify(stored), stored.rev, run.createdAt, run.endedAt ?? null]);
36
43
  }
@@ -51,6 +58,7 @@ export class SqlWorkflowRunStore {
51
58
  return run;
52
59
  }
53
60
  async update(id, scope, run, expect) {
61
+ run = withRedactedError(run);
54
62
  let blob = JSON.stringify({ ...run, id, scope });
55
63
  if (Buffer.byteLength(blob) > MAX_RUN_BLOB_BYTES) {
56
64
  this.onWarn?.("workflow_run_blob_oversize_slimmed", { id, scope, fullBytes: Buffer.byteLength(blob), capBytes: MAX_RUN_BLOB_BYTES });
@@ -80,7 +80,10 @@ function isSupportedRuleMatch(match) {
80
80
  switch (match) {
81
81
  case "exact":
82
82
  case "prefix":
83
+ case "wildcard":
83
84
  return true;
85
+ case "subpath":
86
+ return false;
84
87
  default: {
85
88
  const unknown = match;
86
89
  void unknown;
@@ -166,25 +169,28 @@ function screenLocalImportRow(row) {
166
169
  detail: "this project root carries control bytes (C0/DEL) — it is not a path, one SQL dialect refuses to store it at all, and it breaks the separator assumption this lane's comparison keys rest on",
167
170
  };
168
171
  }
172
+ const consistent = (derived) => {
173
+ if (derived.tool !== row.tool || derived.match !== row.match || derived.command !== row.command) {
174
+ return {
175
+ reason: "row_inconsistent",
176
+ detail: `this row's tool/match/command are derived facts of its rule text, and they disagree: the rule reads as ` +
177
+ `${derived.tool}/${derived.match}/${JSON.stringify(derived.command)} but the row says ${String(row.tool)}/${String(row.match)}/${JSON.stringify(row.command)}`,
178
+ };
179
+ }
180
+ return { rule: derived.rule, scope };
181
+ };
182
+ const written = parseAllowRuleText(row.rule);
183
+ if ("reject" in written)
184
+ return { reason: "rule_rejected", detail: `${written.reject.code}: ${written.reject.message}` };
185
+ if (written.rule.tool === "Read")
186
+ return consistent(written.rule);
169
187
  const pre = precheckCardRuleText(row.rule, row.command);
170
188
  if (pre === undefined) {
171
189
  return { reason: "rule_uncheckable", detail: "the rule text gate could not be asked about this row (its command is absent or not a string) — the row itself is malformed" };
172
190
  }
173
191
  if (!pre.ok)
174
192
  return { reason: "rule_rejected", detail: pre.message };
175
- const canonical = parseAllowRuleText(pre.canonicalRule);
176
- if ("reject" in canonical) {
177
- return { reason: "rule_rejected", detail: `the canonical spelling the engine minted does not re-parse: ${canonical.reject.message}` };
178
- }
179
- const derived = canonical.rule;
180
- if (derived.tool !== row.tool || derived.match !== row.match || derived.command !== row.command) {
181
- return {
182
- reason: "row_inconsistent",
183
- detail: `this row's tool/match/command are derived facts of its rule text, and they disagree: the rule reads as ` +
184
- `${derived.tool}/${derived.match}/${JSON.stringify(derived.command)} but the row says ${String(row.tool)}/${String(row.match)}/${JSON.stringify(row.command)}`,
185
- };
186
- }
187
- return { rule: derived.rule, scope };
193
+ return consistent(written.rule);
188
194
  }
189
195
  const LOCAL_IMPORT_LAYER_PATH = "local-import:rules";
190
196
  const CONTROL_AND_BIDI_DETECT_RE = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`);
@@ -241,6 +247,7 @@ export function createRuleConsentLane(stores, opts) {
241
247
  ...(input.toolCallId !== undefined ? { toolCallId: input.toolCallId } : {}),
242
248
  ...(input.boundInputHash !== undefined ? { boundInputHash: input.boundInputHash } : {}),
243
249
  ...(input.scope !== undefined ? { scope: input.scope } : {}),
250
+ ...(input.scope?.kind === "project" ? { cwd: input.scope.root } : {}),
244
251
  deps,
245
252
  });
246
253
  }
@@ -0,0 +1,13 @@
1
+ import type { RunCancelContext } from "./plugins/store-contracts.js";
2
+ /**
3
+ * @param taskId 这条 run 的账本 id(turn 活性登记的键)。
4
+ * @param startedAtMs 这条 run 起跑的墙钟时刻(epoch ms):取消腿手上有行就用行的 `createdAt`,
5
+ * 只有本腿起点时用本腿起点(两者在同一条 run 上相差 ms 级)。**非有限值**
6
+ * (坏 `createdAt` 解析出的 NaN)⇒ `elapsedMs` 诚实缺席,绝不折成 0
7
+ * ——0 会被读成「刚起跑就取消了」,与「模型挂死 8 分钟后用户放弃」正好相反。
8
+ * @param nowMs 取样时刻(缺省 `Date.now()`;测试注入用)。
9
+ * @returns 三键至少有一键可证时的快照;**一键都证不出** ⇒ `undefined`(写一只空对象等于在 wire 上
10
+ * 断言「我看过现场,什么都没有」,而事实是「我什么都没看见」)。
11
+ */
12
+ export declare function buildCancelContext(taskId: string, startedAtMs: number, nowMs?: number): RunCancelContext | undefined;
13
+ //# sourceMappingURL=run-cancel-context.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { readTurnActivity } from "./turn-activity.js";
2
+ export function buildCancelContext(taskId, startedAtMs, nowMs = Date.now()) {
3
+ const activity = readTurnActivity(taskId);
4
+ const age = activity === undefined ? undefined : Math.max(0, nowMs - activity.at);
5
+ const elapsedMs = Number.isFinite(startedAtMs) ? Math.max(0, nowMs - startedAtMs) : undefined;
6
+ if (age === undefined && elapsedMs === undefined)
7
+ return undefined;
8
+ return {
9
+ ...(activity?.kind !== undefined ? { lastEventKind: activity.kind } : {}),
10
+ ...(age !== undefined ? { lastEventAgeMs: age } : {}),
11
+ ...(activity?.brainStatus !== undefined ? { lastBrainStatus: activity.brainStatus } : {}),
12
+ ...(elapsedMs !== undefined ? { elapsedMs } : {}),
13
+ };
14
+ }
15
+ //# sourceMappingURL=run-cancel-context.js.map
package/dist/run-local.js CHANGED
@@ -14,7 +14,7 @@ import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js
14
14
  import { taskWallClockSec } from "./task-workflow.js";
15
15
  import { applyCatalogToSource } from "./capabilities/center-prompts.js";
16
16
  import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
17
- import { loadConfig, logConfigDiagnostics, numEnv } from "./config.js";
17
+ import { loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
18
18
  import { createConfigProvider } from "./config-provider.js";
19
19
  import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
20
20
  import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
@@ -445,7 +445,7 @@ export async function runLocal(argv, deps = {}) {
445
445
  }
446
446
  const mention = parseModelMention(args.objective, Object.keys(config.models));
447
447
  const scope = args.user ?? "local";
448
- const taskTimeoutSec = Math.max(0, Math.floor(numEnv("TASK_TIMEOUT_SEC", "0")));
448
+ const taskTimeoutSec = Math.floor(parseNumOrFailNonNegative("TASK_TIMEOUT_SEC", process.env.TASK_TIMEOUT_SEC || "0"));
449
449
  const timeoutSec = taskWallClockSec(taskTimeoutSec, false, scenarioName === "discuss");
450
450
  const mcp = mcpForScenario(config.mcpServers, scenarioName);
451
451
  const a2a = a2aForScenario(config.a2aPeers, scenarioName);
package/dist/runs.d.ts CHANGED
@@ -308,10 +308,13 @@ promptManifests?: PromptManifestTracker,
308
308
  * `streamApprovalOn` = 协议上场判据(`resolveStreamApprovalGate`,由路由层求值后传进来 —— 本函数是纯
309
309
  * 执行腿,不该自己读 backend/config)。为假 ⇒ 只接既有四键 ctx(呈卡口/腿轴都不接),本腿字节逐字不变。
310
310
  */
311
+ /** `windowMs` 收成 `number | undefined`([ref] + codex r1-[medium]):`undefined` = **这份装配没有
312
+ * `streamApproval` 段**(stub-harness),不是「运维显式关窗」—— 两者自 [ref] 起结局不同(后者即答
313
+ * 无人值守终局),路由层因此不许再折算,见 `resolveApprovalLeg` 的 `windowMs` 域注。 */
311
314
  approval?: {
312
315
  coordinator: ToolApprovalCoordinator;
313
316
  streamApprovalOn: boolean;
314
- windowMs: number;
317
+ windowMs: number | undefined;
315
318
  windowMarginMs: number;
316
319
  },
317
320
  /** [ref]:park 投影腿的 checkpoint 读口(`deps.checkpointStore`)。在场时,durable park 的
package/dist/runs.js CHANGED
@@ -1,15 +1,18 @@
1
1
  import { runWithVerification, runCascade, defaultTaskRegistry, DURABLE_AGENT_HANDLE_RE } from "@sema-agent/core";
2
+ import { redactErrorMessage } from "./observability/run-terminal-log.js";
2
3
  import { withPrincipal } from "./observability/principal-context.js";
3
4
  import { redactSecrets } from "./trace/redact.js";
4
5
  import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "./trace/project.js";
5
6
  import { createLedgerSink } from "./trace/ledger-sink.js";
6
7
  import { registerEngineNoticeLeg } from "./trace/engine-notice-wire.js";
7
8
  import { recordTurnActivity } from "./turn-activity.js";
9
+ import { buildCancelContext } from "./run-cancel-context.js";
8
10
  import { createApprovalCardEmitter, resolveApprovalLeg } from "./tool-approval.js";
9
11
  import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
10
12
  import { defaultSubagentTailBus, projectTailFrame } from "./fleet/subagent-tail-bus.js";
11
13
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
12
14
  import { recordFailOpen } from "./observability/fail-open.js";
15
+ import { errorText as errText } from "./observability/err-text.js";
13
16
  import { LIVENESS_HEARTBEAT_MS } from "./config-invariants.js";
14
17
  export async function backgroundAgentOutput(registry, handle, access, agentStore) {
15
18
  const row = registry.getAccessibleTask(handle, access);
@@ -162,7 +165,10 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
162
165
  const preemptCtrl = new AbortController();
163
166
  if (preemptEligible)
164
167
  preemptable?.set(taskId, preemptCtrl);
165
- const cancelledResult = () => ({ taskId, sessionId: spec.sessionId, status: "failed", errorCode: "cancelled", errorMessage: "cancelled by user", stats: { turns: 0, tokens: 0 } });
168
+ const cancelledResult = () => {
169
+ const ctx = buildCancelContext(taskId, startedAt);
170
+ return { taskId, sessionId: spec.sessionId, status: "failed", errorCode: "cancelled", errorMessage: "cancelled by user", stats: { turns: 0, tokens: 0 }, ...(ctx ? { cancelContext: ctx } : {}) };
171
+ };
166
172
  const recordDone = (result, status) => {
167
173
  metrics?.addGauge("runs_active", -1);
168
174
  metrics?.inc("tasks_total", { status });
@@ -178,7 +184,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
178
184
  const owner = principal ?? null;
179
185
  const approvalLeg = resolveApprovalLeg({
180
186
  streamApprovalOn: approval?.streamApprovalOn === true,
181
- windowMs: approval?.windowMs ?? 0,
187
+ windowMs: approval?.windowMs,
182
188
  windowMarginMs: approval?.windowMarginMs ?? 0,
183
189
  ...(typeof spec.limits?.maxWalltimeMs === "number" ? { legWalltimeMs: spec.limits.maxWalltimeMs } : {}),
184
190
  nowMonotonicMs: performance.now(),
@@ -221,7 +227,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
221
227
  metrics?.addGauge("runs_active", -1);
222
228
  metrics?.inc("tasks_total", { status: "needs_review" });
223
229
  };
224
- const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys, onActivity: () => recordTurnActivity(taskId) });
230
+ const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys, onActivity: (kind, statusData) => recordTurnActivity(taskId, kind, statusData) });
225
231
  const append = (type, data) => sink.append(type, data);
226
232
  const flush = () => sink.flush();
227
233
  const flushModelUsage = () => appendModelUsageDelta(append, modelUsage, taskId);
@@ -410,18 +416,33 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
410
416
  : withApproval();
411
417
  });
412
418
  if (!reached && cancelCtrl.signal.aborted) {
413
- await flush();
419
+ try {
420
+ await flush();
421
+ }
422
+ catch (e) {
423
+ recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=flush-clean-cancel ${errText(e)}`);
424
+ }
414
425
  const c = cancelledResult();
415
- await append("failed", { errorMessage: c.errorMessage ?? "cancelled by user", errorCode: "cancelled" });
426
+ try {
427
+ await append("failed", { errorMessage: c.errorMessage ?? "cancelled by user", errorCode: "cancelled" });
428
+ }
429
+ catch (e) {
430
+ recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-clean-cancel ${errText(e)}`);
431
+ }
416
432
  reached = { kind: "done", result: c };
417
433
  await runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null);
418
434
  recordDone(c, "cancelled");
419
435
  }
420
436
  }
421
437
  catch (err) {
422
- const message = err instanceof Error ? err.message : String(err);
438
+ const message = errText(err);
423
439
  try {
424
- await flush();
440
+ try {
441
+ await flush();
442
+ }
443
+ catch (e) {
444
+ recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=flush-catch ${errText(e)}`);
445
+ }
425
446
  if (reached?.kind === "done") {
426
447
  await runStore.setTerminal(taskId, reached.result.status, reached.result, reached.result.errorMessage ?? null);
427
448
  }
@@ -433,12 +454,22 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
433
454
  }
434
455
  else if (cancelCtrl.signal.aborted) {
435
456
  const c = cancelledResult();
436
- await append("failed", { errorMessage: c.errorMessage ?? "cancelled by user", errorCode: "cancelled" });
457
+ try {
458
+ await append("failed", { errorMessage: c.errorMessage ?? "cancelled by user", errorCode: "cancelled" });
459
+ }
460
+ catch (e) {
461
+ recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-thrown-cancel ${errText(e)}`);
462
+ }
437
463
  await runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null);
438
464
  }
439
465
  else {
440
- const safeMsg = redactSecrets(message);
441
- await append("failed", { errorMessage: safeMsg });
466
+ const safeMsg = redactErrorMessage(message) ?? message;
467
+ try {
468
+ await append("failed", { errorMessage: safeMsg });
469
+ }
470
+ catch (e) {
471
+ recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-failed ${errText(e)}`);
472
+ }
442
473
  await runStore.setTerminal(taskId, "failed", null, safeMsg);
443
474
  }
444
475
  }