@sema-agent/server 7.69.0 → 7.71.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 (107) hide show
  1. package/README.md +4 -1
  2. package/README.zh-CN.md +4 -1
  3. package/USAGE.md +109 -10
  4. package/dist/approval-ask-machine.d.ts +1 -1
  5. package/dist/approval-card.d.ts +3 -2
  6. package/dist/approval-content-kind.d.ts +13 -7
  7. package/dist/approval-content-kind.js +3 -1
  8. package/dist/approval-reconciler.d.ts +0 -54
  9. package/dist/approval-reconciler.js +2 -2
  10. package/dist/approval.js +3 -2
  11. package/dist/bench/s1/live-deps.js +2 -0
  12. package/dist/boot/execution-env.js +12 -2
  13. package/dist/boot/memory-consolidation.d.ts +2 -1
  14. package/dist/boot/parked-revive-gate.d.ts +2 -0
  15. package/dist/boot/parked-revive-gate.js +15 -10
  16. package/dist/boot/resolve-spec.js +8 -3
  17. package/dist/boot/runner-deps.d.ts +50 -46
  18. package/dist/boot/runner-deps.js +12 -10
  19. package/dist/brain.d.ts +3 -1
  20. package/dist/brain.js +2 -1
  21. package/dist/budget.js +3 -2
  22. package/dist/config-catalog.d.ts +31 -2
  23. package/dist/config-catalog.js +44 -24
  24. package/dist/config-types.d.ts +31 -1
  25. package/dist/config-types.js +1 -0
  26. package/dist/config.js +15 -3
  27. package/dist/deployment-governance.d.ts +6 -3
  28. package/dist/deployment-governance.js +2 -1
  29. package/dist/execution-lane-caps.d.ts +51 -37
  30. package/dist/execution-lane-caps.js +23 -0
  31. package/dist/fleet/fleet-bus.d.ts +22 -1
  32. package/dist/fleet/fleet-bus.js +2 -1
  33. package/dist/http/routes/approvals-assistant.js +3 -12
  34. package/dist/http/routes/diagnostics.js +4 -0
  35. package/dist/http/routes/runs.js +2 -3
  36. package/dist/http/routes/tasks.js +22 -18
  37. package/dist/http/server.js +21 -7
  38. package/dist/index.d.ts +1 -1
  39. package/dist/leader/wire.js +4 -3
  40. package/dist/main.js +6 -4
  41. package/dist/memory-operator-faces.d.ts +1 -1
  42. package/dist/memory-scope.d.ts +24 -0
  43. package/dist/memory-scope.js +12 -3
  44. package/dist/observability/metrics.d.ts +4 -6
  45. package/dist/observability/metrics.js +5 -1
  46. package/dist/observability/permission-rule-events.d.ts +20 -0
  47. package/dist/observability/permission-rule-events.js +28 -0
  48. package/dist/observability/run-terminal-log.d.ts +13 -22
  49. package/dist/observability/run-terminal-log.js +187 -47
  50. package/dist/observability/tool-trace.d.ts +12 -0
  51. package/dist/observability/tool-trace.js +5 -1
  52. package/dist/orchestration/workflow-agent-session-index.d.ts +85 -2
  53. package/dist/orchestration/workflow-agent-session-index.js +38 -5
  54. package/dist/parked-decide.d.ts +40 -1
  55. package/dist/parked-decide.js +19 -2
  56. package/dist/plugins/checkpoint-store-sql.js +5 -5
  57. package/dist/plugins/local-checkpoint-store.js +3 -3
  58. package/dist/plugins/remote-env-adb.d.ts +11 -0
  59. package/dist/plugins/remote-env-adb.js +2 -0
  60. package/dist/plugins/remote-env-device.d.ts +9 -0
  61. package/dist/plugins/remote-env-device.js +3 -0
  62. package/dist/plugins/remote-env-e2b.d.ts +11 -0
  63. package/dist/plugins/remote-env-e2b.js +2 -0
  64. package/dist/plugins/remote-env-host.d.ts +11 -0
  65. package/dist/plugins/remote-env-host.js +2 -0
  66. package/dist/plugins/remote-env-k8s.d.ts +11 -0
  67. package/dist/plugins/remote-env-k8s.js +2 -0
  68. package/dist/plugins/remote-env-local-docker.d.ts +11 -0
  69. package/dist/plugins/remote-env-local-docker.js +2 -0
  70. package/dist/plugins/remote-env-ssh.d.ts +11 -0
  71. package/dist/plugins/remote-env-ssh.js +2 -0
  72. package/dist/plugins/store-backend.d.ts +36 -13
  73. package/dist/plugins/store-backend.js +12 -3
  74. package/dist/plugins/store-contracts.d.ts +54 -18
  75. package/dist/plugins/store-contracts.js +17 -30
  76. package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
  77. package/dist/plugins/workflow-journal-store-sql.js +3 -10
  78. package/dist/plugins/workflow-run-store-sql.d.ts +31 -9
  79. package/dist/plugins/workflow-run-store-sql.js +28 -1
  80. package/dist/posture-source.d.ts +23 -3
  81. package/dist/posture-source.js +1 -1
  82. package/dist/read-face-posture.d.ts +8 -1
  83. package/dist/read-face-posture.js +1 -0
  84. package/dist/run-cancel-context.d.ts +50 -1
  85. package/dist/run-cancel-context.js +22 -0
  86. package/dist/run-local.d.ts +14 -6
  87. package/dist/run-local.js +4 -1
  88. package/dist/runs.d.ts +0 -51
  89. package/dist/runs.js +40 -25
  90. package/dist/task-a2a.js +2 -2
  91. package/dist/task-mcp.js +2 -2
  92. package/dist/task-settings.d.ts +21 -8
  93. package/dist/task-settings.js +2 -2
  94. package/dist/tool-approval.d.ts +14 -8
  95. package/dist/tool-approval.js +3 -3
  96. package/dist/trace/core-keyset-guard.d.ts +135 -11
  97. package/dist/trace/engine-notice-wire.d.ts +1 -1
  98. package/dist/trace/engine-notice-wire.js +1 -0
  99. package/dist/trace/ledger-events.d.ts +22 -2
  100. package/dist/trace/ledger-events.js +6 -1
  101. package/dist/trace/ledger-sink.d.ts +10 -8
  102. package/dist/trace/ledger-sink.js +10 -2
  103. package/dist/trace/project.d.ts +24 -6
  104. package/dist/trace/project.js +5 -3
  105. package/dist/trace/redact.d.ts +28 -0
  106. package/dist/trace/redact.js +58 -30
  107. package/package.json +3 -3
@@ -71,7 +71,8 @@ export type ServiceFileHistoryStore = FileHistoryStore & {
71
71
  /** SVC-2 (core CORE-7) WorkflowJournalStore — core's interface PLUS the service `deleteByRun` (GC extra). tidb/pg =
72
72
  * durable SQL twins (cross-replica resume journal); local = the service {@link FileWorkflowJournalStore}
73
73
  * (single-box but RESTART-durable crash-safe JSONL, replacing core's InMemory). The shared equivalence suite keeps
74
- * all three byte-matched (scope guard / idempotent-per-ordinal / oversize skip-journal). */
74
+ * all three byte-matched (scope guard / idempotent-per-ordinal / oversize **refusal** —— S-194 / core 7.12.0
75
+ * [ref] 起两臂一律抛 `workflow.journal_oversize`,判长口径归 core `assertJournalEntryFits` 单一属主). */
75
76
  export type ServiceWorkflowJournalStore = WorkflowJournalStore & {
76
77
  /** codex R1-H2(journal 读面界):分页+单行字节门的投影读——SQL 端 LIMIT/OFFSET 定行数、LENGTH 门
77
78
  * 定单行(超 maxResultBytes 的行 resultJson=null 只回 resultBytes,读面标 truncated)。HTTP 读面
@@ -115,6 +116,22 @@ export type ToolResultStoreFull = ToolResultStore & {
115
116
  export type BreakerStateStore = TiDBBreakerState | PgBreakerState;
116
117
  export type CostQuotaStore = TiDBCostQuota | PgCostQuota;
117
118
  export type RateLimiterStore = TiDBRateLimiter | PgRateLimiter;
119
+ /**
120
+ * S-215 —— capture opt-out **记录载体的按平面取用座**。形与 core 的
121
+ * `RunnerDeps.memoryCaptureRecordStore`(`(plane: { controlDir }) => SessionCaptureRecordStore`)同构,
122
+ * 多一个**不指名平面**的缺省臂:`src()` = 「本部署记忆引擎自己那一只平面」。
123
+ *
124
+ * 🔴 为什么是工厂而不是一只实例(S-215 codex r1 [high] 验真后改形):core 的记忆平面**不止一个**
125
+ * (`prepare-memory.js:162` 的 personal 平面自带 `derivePersonalControlDir(engineRoot)`,与 project 平面的
126
+ * controlDir 不是同一只)。返回单只实例 ⇒ server 把所有平面折到一只目录上,于是升级前落在 personal
127
+ * 控制面的旧 opt-out 记录在升级后读作**缺席**=采集恢复(隐私面的静默 fail-open)。按平面取用后,
128
+ * file 载体与 core 的缺省**逐字节同址**,唯一的变化是这个席位现在**被声明了**(core 的能力信号要的就是它)。
129
+ * SQL 载体**刻意忽略** plane:它按 session_id 键控、跨平面全局 —— opt-out 是**会话**事实而不是平面事实
130
+ * (逐字理由见 boot/runner-deps.ts 该席的注)。
131
+ */
132
+ export type SessionCaptureRecordSource = (plane?: {
133
+ controlDir: string;
134
+ }) => SessionCaptureRecordStore;
118
135
  export interface StoreBackend {
119
136
  /** Protocol-truth kind (clay 2026-07-06 wording): "mysql" = any MySQL-protocol server (MySQL / TiDB /
120
137
  * MariaDB) behind the one mysql2 pool — the startup summary prints durable(mysql) for all of them. */
@@ -185,19 +202,25 @@ export interface StoreBackend {
185
202
  * 「没有行 ⇒ 代码缺省 allow」—— 那是**放宽面**上的静默降级(permissionRule 那条禁令的同一理由)。
186
203
  * File 形候真实的 local+governed 部署需求出现再落(候需求档)。 */
187
204
  memoryOptOutGrant(): MemoryOptOutGrantStore | undefined;
188
- /** [ref]②([ref] §2.1b,core 7.0.2 [ref] 件2):会话 capture opt-out **记录**的 SQL 载体
189
- * (`session_capture_optout`)—— core `RunnerDeps.memoryCaptureRecordStore` /
190
- * `MemoryEngineOptions.captureRecordStore` 的持久层(Promise 形三腿)。与 §3.1 的**授权表**不是一个轴:
191
- * 那张管「谁**可以** opt-out」(operator 写的 verdict),这张管「谁**已经** opt-out」(用户写的一次性
192
- * 单向记录)。
205
+ /** [ref]②([ref] §2.1b,core 7.0.2 [ref] 件2)/ S-215:会话 capture opt-out **记录**的载体 ——
206
+ * core `RunnerDeps.memoryCaptureRecordStore` / `MemoryEngineOptions.captureRecordStore` 的持久层。
207
+ * 与 §3.1 的**授权表**不是一个轴:那张管「谁**可以** opt-out」(operator 写的 verdict),这张管
208
+ * 「谁**已经** opt-out」(用户写的一次性单向记录)。
209
+ *
210
+ * 返回的是**按平面取用的源**({@link SessionCaptureRecordSource}),不是一只实例 —— core 的记忆平面
211
+ * 不止一只,折平会挪动旧记录的地址(理由全文见该类型的头注)。
212
+ * 载体按车道分形,但**三条车道都供**(S-215 起):SQL 两支 = 双方言 twin(`session_capture_optout`,
213
+ * Promise 形三腿,池即依托 ⇒ 无条件供,`plane` 刻意忽略);`local` = core 的控制面文件三腿
214
+ * `fileSessionCaptureRecordStore(controlDir)` 按平面现造,依托是**记忆引擎的控制面目录** ⇒ 记忆面暗的
215
+ * 部署上该平面不存在,于是这一支**随引擎接线门缺席**(唯一的 `undefined` 成因,逐字理由见 LocalBackend 的实装注)。
193
216
  *
194
- * 🔴 **可选**(与 memoryOptOutGrant 同族):SQL 后端 = 双方言 twin;`local` 车道 **undefined**,且缺席是
195
- * 一个真答案 —— 缺席时 core 用它的控制面文件三腿(`fileSessionCaptureRecordStore`),单机部署上那正是
196
- * 重启可存活的正确载体(383 §2.1b:「absent = 文件三腿,单进程/单机字节不变」)。SQL 载体要解决的病
197
- * 只在无状态多副本形:文件记录对 resume 副本不可见,「record not found」恰朝**继续采集**方向坏。
198
- * 刻意不给 local 一个 SQL/内存 twin:内存形会在重启时把一条隐私记录静默丢掉(比文件三腿严格更差),
199
- * 文件形 core 已自带。 */
200
- sessionCaptureRecords(): SessionCaptureRecordStore | undefined;
217
+ * 🔴 修前 local 恒 `undefined` 且注称「缺席是一个真答案」—— 那句话在**事实层面**成立(单机的正确载体
218
+ * 确实就是文件三腿)、在**接线层面**是假的:core 的 `captureCarrierUnsupported`
219
+ * (core 7.13.0 `prepare-task.js:497`)判的是「这个席位在不在」而不是「文件形能不能用」,叠上
220
+ * `isRemoteExecutionEnv` 的接口形鸭子判(`remote-env.js:21`)把本仓 host lane 一并算作 remote,
221
+ * 于是单机默认部署上 `memoryCapture:"off"` 两条 ingress 结构性不可达(B-085)。
222
+ * 刻意仍不给 local 一个**内存** twin:内存形会在重启时把一条隐私记录静默丢掉(比文件三腿严格更差)。 */
223
+ sessionCaptureRecords(): SessionCaptureRecordSource | undefined;
201
224
  /** S-1 legacy-epoch discriminant (core `RunnerDeps.legacyRewindBoundaryProbe`): does the RETIRED whole-tree
202
225
  * snapshot epoch hold a boundary for (sessionId, entryId)? Reads the legacy `snapshot_manifest` table.
203
226
  * SQL backends only (local never had a whole-tree epoch table). Diagnostic input on the refusal path —
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { AdoptionError, FileStorageBackend, FileSessionRepo } from "@sema-agent/core";
4
4
  import { FileWorkflowJournalStore } from "@sema-agent/core";
5
+ import { fileSessionCaptureRecordStore } from "@sema-agent/core";
5
6
  import { TiDBOutcomeLedger, PgOutcomeLedger } from "./outcome-ledger-sql.js";
6
7
  import { FileOutcomeSink } from "./file-outcome-sink.js";
7
8
  import { TiDBWorkflowRunStore, TiDBWorkflowCompletionInbox, TiDBWorkflowNotifyJournalStore, TiDBWorkflowAgentSessionIndex, PgWorkflowRunStore, PgWorkflowCompletionInbox, PgWorkflowNotifyJournalStore, PgWorkflowAgentSessionIndex } from "./workflow-run-store-sql.js";
@@ -20,6 +21,7 @@ import { TiDBSessionPolicyStore, PgSessionPolicyStore } from "./session-policy-s
20
21
  import { TiDBWorkflowJournalStore, PgWorkflowJournalStore } from "./workflow-journal-store-sql.js";
21
22
  import { TiDBFileHistoryStore, PgFileHistoryStore, FILE_HISTORY_BLOB_TABLE, FILE_HISTORY_BLOB_KEY_PREFIX } from "./file-history-store-sql.js";
22
23
  import { MinioBlobBackend, SqlBlobBackend } from "./blob-backend.js";
24
+ import { fileMemoryControlDirFor } from "../memory-scope.js";
23
25
  import { createTidbPool, ensureSchema as ensureTidbSchema } from "./tidb-pool.js";
24
26
  import { createPgPool, ensurePgSchema, pgPoolOptions } from "./pg-pool.js";
25
27
  import { TiDBRunStore, PgRunStore } from "./run-store-sql.js";
@@ -136,7 +138,8 @@ class TiDBBackend {
136
138
  sessionPolicy() { return new TiDBSessionPolicyStore(this.pool); }
137
139
  fileHistory() { return new TiDBFileHistoryStore(this.pool, historyBlobBackend(this.config, "tidb", this.pool), this.config.fileHistory); }
138
140
  memoryOptOutGrant() { return new TiDBMemoryOptOutGrantStore(this.pool); }
139
- sessionCaptureRecords() { return new TiDBSessionCaptureRecordStore(this.pool); }
141
+ sessionCaptureRecords() { return () => (this.captureRecordsInst ??= new TiDBSessionCaptureRecordStore(this.pool)); }
142
+ captureRecordsInst;
140
143
  legacyRewindBoundaryProbe(query) { return legacyProbeImpl("tidb", this.pool, query); }
141
144
  legacySnapshotBlobSweep() { return legacySweepImpl(this.config, "tidb", this.pool); }
142
145
  legacySnapshotPurge(sessionId) { return legacyPurgeImpl("tidb", this.pool, sessionId); }
@@ -183,7 +186,8 @@ class PgBackend {
183
186
  sessionPolicy() { return new PgSessionPolicyStore(this.pool); }
184
187
  fileHistory() { return new PgFileHistoryStore(this.pool, historyBlobBackend(this.config, "pg", this.pool), this.config.fileHistory); }
185
188
  memoryOptOutGrant() { return new PgMemoryOptOutGrantStore(this.pool); }
186
- sessionCaptureRecords() { return new PgSessionCaptureRecordStore(this.pool); }
189
+ sessionCaptureRecords() { return () => (this.captureRecordsInst ??= new PgSessionCaptureRecordStore(this.pool)); }
190
+ captureRecordsInst;
187
191
  legacyRewindBoundaryProbe(query) { return legacyProbeImpl("pg", this.pool, query); }
188
192
  legacySnapshotBlobSweep() { return legacySweepImpl(this.config, "pg", this.pool); }
189
193
  legacySnapshotPurge(sessionId) { return legacyPurgeImpl("pg", this.pool, sessionId); }
@@ -259,6 +263,10 @@ class LocalBackend {
259
263
  this.sendFileLedgerStore = new FileSendFileLedger(this.fileBackend.root);
260
264
  this.workflowJournalStore = new FileWorkflowJournalStore(this.fileBackend.root);
261
265
  this.outcomeSinkInst = new FileOutcomeSink(this.fileBackend.root);
266
+ const ownControlDir = fileMemoryControlDirFor(config);
267
+ this.captureRecordsSource = ownControlDir === undefined
268
+ ? undefined
269
+ : (plane) => fileSessionCaptureRecordStore(plane?.controlDir ?? ownControlDir);
262
270
  }
263
271
  async ensureSchema() { }
264
272
  async close() {
@@ -304,7 +312,8 @@ class LocalBackend {
304
312
  sessionPolicy() { return this.fileBackend.sessionPolicyStore; }
305
313
  fileHistory() { return this.fileBackend.fileHistoryStore; }
306
314
  memoryOptOutGrant() { return undefined; }
307
- sessionCaptureRecords() { return undefined; }
315
+ sessionCaptureRecords() { return this.captureRecordsSource; }
316
+ captureRecordsSource;
308
317
  workflowJournal() { return this.workflowJournalStore; }
309
318
  outcomeSink() { return this.outcomeSinkInst; }
310
319
  session() { return this.sessionStore; }
@@ -108,27 +108,65 @@ export interface RunRecord {
108
108
  updatedAt: string;
109
109
  }
110
110
  /**
111
- * 「这一行已经走到**终局**了吗」——`RunRecord["status"]`(= `"running" | TaskStatus`)的**穷举**判据,
112
- * 与类型定义同住一个文件(词表的属主 = 判据的属主)。
111
+ * `RunRecord["status"]`(= `"running" | TaskStatus`)的**唯一分类表** —— 词表的属主 = 判据的属主,
112
+ * 与类型定义同住一个文件。四个类:
113
+ * · `live` —— 还在跑;
114
+ * · `succeeded` —— 成功终局;
115
+ * · `unsuccessful` —— **非成功**终局。两个词:`failed`(限额/供应商/中止/无效输出)与 `blocked`
116
+ * —— 后者是 core `TerminalCause` 的 `{kind:"blocked", reason}`,d.ts 逐字为「the agent could not
117
+ * finish and said why」= **agent 自报的终态**(说完就结束,不等人、不留 checkpoint)。它**不是**
118
+ * 治理判决:门拒绝那条腿在 core 里走 `paused`/`failed`,永远不铸这个词;
119
+ * · `parked` —— 停在门上还能再动(`suspended` 审批/资源门、`needs_review` 评审门)。
113
120
  *
114
- * 🔴 为什么是 `switch` 而不是一张 `ReadonlySet<string>`(2026-08-08,[ref] 二轮扫描 finding):终局词表
115
- * **真变过**(core 5.8.0 把 `"timeout"` 从 `TaskStatus` 里删了)。而集合形的门对象是 `string`,对词表
116
- * 两个方向都无钉:
117
- * · **增词**(core 新加一个终局态)⇒ 集合不含它 ⇒ 该行被静默判成「非终局」,收敛器会一直等一个
118
- * 永远不会再动的行;
119
- * · **删词** ⇒ 集合里留一个死字面量,没有任何东西会说话。
120
- * 穷举 `switch` 把两个方向都变成**编译期**问题:core 加成员 ⇒ `default` 臂收不敛到 `never` ⇒ tsc 红,
121
- * 加成员的人被迫当场回答「这个新态算不算终局」;删成员 ⇒ 对应 `case` 变成不可达字面量、同样红。
122
- * 这条判据不许再被手抄成第二份 —— 两个消费点(`approval-reconciler.ts` 判据 2、`http/routes/runs.ts`
123
- * 的取消后复读)都从这里取。
121
+ * 🔴 为什么是**一张表**而不是三张 switch(S-213③,2026-09-11):`blocked` 这个词此前在本仓有**两个
122
+ * 独立写者**且答案互斥 —— `observability/run-terminal-log.ts` 判它是「治理判决,不是失败 ⇒ 不打行」,
123
+ * `fleet/fleet-bus.ts` 判它是「非成功终局(agent 放弃)⇒ failed 桶、行离场」。两处各自穷举、各自正确
124
+ * 地编译,而一条 `report_blocked` 的 run 在面板上离场、在引擎日志里一个字都没有。三张手抄的 switch
125
+ * 就是三个写者;收成一张表之后,词的语义只有这一处能改。
124
126
  *
125
- * 词表现状:终局 = completed / failed / blocked;非终局 = running / suspended / needs_review
126
- * (suspended 与 needs_review 都还能再动,`running` 更不必说)。
127
+ * 🔴 为什么这**不是**当年被否掉的 `ReadonlySet<string>`([ref] 二轮扫描 finding):那份集合的门对象是
128
+ * `string`,对词表两个方向都无钉 —— 增词静默判「非终局」、删词留一个死字面量没人说话。本表两个方向
129
+ * 都是**编译期**问题:`satisfies Readonly<Record<RunRecord["status"], RunStatusClass>>` 要求每个词都有
130
+ * 一行(core 加成员 ⇒ 缺行 ⇒ tsc 红,加成员的人被迫当场回答「这个新态算哪一类」),而多出一行
131
+ * core 已经没有的词同样红(超出键)。终局词表真变过(core 5.8.0 把 `"timeout"` 从 `TaskStatus` 删了)。
132
+ *
133
+ * 判据不许再被手抄成第二份:消费点(`approval-reconciler.ts` 判据 2、`http/routes/runs.ts` 的取消后
134
+ * 复读、`observability/run-terminal-log.ts` 的级别判、`fleet/fleet-bus.ts` 的 failed 桶编译钉)都从这里取。
135
+ */
136
+ export type RunStatusClass = "live" | "succeeded" | "unsuccessful" | "parked";
137
+ export declare const RUN_STATUS_CLASS: {
138
+ readonly running: "live";
139
+ readonly completed: "succeeded";
140
+ readonly failed: "unsuccessful";
141
+ readonly blocked: "unsuccessful";
142
+ readonly suspended: "parked";
143
+ readonly needs_review: "parked";
144
+ };
145
+ /** 表判为「非成功终局」的词——**从表派生**,不是手抄的第二份清单。今天 = `"failed" | "blocked"`。
146
+ * 消费点(`fleet/fleet-bus.ts` 的 failed 桶)拿它做编译期同源钉:表翻面 ⇒ 那道钉当场红。 */
147
+ export type UnsuccessfulRunStatus = {
148
+ [K in RunRecord["status"]]: (typeof RUN_STATUS_CLASS)[K] extends "unsuccessful" ? K : never;
149
+ }[RunRecord["status"]];
150
+ /**
151
+ * 「这一行已经走到**终局**了吗」——终局 = 成功 or 非成功(completed / failed / blocked);
152
+ * 非终局 = running / suspended / needs_review(后两者都还能再动)。
153
+ *
154
+ * 未知词判**非终局**:判成「终局」的代价不可逆 —— `approval-reconciler` 判据 2 会把一张还活着的 ask
155
+ * 永久打成 DENIED/routing_failure,取消路由也会把这一行当终局上报。
127
156
  */
128
157
  export declare function isTerminalRunStatus(status: RunRecord["status"]): boolean;
129
158
  /**
130
- * 「这一行**停在门上**(park)了吗」——`RunRecord["status"]` 的第二张穷举判据,与 {@link isTerminalRunStatus}
131
- * 同住词表属主处(扫描P2,2026-08-12)。
159
+ * 「这一行的终局是**非成功**的吗」(S-213③ 的共享判据)——`failed` 与 `blocked` 两词。
160
+ *
161
+ * 🔴 它回答的问题与 {@link isTerminalRunStatus} **不同**:后者只问「还会不会再动」,本判据问「这次任务
162
+ * 到底成没成」。凡是「这条 run 该不该在失败面上出声/离场」的判断都读这一条,不许再手抄
163
+ * `=== "failed" || === "blocked"`(那正是 `blocked` 长出两个互斥读法的成因)。
164
+ *
165
+ * 未知词判**不是**非成功:对读不懂的词保持沉默,好过按 error 级刷屏。
166
+ */
167
+ export declare function isUnsuccessfulRunStatus(status: RunRecord["status"]): boolean;
168
+ /**
169
+ * 「这一行**停在门上**(park)了吗」(扫描P2,2026-08-12)。
132
170
  *
133
171
  * 🔴 为什么必须有名字:park 是**两个词**,不是一个 —— `suspended`(tool_approval / policy_ask / human /
134
172
  * resource_limit 门)与 `needs_review`(`plan_review` / dry-run 拦截门,写点 = 各 store 的
@@ -139,8 +177,6 @@ export declare function isTerminalRunStatus(status: RunRecord["status"]): boolea
139
177
  *
140
178
  * 未知词的方向与 {@link isTerminalRunStatus} 同向保守:判**不是** park。理由是消费点会据此做**不可逆**
141
179
  * 动作(cancel 的恢复把手会 CAS-expire 一张 checkpoint);对一个本进程读不懂的状态,少动一次远好过错杀。
142
- * 真正的执法点是下面那条 `never`:core 的 `TaskStatus` 一加成员就 tsc 红,加成员的人必须当场回答
143
- * 「这个新态算不算 park」。
144
180
  */
145
181
  export declare function isParkedRunStatus(status: RunRecord["status"]): boolean;
146
182
  /** One row of the GET /v1/sessions list (CC /resume picker): a DISTINCT session (sessionId) aggregated from its
@@ -1,37 +1,24 @@
1
1
  import { parseJsonOr, toIso as iso, normalizeIsoOrNull as isoOrNull } from "./sql-row-helpers.js";
2
+ export const RUN_STATUS_CLASS = {
3
+ running: "live",
4
+ completed: "succeeded",
5
+ failed: "unsuccessful",
6
+ blocked: "unsuccessful",
7
+ suspended: "parked",
8
+ needs_review: "parked",
9
+ };
10
+ function runStatusClassOf(status) {
11
+ return RUN_STATUS_CLASS[status];
12
+ }
2
13
  export function isTerminalRunStatus(status) {
3
- switch (status) {
4
- case "completed":
5
- case "failed":
6
- case "blocked":
7
- return true;
8
- case "running":
9
- case "suspended":
10
- case "needs_review":
11
- return false;
12
- default: {
13
- const unhandled = status;
14
- void unhandled;
15
- return false;
16
- }
17
- }
14
+ const cls = runStatusClassOf(status);
15
+ return cls === "succeeded" || cls === "unsuccessful";
16
+ }
17
+ export function isUnsuccessfulRunStatus(status) {
18
+ return runStatusClassOf(status) === "unsuccessful";
18
19
  }
19
20
  export function isParkedRunStatus(status) {
20
- switch (status) {
21
- case "suspended":
22
- case "needs_review":
23
- return true;
24
- case "running":
25
- case "completed":
26
- case "failed":
27
- case "blocked":
28
- return false;
29
- default: {
30
- const unhandled = status;
31
- void unhandled;
32
- return false;
33
- }
34
- }
21
+ return runStatusClassOf(status) === "parked";
35
22
  }
36
23
  export function mapBakeRow(r) {
37
24
  return {
@@ -57,7 +57,7 @@ export declare class SqlWorkflowJournalStore implements WorkflowJournalStore {
57
57
  /** Record one agent's result. Idempotent per (run_id, ordinal): the PK upsert OVERWRITES (last-write-wins), so a
58
58
  * resumed run re-appending its replayed prefix verbatim leaves no duplicate. callKeyOrdinal is the SOLE ordinal
59
59
  * source (core's parser — a malformed key collapses to 0, matching InMemory). Throws on a REAL failure
60
- * (LOAD-BEARING), EXCEPT an oversize result is skip-journaled (see below) rather than throwing the workflow dead. */
60
+ * (LOAD-BEARING) — an oversize entry included, on BOTH arms (S-194 / core 7.12.0 [ref]; see below). */
61
61
  append(runId: string, scope: string, entry: WorkflowJournalEntry): Promise<void>;
62
62
  /** Entries for `runId` IF its recorded scope === `scope`, ASCENDING by ordinal; otherwise EMPTY. CORE-9 audit
63
63
  * BLOCKER: scope in the WHERE (twin of WorkflowRunStore) — the LLM controls `resumeFromRunId`, so a cross-scope
@@ -1,6 +1,6 @@
1
1
  import { callKeyOrdinal } from "@sema-agent/core";
2
- import { oversizeJournalResult } from "@sema-agent/core";
3
- import { journalEntryOfRow, journalEntryPayload, journalRowPayloadOf } from "../orchestration/workflow-journal-entry.js";
2
+ import { assertJournalEntryFits } from "@sema-agent/core";
3
+ import { journalEntryOfRow, journalRowPayloadOf } from "../orchestration/workflow-journal-entry.js";
4
4
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
5
5
  const DEFAULT_RESUME_CLAIM_TTL_MS = 60 * 60 * 1000;
6
6
  export class SqlWorkflowJournalStore {
@@ -14,15 +14,8 @@ export class SqlWorkflowJournalStore {
14
14
  return this.db.dialect === "tidb" ? tidb : pg;
15
15
  }
16
16
  async append(runId, scope, entry) {
17
+ assertJournalEntryFits(entry);
17
18
  const serialized = journalRowPayloadOf(entry);
18
- if (oversizeJournalResult(serialized)) {
19
- if (journalEntryPayload(entry).parked) {
20
- throw new Error(`workflow journal: PARKED entry ${entry.callKey} is ${serialized.length} bytes, over the journal result cap — refusing to skip it. ` +
21
- "Skipping a parked entry would make a resume start a SECOND live agent beside the session already pinned by the pending checkpoint (duplicating pre-approval side effects); " +
22
- "a completed result may be skipped (resume just re-runs that agent), a park may not.");
23
- }
24
- return;
25
- }
26
19
  await this.db.query(this.q("INSERT INTO workflow_journal (run_id, ordinal, scope, call_key, result, created_at_ms) VALUES (?,?,?,?,?,?) " +
27
20
  "ON DUPLICATE KEY UPDATE call_key = VALUES(call_key), result = VALUES(result)", "INSERT INTO workflow_journal (run_id, ordinal, scope, call_key, result, created_at_ms) VALUES ($1,$2,$3,$4,$5,$6) " +
28
21
  "ON CONFLICT (run_id, ordinal) DO UPDATE SET call_key = EXCLUDED.call_key, result = EXCLUDED.result"), [runId, callKeyOrdinal(entry.callKey), scope, entry.callKey, serialized, Date.now()]);
@@ -59,7 +59,7 @@ import type { Pool as PgPool } from "pg";
59
59
  import { summarizeWorkflowRun, type WorkflowRunStore } from "@sema-agent/core";
60
60
  import { type WorkflowCompletionInbox, type WorkflowCompletionInboxEntry } from "../orchestration/workflow-completion-inbox.js";
61
61
  import type { WorkflowNotifyJournalStore, WorkflowNotifyJournalEntry } from "../orchestration/workflow-notify-journal.js";
62
- import type { WorkflowAgentSessionIndex, WorkflowAgentSessionRow } from "../orchestration/workflow-agent-session-index.js";
62
+ import { type WorkflowAgentSessionIndex, type WorkflowAgentSessionRow } from "../orchestration/workflow-agent-session-index.js";
63
63
  import { type SqlDriver } from "./sql-driver.js";
64
64
  type WorkflowRun = Parameters<WorkflowRunStore["put"]>[1];
65
65
  /** Row cap guard: a run blob beyond this is NOT stored on update (the prior revision stays) — TiDB's
@@ -68,19 +68,41 @@ type WorkflowRun = Parameters<WorkflowRunStore["put"]>[1];
68
68
  export declare const MAX_RUN_BLOB_BYTES: number;
69
69
  declare const onWarnType: (msg: string, meta: Record<string, unknown>) => void;
70
70
  export type InboxWarn = typeof onWarnType;
71
- /** Review fix (1.108, lens④ HIGH): an oversize blob must NOT fail the update outright — core's persist chain
72
- * treats `false` as a CAS loss and does NOT advance its tracked rev, so once a run outgrows the cap EVERY later
73
- * write (including the TERMINAL one) keeps failing → the durable row is stuck `running` forever and the 24h
74
- * orphan sweep finalizes a genuinely-completed run as `abandoned`. Instead degrade the PAYLOAD, not the write:
75
- * step 1 truncate the two unbounded strings (result/error); step 2 drop the heavy arrays (phases/agents/groups)
76
- * with an explicit marker. Returns null only when even the skeleton is oversize (physics — caller keeps the
77
- * prior revision, matching the old behavior for that corner). Dialect-neutral (pure JS, no SQL) — shared
78
- * verbatim by both twins. */
79
71
  /** S-107:workflow_run 行的 `error`(脚本抛出的 `err.message`,core 源头不脱)在**两条写腿**(put / update 常态 + oversize 降级)
80
72
  * 同一口脱;`result` 由 core 完成时源头脱。幂等,不抛(超大 ⇒ 占位)。 */
81
73
  export declare function withRedactedError<T extends {
82
74
  error?: string | undefined;
83
75
  }>(run: T): T;
76
+ /** Review fix (1.108, lens④ HIGH): an oversize blob must NOT fail the update outright — core's persist chain
77
+ * treats `false` as a CAS loss and does NOT advance its tracked rev, so once a run outgrows the cap EVERY later
78
+ * write (including the TERMINAL one) keeps failing → the durable row is stuck `running` forever and the 24h
79
+ * orphan sweep finalizes a genuinely-completed run as `abandoned`. Instead degrade the PAYLOAD, not the write.
80
+ *
81
+ * 🔴 **S-192(7.71.0):瘦身只裁展示载荷,parked 行的恢复身份是最后才放弃的东西。**
82
+ * 病(修前逐字):step2 写作 `agents: []` ⇒ 一只 parked 的 workflow 子代的 `callKey` / `sessionId` /
83
+ * `parkedCheckpointToken` 随整段消失。server 侧复核「这条 park 仍在此 token 下」的依据**只有这一行**(join 索引
84
+ * 刻意不存它——它是能力;引擎自己的赎回键另住 resume journal),于是 `/decide` 的 workflow 车道在 `run.agents`
85
+ * 里找不到那一行 ⇒ 落 legacy 腿拿一个**错因**的 409 `conflict.resume_context_unavailable`,人的批准无处兑付;
86
+ * 同时 core resume 路径上那道「blob 里 parked 而 journal 缺条目 ⇒ 拒 resume」的防线(`prior.agents`)也随之失明。
87
+ *
88
+ * 阶梯 = 一张**按价值排序的放弃清单**,一级一级试到装得下为止(**判据只有一条:恢复身份最后放弃**):
89
+ * step 1 —— 两个无界字符串(`result` / `error`)截断带标记;
90
+ * step 2 —— `phases` / `groups` 整段丢,`agents` 收敛成**恢复身份行**
91
+ * ({@link buildParkedRecoveryIdentities};非 parked 的行不带恢复身份 ⇒ 整行丢,与修前同);
92
+ * step 3 —— 顶层**观测载荷**(codex r1-[high],红先复现后修):`effectiveArgs` 与 `resultFull` 整段丢、
93
+ * `name` / `description` 先脱后切。core 对 `effectiveArgs` 的成文口径逐字是 OBSERVATION-ONLY
94
+ * (never a gate input,never re-read by the engine;resume 的身份由 journal 的 callKey 携带,
95
+ * 不是这个字段),`resultFull` 同类(取回面),两者都**无界**。修前这一级不存在,于是阶梯
96
+ * 一边原样留着它们、一边把唯一那条 parked 行删掉盖标记 —— 自己违反了自己那条判据,
97
+ * 一条本来赎得回的审批被判死(生产上界 4 MiB 上真复现);
98
+ * step 4 —— 恢复身份行上的**展示名**(`label`)—— 它是这条行上唯一可弃的展示物,身份四位恒在
99
+ * (codex r2-[high],红先复现后修:修前阶梯一边留着每行 200 字符的 label、一边把**全部**身份
100
+ * 删掉盖标记 —— 同一条判据第二次被自己违反;6000 条 CJK label 的 parked 行在生产上界上
101
+ * 实测 `agents:[]` + `parks:6000`,而清空 label 后 6000 条身份**全都装得下**);
102
+ * step 5 —— 连只剩身份的行都装不下 ⇒ 丢掉它们并落**显式不可恢复标记**({@link PARK_IDENTITY_LOSS_KEY}),
103
+ * `/decide` 与 SLA sweep 读到它就**响亮拒**,而不是让一条赎回不了的 park 看起来像普通 miss。
104
+ * Returns null only when even the marked skeleton is oversize (physics — caller keeps the prior revision,
105
+ * matching the old behavior for that corner). Dialect-neutral (pure JS, no SQL) — shared verbatim by both twins. */
84
106
  export declare function slimOversizeRun(run: WorkflowRun & {
85
107
  id: string;
86
108
  scope: string;
@@ -1,9 +1,11 @@
1
1
  import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
2
2
  import { summarizeWorkflowRun, isTerminalWorkflowStatus, WorkflowRunStoreError } from "@sema-agent/core";
3
3
  import { foldKeyFamily, MAX_PENDING_PER_SESSION, PURGE_FENCE_MS, SERVED_FENCE_MS, } from "../orchestration/workflow-completion-inbox.js";
4
+ import { buildParkedRecoveryIdentities, buildParkIdentityLoss, PARK_IDENTITY_LOSS_KEY } from "../orchestration/workflow-agent-session-index.js";
4
5
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
5
6
  import { isDupKeyError } from "./sql-errors.js";
6
7
  export const MAX_RUN_BLOB_BYTES = 4 * 1024 * 1024;
8
+ const SLIM_HEADER_MAX = 200;
7
9
  const onWarnType = (msg, meta) => void [msg, meta];
8
10
  export function withRedactedError(run) {
9
11
  if (typeof run.error !== "string")
@@ -22,8 +24,33 @@ export function slimOversizeRun(run, maxBytes) {
22
24
  let blob = JSON.stringify(step1);
23
25
  if (Buffer.byteLength(blob) <= maxBytes)
24
26
  return blob;
25
- const step2 = { ...step1, phases: [], agents: [], groups: [], error: step1.error ? String(step1.error).slice(0, 1000) + MARK : MARK };
27
+ const identities = buildParkedRecoveryIdentities(run.agents);
28
+ const shortError = step1.error ? String(step1.error).slice(0, 1000) + MARK : MARK;
29
+ const step2 = { ...step1, phases: [], agents: identities, groups: [], error: shortError };
26
30
  blob = JSON.stringify(step2);
31
+ if (Buffer.byteLength(blob) <= maxBytes)
32
+ return blob;
33
+ const step3 = { ...step2 };
34
+ delete step3.effectiveArgs;
35
+ delete step3.resultFull;
36
+ if (typeof step3.name === "string")
37
+ step3.name = redactHead(step3.name, SLIM_HEADER_MAX);
38
+ if (typeof step3.description === "string")
39
+ step3.description = redactHead(step3.description, SLIM_HEADER_MAX);
40
+ blob = JSON.stringify(step3);
41
+ if (Buffer.byteLength(blob) <= maxBytes)
42
+ return blob;
43
+ const bareIdentities = buildParkedRecoveryIdentities(run.agents, 0);
44
+ const step4 = { ...step3, agents: bareIdentities };
45
+ blob = JSON.stringify(step4);
46
+ if (Buffer.byteLength(blob) <= maxBytes)
47
+ return blob;
48
+ const step5 = {
49
+ ...step4,
50
+ agents: [],
51
+ ...(identities.length > 0 ? { [PARK_IDENTITY_LOSS_KEY]: buildParkIdentityLoss(identities.length) } : {}),
52
+ };
53
+ blob = JSON.stringify(step5);
27
54
  return Buffer.byteLength(blob) <= maxBytes ? blob : null;
28
55
  }
29
56
  export class SqlWorkflowRunStore {
@@ -13,8 +13,16 @@
13
13
  * `env 显式` > `center 发布` > `posture 派生` > `引擎/服务端默认`。
14
14
  *
15
15
  * ## 定界
16
- * 本模块只说**来源**。值本身、以及「这台机器凭什么派生出这个值」那句话,由每个键自己给
17
- * ({@link PostureKnobFacts.postureMeaning})——那句话是键专属的事实,共享它只会得到一句谁都不认的空话。
16
+ * 本模块只说**来源**。值本身、以及**两条键专属臂**的句子——「这台机器凭什么派生出这个值」
17
+ * ({@link PostureKnobFacts.postureMeaning})与「组织发布的值在这台机器上怎么生效」
18
+ * ({@link PostureKnobFacts.centerMeaning})——由每个键自己给。那两句话是键专属的事实,共享它们只会
19
+ * 得到一句谁都不认的空话,或者更坏:一句**对某些键是假的**话。
20
+ *
21
+ * 🔴 `centerMeaning` 是 G-7(test [ref])的修复面,成因是一次真的过期断言:这一臂原本硬编码
22
+ * 「applied from the config-center (**restart-to-apply**)」,而 S-174(core 7.10.0 [ref])之后
23
+ * `readFace.face` 是**热换席**(`Runner.swapDeps({readFace})`,下一拍 refresh 当场换进全部 Runner,
24
+ * `read-face` 重启片也不再为它签理由)——运维照那句话去滚动重启一片 fleet,重启的是一个已经生效的变更。
25
+ * 生效时机是**键**的事实,不是四词表的属性,所以它跟 `postureMeaning` 一样归键。
18
26
  */
19
27
  /** 旋钮取值来源的闭集词表(顺序 = 优先序)。 */
20
28
  export declare const POSTURE_SOURCE_WORDS: readonly ["env", "center", "posture", "engine-default"];
@@ -25,13 +33,25 @@ export interface PostureKnobFacts {
25
33
  /** 这台机器上占住这个键的 env 变量名(指路句要逐字点它,运维才知道去改哪个)。 */
26
34
  readonly knob: string;
27
35
  readonly source: PostureSource;
36
+ /**
37
+ * `center` 那一臂**专属**的一句话:组织发布的这个值在这台机器上**怎么生效**(热换席?等重启?
38
+ * 还是这个键根本没有 center 腿?),以及怎么把这个键取回来。只在 `source === "center"` 时被读到。
39
+ *
40
+ * 🔴 为什么归键而不是共享(G-7 的成因):生效时机是键的事实。同一句「restart-to-apply」对
41
+ * `readFace.face` 是假的(S-174 热换席),对三个 S-178 旋钮则连触发条件都没有(`config.ts` 的
42
+ * `postureSourceOfKnob` / `postureSourceOfNumEnv` 只产 env/posture/engine-default,center 写不到
43
+ * 它们)—— 一句共享的硬编码必然对其中一类说谎。
44
+ */
45
+ readonly centerMeaning: string;
28
46
  /**
29
47
  * `posture` 那一臂**专属**的一句话:这台机器为什么派生出了这个值,以及怎么把它钉回去。
30
48
  * 只在 `source === "posture"` 时被读到 —— 别的臂的句子是通用的(见 {@link postureSourceNote})。
31
49
  */
32
50
  readonly postureMeaning: string;
33
51
  }
34
- /** 运维指路句 —— **穷举 switch,无缺省臂**:四词闭集加一个成员这里就编译红。 */
52
+ /** 运维指路句 —— **穷举 switch,无缺省臂**:四词闭集加一个成员这里就编译红。
53
+ * 两条**通用**臂(`env` / `engine-default`)的句子在这里写死并点名旋钮;两条**键专属**臂
54
+ * (`center` / `posture`)原样取键给的那句话(理由见 {@link PostureKnobFacts} 的两个字段注)。 */
35
55
  export declare function postureSourceNote(facts: PostureKnobFacts): string;
36
56
  /**
37
57
  * 这个值是**运维显式表态**的吗?
@@ -4,7 +4,7 @@ export function postureSourceNote(facts) {
4
4
  case "env":
5
5
  return `pinned by this machine's ${facts.knob} env var (deployment sovereignty — it wins over any published value for this key)`;
6
6
  case "center":
7
- return `applied from the config-center (restart-to-apply); set ${facts.knob} on this machine to take the key back`;
7
+ return facts.centerMeaning;
8
8
  case "posture":
9
9
  return facts.postureMeaning;
10
10
  case "engine-default":
@@ -46,7 +46,14 @@ export interface ReadFacePostureFacts {
46
46
  */
47
47
  export declare function readFaceForPosture(singleUserTurnkey: boolean): ReadFacePostureFacts;
48
48
  /** 本档的运维指路句 —— 骨架由共享件 {@link postureSourceNote} 给(四词穷举在那里,加词即编译红);
49
- * 本函数只提供 `posture` 那一臂的**键专属**事实句。 */
49
+ * 本函数提供两条**键专属**臂(`center` / `posture`)的事实句。
50
+ *
51
+ * 🔴 `center` 那句话的事实来源是 S-174(core 7.10.0 [ref]):`RunnerDeps.readFace` 进了 core 的可换席
52
+ * 闭集 `SWAPPABLE_DEP_SEATS` ⇒ 组织发布/撤回一个档位,**下一拍 refresh 当场换进全部 Runner**,
53
+ * `config-center/restart-signal.ts` 的 `read-face` 片也不再为它签重启理由(判据同源 =
54
+ * `CENTER_READ_FACE_LIVE_KEYS`,由 core 的 `isSwappableDepSeat` 派生)。本句此前逐字写着
55
+ * 「(restart-to-apply)」—— G-7 抓获的那条过期断言,方向恰好是「让运维为一个已生效的变更重启 fleet」。
56
+ * ⚠️ 只说 `face` 这一席:三个 deny 键仍是 boot-baked,那半场的话不在这条读面上(它们没有来源位)。 */
50
57
  export declare function readFaceSourceNote(source: ReadFaceSource): string;
51
58
  /** operator 诊断面的投影(`GET /v1/diagnostics/wiring`)。`face:null` = 本部署没钉档,引擎默认接管。 */
52
59
  export declare function buildReadFaceWiring(facts: ReadFacePostureFacts): {
@@ -7,6 +7,7 @@ export function readFaceSourceNote(source) {
7
7
  return postureSourceNote({
8
8
  knob: "READ_FACE",
9
9
  source,
10
+ centerMeaning: "published by the config-center and applied HOT — the containment FACE is a swappable engine seat (core 7.10.0), so the next refresh (or POST /v1/admin/config/refresh) swaps it into every Runner with NO restart; set READ_FACE on this machine to take the key back",
10
11
  postureMeaning: "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): reads outside the workspace roots are allowed by default — set READ_FACE=roots to pin the containing posture back",
11
12
  });
12
13
  }
@@ -1,4 +1,4 @@
1
- import type { RunCancelContext } from "./plugins/store-contracts.js";
1
+ import type { PersistedTaskResult, RunCancelContext } from "./plugins/store-contracts.js";
2
2
  /**
3
3
  * @param taskId 这条 run 的账本 id(turn 活性登记的键)。
4
4
  * @param startedAtMs 这条 run 起跑的墙钟时刻(epoch ms):取消腿手上有行就用行的 `createdAt`,
@@ -10,4 +10,53 @@ import type { RunCancelContext } from "./plugins/store-contracts.js";
10
10
  * 断言「我看过现场,什么都没有」,而事实是「我什么都没看见」)。
11
11
  */
12
12
  export declare function buildCancelContext(taskId: string, startedAtMs: number, nowMs?: number): RunCancelContext | undefined;
13
+ /**
14
+ * 🔴 **用户取消的机器码 —— 全仓单源**(合并重扫 [5])。
15
+ *
16
+ * run 行的 `errorCode` 列、因由臂上的 `terminal.code`、`tasks_total{status}` 的 metrics 标签、fleet 行的
17
+ * 终态词,是**同一个词**;同一个词就该同一个来源。此前仓里是「一个词两只常量 + 十余处字面量」
18
+ * (`approval-reconciler.ts` 另有一份 `CANCELLED_ERROR_CODE`),改它的值会让 recordDone / fleet 标签 /
19
+ * reconciler 的 VOID 判据 / cancel 端点的幂等判据静默分叉。
20
+ *
21
+ * 机器门(新写者当场红):`test/cancel-terminal-single-owner.test.ts` 的 G2/G3。
22
+ */
23
+ export declare const CANCELLED_CODE = "cancelled";
24
+ /**
25
+ * S-104 —— **取消终局的唯一铸点**。三条用户取消腿(sync stream 的断连支与 cancel-verb 支、durable bg 腿)
26
+ * 都从这里出结果,于是「一次取消在账本/wire 上长什么样」只有一份口径。
27
+ *
28
+ * ## 两种到达形,一条规则
29
+ * `engineResult` 在场与否 = 「引擎有没有给出终局」,不是两条规则:
30
+ * · **在场**(现行常态):**跑起来之后**的 `signal.aborted` 被 core 当成流内事件优雅收尾 —— 流照常
31
+ * 发 `done`,`terminal` 是一条**不带码**的 `failed`。这里在**它**之上补标,不另造一份结果(另造会把
32
+ * 引擎已经算出的 `stats` / `result` / `editedFiles` 全丢掉)。
33
+ * · **缺席**:根本没读到 done —— 客户端断连后迭代器早收,或**开跑之前**就已经立起的旗
34
+ * (亲核 core 7.11.2 `dist/core/runner/runtask.js:623-626`:`runLocked` 入口对已 aborted 的 signal
35
+ * 抛 `AbortError`)⇒ 合成一条诚实的取消终局。
36
+ *
37
+ * ## 「只补空座」纪律(与 core 的 `amendTerminal` 同规),而且**补码与补快照同进同退**
38
+ * · 引擎给的终局**不是无码的 failed**(它完成了 / 阻塞了 / 自己带了码)⇒ **整只原样返回**。
39
+ * 这一条是本车自查轮逮住的自造缺陷:首版只让**补码**受这条纪律管、快照却无条件挂,于是「取消动词
40
+ * 到了、但活其实已经干完了」会产出一条 `completed` 却带着取消便签的终局 —— 契约 §5.3 逐字写着
41
+ * 「只在用户取消形上铸」,两个方向的读者都会读错。一次与 abort 赛跑的真失败(`brain.network`)同理:
42
+ * 贴成 `cancelled` 会把故障统计洗白。
43
+ * · `cancelContext`:已经有快照就不重铸(重铸会把「上一条引擎事件多久以前」重新计时)。
44
+ *
45
+ * ## 为什么不判「这条 failed 是不是 abort 形」
46
+ * 判据在**调用点**:调用点问的是「本腿这次是不是被取消动词打中了」(`cancelCtrl.signal.aborted` /
47
+ * `cancelledViaVerb`),那是 server 自己的事实。让本函数去嗅 `terminal.message` 的字面就是再造事实
48
+ * —— [ref] 二那次事故的同一个形。
49
+ */
50
+ export declare function mintCancelledResult(args: {
51
+ /** 这条 run 的账本 id(活性登记的键;账本不在场时给空串,快照按「证不出」处理)。 */
52
+ taskId: string;
53
+ /** 会话 id(合成形要写进结果;引擎给了结果时不用)。 */
54
+ sessionId: string;
55
+ /** 本腿起跑的墙钟时刻(epoch ms)。语义与缺席纪律见 {@link buildCancelContext}。 */
56
+ startedAtMs: number;
57
+ /** 合成形的失败文案(引擎给了终局时不用)。 */
58
+ message: string;
59
+ /** 引擎给出的终局(在场 ⇒ 在它之上补标)。 */
60
+ engineResult?: PersistedTaskResult;
61
+ }): PersistedTaskResult;
13
62
  //# sourceMappingURL=run-cancel-context.d.ts.map
@@ -12,4 +12,26 @@ export function buildCancelContext(taskId, startedAtMs, nowMs = Date.now()) {
12
12
  ...(elapsedMs !== undefined ? { elapsedMs } : {}),
13
13
  };
14
14
  }
15
+ export const CANCELLED_CODE = "cancelled";
16
+ export function mintCancelledResult(args) {
17
+ const ctx = args.taskId === "" ? undefined : buildCancelContext(args.taskId, args.startedAtMs);
18
+ const engine = args.engineResult;
19
+ if (engine !== undefined) {
20
+ if (engine.terminal.kind !== "failed" || engine.terminal.code !== undefined)
21
+ return engine;
22
+ return {
23
+ ...engine,
24
+ terminal: { ...engine.terminal, code: CANCELLED_CODE },
25
+ ...(engine.cancelContext === undefined && ctx !== undefined ? { cancelContext: ctx } : {}),
26
+ };
27
+ }
28
+ return {
29
+ taskId: args.taskId,
30
+ sessionId: args.sessionId,
31
+ terminal: { kind: "failed", code: CANCELLED_CODE, message: args.message },
32
+ result: "",
33
+ stats: { turns: 0, tokens: 0 },
34
+ ...(ctx ? { cancelContext: ctx } : {}),
35
+ };
36
+ }
15
37
  //# sourceMappingURL=run-cancel-context.js.map