@sema-agent/server 7.70.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 (77) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +6 -3
  4. package/dist/approval-content-kind.d.ts +13 -7
  5. package/dist/approval-content-kind.js +3 -1
  6. package/dist/approval.js +3 -2
  7. package/dist/bench/s1/live-deps.js +2 -0
  8. package/dist/boot/execution-env.js +12 -2
  9. package/dist/boot/memory-consolidation.d.ts +2 -1
  10. package/dist/boot/parked-revive-gate.d.ts +2 -0
  11. package/dist/boot/parked-revive-gate.js +5 -1
  12. package/dist/boot/resolve-spec.js +8 -3
  13. package/dist/boot/runner-deps.d.ts +16 -43
  14. package/dist/boot/runner-deps.js +10 -4
  15. package/dist/brain.d.ts +3 -1
  16. package/dist/brain.js +2 -1
  17. package/dist/config-catalog.js +3 -1
  18. package/dist/config-types.d.ts +31 -1
  19. package/dist/config-types.js +1 -0
  20. package/dist/config.js +15 -3
  21. package/dist/deployment-governance.d.ts +6 -3
  22. package/dist/deployment-governance.js +2 -1
  23. package/dist/execution-lane-caps.d.ts +51 -37
  24. package/dist/execution-lane-caps.js +23 -0
  25. package/dist/fleet/fleet-bus.d.ts +22 -1
  26. package/dist/fleet/fleet-bus.js +2 -1
  27. package/dist/http/server.js +17 -5
  28. package/dist/index.d.ts +1 -1
  29. package/dist/leader/wire.js +4 -3
  30. package/dist/main.js +4 -2
  31. package/dist/memory-operator-faces.d.ts +1 -1
  32. package/dist/memory-scope.d.ts +24 -0
  33. package/dist/memory-scope.js +12 -3
  34. package/dist/observability/metrics.d.ts +4 -0
  35. package/dist/observability/metrics.js +2 -0
  36. package/dist/observability/run-terminal-log.d.ts +4 -3
  37. package/dist/observability/run-terminal-log.js +4 -15
  38. package/dist/orchestration/workflow-agent-session-index.d.ts +85 -2
  39. package/dist/orchestration/workflow-agent-session-index.js +38 -5
  40. package/dist/parked-decide.d.ts +40 -1
  41. package/dist/parked-decide.js +19 -2
  42. package/dist/plugins/checkpoint-store-sql.js +5 -5
  43. package/dist/plugins/local-checkpoint-store.js +3 -3
  44. package/dist/plugins/remote-env-adb.d.ts +11 -0
  45. package/dist/plugins/remote-env-adb.js +2 -0
  46. package/dist/plugins/remote-env-device.d.ts +9 -0
  47. package/dist/plugins/remote-env-device.js +3 -0
  48. package/dist/plugins/remote-env-e2b.d.ts +11 -0
  49. package/dist/plugins/remote-env-e2b.js +2 -0
  50. package/dist/plugins/remote-env-host.d.ts +11 -0
  51. package/dist/plugins/remote-env-host.js +2 -0
  52. package/dist/plugins/remote-env-k8s.d.ts +11 -0
  53. package/dist/plugins/remote-env-k8s.js +2 -0
  54. package/dist/plugins/remote-env-local-docker.d.ts +11 -0
  55. package/dist/plugins/remote-env-local-docker.js +2 -0
  56. package/dist/plugins/remote-env-ssh.d.ts +11 -0
  57. package/dist/plugins/remote-env-ssh.js +2 -0
  58. package/dist/plugins/store-backend.d.ts +34 -12
  59. package/dist/plugins/store-backend.js +12 -3
  60. package/dist/plugins/store-contracts.d.ts +54 -18
  61. package/dist/plugins/store-contracts.js +17 -30
  62. package/dist/plugins/workflow-run-store-sql.d.ts +31 -9
  63. package/dist/plugins/workflow-run-store-sql.js +28 -1
  64. package/dist/run-local.js +3 -0
  65. package/dist/task-a2a.js +2 -2
  66. package/dist/task-mcp.js +2 -2
  67. package/dist/task-settings.d.ts +21 -8
  68. package/dist/task-settings.js +2 -2
  69. package/dist/trace/core-keyset-guard.d.ts +128 -8
  70. package/dist/trace/engine-notice-wire.d.ts +1 -1
  71. package/dist/trace/engine-notice-wire.js +1 -0
  72. package/dist/trace/ledger-events.d.ts +5 -1
  73. package/dist/trace/ledger-sink.js +8 -1
  74. package/dist/trace/project.d.ts +21 -4
  75. package/dist/trace/project.js +5 -3
  76. package/dist/trace/redact.js +10 -5
  77. package/package.json +3 -3
@@ -23,6 +23,7 @@ export class RemoteSshExecutionEnv {
23
23
  destroyed = false;
24
24
  connEpoch = 0;
25
25
  inFlight;
26
+ homeDir;
26
27
  cfg;
27
28
  injectedConnectOnce;
28
29
  constructor(config, deps) {
@@ -41,6 +42,7 @@ export class RemoteSshExecutionEnv {
41
42
  hostKey: config.hostKey,
42
43
  };
43
44
  this.cwd = this.cfg.mountPath;
45
+ this.homeDir = config.homeDir;
44
46
  }
45
47
  resolve(p) {
46
48
  return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
@@ -116,6 +116,22 @@ export type ToolResultStoreFull = ToolResultStore & {
116
116
  export type BreakerStateStore = TiDBBreakerState | PgBreakerState;
117
117
  export type CostQuotaStore = TiDBCostQuota | PgCostQuota;
118
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;
119
135
  export interface StoreBackend {
120
136
  /** Protocol-truth kind (clay 2026-07-06 wording): "mysql" = any MySQL-protocol server (MySQL / TiDB /
121
137
  * MariaDB) behind the one mysql2 pool — the startup summary prints durable(mysql) for all of them. */
@@ -186,19 +202,25 @@ export interface StoreBackend {
186
202
  * 「没有行 ⇒ 代码缺省 allow」—— 那是**放宽面**上的静默降级(permissionRule 那条禁令的同一理由)。
187
203
  * File 形候真实的 local+governed 部署需求出现再落(候需求档)。 */
188
204
  memoryOptOutGrant(): MemoryOptOutGrantStore | undefined;
189
- /** [ref]②([ref] §2.1b,core 7.0.2 [ref] 件2):会话 capture opt-out **记录**的 SQL 载体
190
- * (`session_capture_optout`)—— core `RunnerDeps.memoryCaptureRecordStore` /
191
- * `MemoryEngineOptions.captureRecordStore` 的持久层(Promise 形三腿)。与 §3.1 的**授权表**不是一个轴:
192
- * 那张管「谁**可以** opt-out」(operator 写的 verdict),这张管「谁**已经** opt-out」(用户写的一次性
193
- * 单向记录)。
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 的实装注)。
194
216
  *
195
- * 🔴 **可选**(与 memoryOptOutGrant 同族):SQL 后端 = 双方言 twin;`local` 车道 **undefined**,且缺席是
196
- * 一个真答案 —— 缺席时 core 用它的控制面文件三腿(`fileSessionCaptureRecordStore`),单机部署上那正是
197
- * 重启可存活的正确载体(383 §2.1b:「absent = 文件三腿,单进程/单机字节不变」)。SQL 载体要解决的病
198
- * 只在无状态多副本形:文件记录对 resume 副本不可见,「record not found」恰朝**继续采集**方向坏。
199
- * 刻意不给 local 一个 SQL/内存 twin:内存形会在重启时把一条隐私记录静默丢掉(比文件三腿严格更差),
200
- * 文件形 core 已自带。 */
201
- 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;
202
224
  /** S-1 legacy-epoch discriminant (core `RunnerDeps.legacyRewindBoundaryProbe`): does the RETIRED whole-tree
203
225
  * snapshot epoch hold a boundary for (sessionId, entryId)? Reads the legacy `snapshot_manifest` table.
204
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 {
@@ -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 {
package/dist/run-local.js CHANGED
@@ -19,6 +19,7 @@ import { loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./c
19
19
  import { createConfigProvider } from "./config-provider.js";
20
20
  import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
21
21
  import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
22
+ import { executionLaneHomeDir } from "./execution-lane-caps.js";
22
23
  import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
23
24
  import { loadSkills } from "./capabilities/skills.js";
24
25
  import { createRepoClient } from "./capabilities/repo-tools.js";
@@ -273,6 +274,7 @@ export async function runLocal(argv, deps = {}) {
273
274
  const executionEnvFactory = hostRx
274
275
  ? hostExecutionEnvFactory({
275
276
  workspaceDir,
277
+ ...((h) => (h === undefined ? {} : { homeDir: h }))(executionLaneHomeDir(config)),
276
278
  backgroundShell: config.requirePrincipal !== true,
277
279
  ...(hostRx.commandTimeoutMs != null ? { commandTimeoutMs: hostRx.commandTimeoutMs } : {}),
278
280
  })
@@ -369,6 +371,7 @@ export async function runLocal(argv, deps = {}) {
369
371
  ],
370
372
  });
371
373
  const sharedRunnerDeps = createSharedRunnerDeps({
374
+ metrics,
372
375
  config,
373
376
  governanceSeams,
374
377
  logger,
package/dist/task-a2a.js CHANGED
@@ -1,4 +1,4 @@
1
- import { resolveComplianceDenies } from "@sema-agent/core";
1
+ import { resolveComplianceDenies, TOOL_WIRE_NAME_MAX_CHARS } from "@sema-agent/core";
2
2
  import { HttpError } from "./security.js";
3
3
  export const MAX_REQUEST_A2A_PEERS = 32;
4
4
  export function a2aInjectionHonored(config) {
@@ -19,7 +19,7 @@ function normalizeA2aPeer(e) {
19
19
  if (!e || typeof e !== "object")
20
20
  return null;
21
21
  const o = e;
22
- if (typeof o.name !== "string" || o.name.length === 0 || o.name.length > 128)
22
+ if (typeof o.name !== "string" || o.name.length === 0 || o.name.length > TOOL_WIRE_NAME_MAX_CHARS)
23
23
  return null;
24
24
  if (typeof o.url !== "string" || !HTTP_URL_RE.test(o.url))
25
25
  return null;
package/dist/task-mcp.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mcpInjectionDroppedNotice, resolveComplianceDenies } from "@sema-agent/core";
1
+ import { mcpInjectionDroppedNotice, resolveComplianceDenies, TOOL_WIRE_NAME_MAX_CHARS } from "@sema-agent/core";
2
2
  import { HttpError } from "./security.js";
3
3
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
4
4
  import { readMcpSourceLabel } from "./mcp-source-label.js";
@@ -26,7 +26,7 @@ function normalizeServer(e, noteBadOrigin) {
26
26
  if (!e || typeof e !== "object")
27
27
  return bad("entry");
28
28
  const o = e;
29
- if (typeof o.name !== "string" || o.name.length === 0 || o.name.length > 128)
29
+ if (typeof o.name !== "string" || o.name.length === 0 || o.name.length > TOOL_WIRE_NAME_MAX_CHARS)
30
30
  return bad("name");
31
31
  const t = o.transport;
32
32
  if (!t || typeof t !== "object")
@@ -312,15 +312,28 @@ export interface FsWriteGateWiring {
312
312
  /**
313
313
  * 🔴 S-177 / codex r2 [high](验真后修)—— **执行环境的 home**(core `PathRuleBases.home`,`~/…` 形的基)。
314
314
  *
315
- * core 对缺席的 `home` 回落 `os.homedir()` —— 那是**这个 server 进程**的 home。引擎与执行同机时它就是
316
- * 对的;**远端车道**(SSH / 容器 / 沙箱)上执行用户的 home 与 server 的 home 是两个目录,于是
317
- * `deny Edit(~/.sema/**)` 编译成功、却拿 server 的 home 去匹配远端的目标 ⇒ 一条收紧规则**静默失效**
318
- * (与 `taskRoot` 那条哨兵病同族、同方向)。
315
+ * 🔴 **S-213④ 起本字段在远端车道上也可以有值**:四只远端 adapter 此前一个都不声明 `ExecutionEnv.homeDir`,
316
+ * 而本字段那时只在 `remoteExec` 整个缺席时才铸 —— 于是**每一条远端部署的 `~/` 规则都恒 422**(接入审计
317
+ * M13)。修法不是放松下面那道门,而是让部署**真的知道** home:`execution-lane-caps.ts` 的
318
+ * `executionLaneHomeDir` 是唯一属主,`boot/execution-env.ts` 把它递给 adapter 的 `homeDir` 座、
319
+ * `boot/resolve-spec.ts` / `boot/parked-revive-gate.ts` 把**同一个值**递成本字段。两处同源是承重的:
320
+ * 各算一份就会出现「422 门放行而引擎判 unreadable」或反过来。
319
321
  *
320
- * ⇒ 本字段只在 home **真知道**时在场(引擎与执行同机)。缺席时,`~/` 形的规则由
321
- * {@link rejectUnmatchableSettingsNames} **响亮拒**(422),绝不回落到 server 的 home。
322
- * ⚠️ 上游缺口如实登记:core 今天没有「这条规则需要哪些 base」的查询口,也不把 `home` 当必答项 ——
323
- * 所以「这条规则用了 `~/` 吗」由本仓读**core 自己解析出的规范 command**(`~/` 前缀)来判,不自铸文法。
322
+ * 🔴 **S-213⑦ 改真话**(亲读 core 7.12.0 `dist/core/permission-rule-model.js:365-372` /
323
+ * `:340-350`,推翻本注此前的两句):
324
+ * · core 对缺席的 `home` **不**回落 `os.homedir()` —— `resolvePathPattern` 走
325
+ * `lexicalNormalAbsolutePathOf(baseValueOf("home", bases) ?? "")`,base 缺席 ⇒ `undefined` ⇒ 直接回
326
+ * `{missingBase:"home"}`,该调用的射程判 `unreadable`(fail-closed 的 ask,人来清),从不猜一个目录。
327
+ * 换言之引擎侧本来就没有「拿 server 的 home 去匹配远端目标」这条静默腿(那句旧注是本仓自己的推断)。
328
+ * · core **有**「这条规则需要哪些 base」的查询口 —— 根导出的 `ruleBasesNeeded(rule)`
329
+ * (`//abs` ⇒ 空、`~/` ⇒ `["home"]`、`/x` ⇒ `["root"]`、其余 ⇒ `["cwd"]`),`resolvePathPattern`
330
+ * 自己也读它。本仓的判据腿今天读的是 core 解析出的规范 command 前缀,与该查询口**同一分类**、
331
+ * 不是第二套文法;两者哪天分叉由这条注负责被人看见。
332
+ *
333
+ * ⇒ 本字段在**这条部署的执行车道 home 已知**时在场(`executionLaneHomeDir`,见 `execution-lane-caps.ts`)。
334
+ * 缺席时 `~/` 形规则由 {@link rejectUnmatchableSettingsNames} **响亮拒**(422),而不是让它编译成一条
335
+ * core 只会判 `unreadable` 的规则 —— 两道门同向(都不猜目录),422 只是把同一个拒挪到更早、更响的地方:
336
+ * 运维在**下单那一刻**就知道这条规则这条车道读不了,而不是等到跑起来每次都弹一张 ask。
324
337
  */
325
338
  taskHome?: string;
326
339
  /** The session scratchpad dir (envFacts.scratchpadDir, [ref]③) — writes there are always auto-allowed. */
@@ -279,8 +279,8 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
279
279
  const parsed = parseRuleText(name, behavior);
280
280
  if ("rule" in parsed && ruleToolGrammarOf(parsed.rule.tool) === "path" && parsed.rule.command.startsWith("~/")) {
281
281
  throw new Error(`${field} has a home-relative path rule (${name}) but this deployment cannot resolve the EXECUTION environment's home directory — ` +
282
- `on a remote lane the engine's own home is a different user's directory, so the rule would compile and then match nothing that runs. ` +
283
- `Spell the path absolutely (${"Edit(//abs/path/**)"} form), or run the task on a lane whose home this worker knows.`);
282
+ `the engine never guesses one: without a home base the rule resolves to "unreadable" and every matching call stops to ask, so it guards nothing. ` +
283
+ `Spell the path absolutely (${"Edit(//abs/path/**)"} form), declare the lane's home with REMOTE_EXEC_HOME_DIR, or run the task on a lane whose home this worker knows.`);
284
284
  }
285
285
  }
286
286
  }