@sema-agent/server 7.70.0 → 7.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +8 -4
  4. package/dist/approval-card.d.ts +16 -17
  5. package/dist/approval-card.js +0 -9
  6. package/dist/approval-content-kind.d.ts +13 -7
  7. package/dist/approval-content-kind.js +3 -1
  8. package/dist/approval.js +3 -2
  9. package/dist/bench/s1/live-deps.js +2 -0
  10. package/dist/boot/execution-env.js +12 -2
  11. package/dist/boot/memory-consolidation.d.ts +2 -1
  12. package/dist/boot/parked-revive-gate.d.ts +2 -0
  13. package/dist/boot/parked-revive-gate.js +5 -1
  14. package/dist/boot/resolve-spec.js +8 -3
  15. package/dist/boot/runner-deps.d.ts +48 -59
  16. package/dist/boot/runner-deps.js +10 -4
  17. package/dist/brain.d.ts +3 -1
  18. package/dist/brain.js +2 -8
  19. package/dist/budget.d.ts +12 -1
  20. package/dist/budget.js +1 -1
  21. package/dist/config-catalog.js +13 -11
  22. package/dist/config-center/apply-effective.js +2 -4
  23. package/dist/config-center/facade.d.ts +1 -1
  24. package/dist/config-center/restart-signal.js +2 -2
  25. package/dist/config-center/skills-mcp.js +15 -0
  26. package/dist/config-types.d.ts +86 -4
  27. package/dist/config-types.js +19 -0
  28. package/dist/config.d.ts +42 -0
  29. package/dist/config.js +71 -22
  30. package/dist/deployment-governance.d.ts +6 -3
  31. package/dist/deployment-governance.js +2 -1
  32. package/dist/execution-lane-caps.d.ts +51 -37
  33. package/dist/execution-lane-caps.js +23 -0
  34. package/dist/fleet/fleet-bus.d.ts +22 -1
  35. package/dist/fleet/fleet-bus.js +2 -1
  36. package/dist/http/durable-run-leg.d.ts +64 -0
  37. package/dist/http/durable-run-leg.js +63 -0
  38. package/dist/http/route-ctx.d.ts +14 -0
  39. package/dist/http/route-ctx.js +3 -0
  40. package/dist/http/routes/approvals-assistant.js +3 -3
  41. package/dist/http/routes/capabilities.js +2 -2
  42. package/dist/http/routes/runs.js +10 -58
  43. package/dist/http/routes/workflows.js +1 -0
  44. package/dist/http/server.js +72 -22
  45. package/dist/index.d.ts +1 -1
  46. package/dist/leader/wire.js +4 -3
  47. package/dist/main.js +8 -3
  48. package/dist/memory-operator-faces.d.ts +1 -1
  49. package/dist/memory-scope.d.ts +24 -0
  50. package/dist/memory-scope.js +12 -3
  51. package/dist/observability/fail-open.d.ts +1 -1
  52. package/dist/observability/fail-open.js +1 -1
  53. package/dist/observability/metrics.d.ts +4 -0
  54. package/dist/observability/metrics.js +3 -1
  55. package/dist/observability/run-terminal-log.d.ts +4 -3
  56. package/dist/observability/run-terminal-log.js +4 -15
  57. package/dist/observability/tool-trace.d.ts +18 -1
  58. package/dist/observability/tool-trace.js +4 -3
  59. package/dist/orchestration/workflow-agent-session-index.d.ts +85 -2
  60. package/dist/orchestration/workflow-agent-session-index.js +38 -5
  61. package/dist/parked-decide.d.ts +40 -1
  62. package/dist/parked-decide.js +22 -4
  63. package/dist/plugins/checkpoint-store-sql.js +5 -5
  64. package/dist/plugins/e2b-orphan-reclaim.d.ts +4 -4
  65. package/dist/plugins/e2b-orphan-reclaim.js +18 -20
  66. package/dist/plugins/local-checkpoint-store.js +3 -3
  67. package/dist/plugins/remote-env-adb.d.ts +11 -0
  68. package/dist/plugins/remote-env-adb.js +2 -0
  69. package/dist/plugins/remote-env-device.d.ts +9 -0
  70. package/dist/plugins/remote-env-device.js +3 -0
  71. package/dist/plugins/remote-env-e2b.d.ts +11 -0
  72. package/dist/plugins/remote-env-e2b.js +2 -0
  73. package/dist/plugins/remote-env-host.d.ts +11 -0
  74. package/dist/plugins/remote-env-host.js +2 -0
  75. package/dist/plugins/remote-env-k8s.d.ts +11 -0
  76. package/dist/plugins/remote-env-k8s.js +2 -0
  77. package/dist/plugins/remote-env-local-docker.d.ts +11 -0
  78. package/dist/plugins/remote-env-local-docker.js +2 -0
  79. package/dist/plugins/remote-env-ssh.d.ts +11 -0
  80. package/dist/plugins/remote-env-ssh.js +2 -0
  81. package/dist/plugins/store-backend.d.ts +34 -12
  82. package/dist/plugins/store-backend.js +12 -3
  83. package/dist/plugins/store-contracts.d.ts +81 -18
  84. package/dist/plugins/store-contracts.js +18 -30
  85. package/dist/plugins/workflow-run-store-sql.d.ts +31 -9
  86. package/dist/plugins/workflow-run-store-sql.js +28 -1
  87. package/dist/run-cancel-context.js +1 -1
  88. package/dist/run-local.js +7 -1
  89. package/dist/runs.d.ts +16 -2
  90. package/dist/runs.js +2 -1
  91. package/dist/runtime-caps-resolver.js +5 -3
  92. package/dist/task-a2a.js +2 -1
  93. package/dist/task-mcp.js +2 -1
  94. package/dist/task-settings.d.ts +21 -8
  95. package/dist/task-settings.js +2 -2
  96. package/dist/tool-approval.d.ts +26 -21
  97. package/dist/tool-approval.js +2 -2
  98. package/dist/trace/core-keyset-guard.d.ts +136 -10
  99. package/dist/trace/engine-notice-wire.d.ts +1 -1
  100. package/dist/trace/engine-notice-wire.js +1 -0
  101. package/dist/trace/ledger-events.d.ts +5 -1
  102. package/dist/trace/ledger-sink.js +8 -1
  103. package/dist/trace/project.d.ts +33 -4
  104. package/dist/trace/project.js +24 -6
  105. package/dist/trace/redact.js +10 -5
  106. package/dist/usage-analytics.d.ts +9 -1
  107. package/dist/usage-analytics.js +8 -0
  108. package/dist/wire-name-bound.d.ts +14 -0
  109. package/dist/wire-name-bound.js +13 -0
  110. package/package.json +3 -3
@@ -49,6 +49,7 @@ export class RemoteK8sExecutionEnv {
49
49
  handleSnapshotId;
50
50
  inFlightExecs = 0;
51
51
  connectPromise;
52
+ homeDir;
52
53
  cfg;
53
54
  deps;
54
55
  constructor(config, deps) {
@@ -75,6 +76,7 @@ export class RemoteK8sExecutionEnv {
75
76
  ...(config.podEnv ? { podEnv: config.podEnv } : {}),
76
77
  };
77
78
  this.cwd = this.cfg.mountPath;
79
+ this.homeDir = config.homeDir;
78
80
  this.deps = deps ?? {};
79
81
  this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot };
80
82
  const bgMax = Math.max(1, Math.min(K8S_BG_MAX_TIMEOUT_SEC, Math.floor(this.cfg.timeoutMs / 1000)));
@@ -10,6 +10,15 @@ export interface LocalDockerEnvConfig {
10
10
  * in-network `node:20` mirror (SWR deprecated).
11
11
  */
12
12
  image: string;
13
+ /**
14
+ * S-213④ / core [ref] —— 本执行环境的 **home 目录**(`ExecutionEnv.homeDir` 座,绝对路径,本环境自己的
15
+ * 命名空间里)。`~/…` 形权限规则(`Edit(~/.ssh/**)`)对**跑在本 env 里的调用**就以它为基。
16
+ * 缺席 ⇒ 本 env 声明「我没有 home」,core 对 `~/` 规则判 `unreadable`(fail-closed 的 ask),
17
+ * 绝不回落引擎进程自己的 home(那在远端腿上是另一个用户的另一个目录 —— 静默守错目录正是 [ref] 的病)。
18
+ * 值由 `boot/execution-env.ts` 从**唯一属主** `execution-lane-caps.ts` 的 `executionLaneHomeDir` 递进来;
19
+ * adapter 不自算(两处各算一份 = 422 门与引擎对同一条规则给相反答案)。
20
+ */
21
+ homeDir?: string;
13
22
  /** Workspace root INSIDE the container; relative paths resolve against it. Default {@link DEFAULT_MOUNT_PATH}. */
14
23
  mountPath?: string;
15
24
  /** Path to the docker binary (or a docker-compatible CLI). Default "docker" (must be on PATH in the worker). */
@@ -73,6 +82,8 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
73
82
  };
74
83
  /** Working directory inside the container; relative paths resolve against it (ExecutionEnv contract). */
75
84
  cwd: string;
85
+ /** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
86
+ readonly homeDir?: string | undefined;
76
87
  private readonly cfg;
77
88
  private readonly spawnFn;
78
89
  /** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */
@@ -20,6 +20,7 @@ const unsupported = (op) => ({
20
20
  export class RemoteLocalDockerExecutionEnv {
21
21
  capabilities = { isolation: true, suspendable: false };
22
22
  cwd;
23
+ homeDir;
23
24
  cfg;
24
25
  spawnFn;
25
26
  containerName;
@@ -41,6 +42,7 @@ export class RemoteLocalDockerExecutionEnv {
41
42
  controlTimeoutMs: config.controlTimeoutMs ?? DEFAULT_CONTROL_TIMEOUT_MS,
42
43
  };
43
44
  this.cwd = this.cfg.mountPath;
45
+ this.homeDir = config.homeDir;
44
46
  this.spawnFn = deps.spawn ?? spawn;
45
47
  const id = config.id ?? randomBytes(6).toString("hex");
46
48
  const suffix = randomBytes(4).toString("hex");
@@ -8,6 +8,15 @@ export interface SshEnvConfig {
8
8
  /** PEM private key — from the adapter-held secret env (NEVER the workspace). [ref] §5 #2. */
9
9
  privateKey: string;
10
10
  passphrase?: string;
11
+ /**
12
+ * S-213④ / core [ref] —— 本执行环境的 **home 目录**(`ExecutionEnv.homeDir` 座,绝对路径,本环境自己的
13
+ * 命名空间里)。`~/…` 形权限规则(`Edit(~/.ssh/**)`)对**跑在本 env 里的调用**就以它为基。
14
+ * 缺席 ⇒ 本 env 声明「我没有 home」,core 对 `~/` 规则判 `unreadable`(fail-closed 的 ask),
15
+ * 绝不回落引擎进程自己的 home(那在远端腿上是另一个用户的另一个目录 —— 静默守错目录正是 [ref] 的病)。
16
+ * 值由 `boot/execution-env.ts` 从**唯一属主** `execution-lane-caps.ts` 的 `executionLaneHomeDir` 递进来;
17
+ * adapter 不自算(两处各算一份 = 422 门与引擎对同一条规则给相反答案)。
18
+ */
19
+ homeDir?: string;
11
20
  /** Workspace root; relative paths resolve against it. Default "." (the login dir). */
12
21
  mountPath?: string;
13
22
  /** Connect handshake bound (ms). */
@@ -72,6 +81,8 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
72
81
  * =重连风暴);②共享尝试**不携带任何 caller 信号**(首创者的 pre-abort 不得毒化 joiner;单 caller
73
82
  * 取消只退它自己的等待,底层握手余命 ≤ readyTimeout 有界,彻底取消归 destroy 代际闸)。 */
74
83
  private inFlight?;
84
+ /** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
85
+ readonly homeDir?: string | undefined;
75
86
  private readonly cfg;
76
87
  private readonly injectedConnectOnce?;
77
88
  constructor(config: SshEnvConfig, deps?: SshEnvDeps);
@@ -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,92 @@ 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
+ * 一个状态词的类;**表里没有的词 ⇒ `undefined`**。
152
+ *
153
+ * 🔴 形参是 `string` 而不是 `RunRecord["status"]`(S-221):本函数的**全部工作**就是「这个词在不在表里」,
154
+ * 把入参收成闭集等于在签名上宣称「传进来的一定是表内词」,而下一行的 `undefined` 臂正是为了不是的那些 ——
155
+ * 签名与实现自相矛盾时,消费点只能靠 `as RunRecord["status"]` 硬转把窄读口(`{ status: string }`,
156
+ * 三形 store 共同面)喂进来,而那正是本文件头注点名的 `rowToRun` 裸 cast 的同一条病。闭集词照样能传
157
+ * (联合 ⊂ string),编译期的执法点在上面那张表的双向钉,不在这里。
158
+ *
159
+ * 导出面(S-221):`plugins/e2b-orphan-reclaim.ts` 的三臂判决(还能再动 / 终局 / 读不懂)需要的正是
160
+ * 「类 + unknown」这一对,而不是三条 boolean 谓词各问一遍 —— 它此前因此自带了第二份词表。
161
+ *
162
+ * 🔴 查的是 **null 原型**的那只表,不是 `RUN_STATUS_CLASS` 本身(codex 对抗复审 r1 [high],亲验后修):
163
+ * 普通对象字面量的查表**穿原型链** —— `status` 落在 `constructor` / `toString` / `__proto__` /
164
+ * `hasOwnProperty` / `valueOf` 上时查到的是 `Object.prototype` 的成员而不是 `undefined`,于是
165
+ * 「读不懂的词」这一臂被整条绕过。亲验读数(修前):`runStatusClassOf("constructor")` 返回
166
+ * `function Object`。这类值是**真可达**的(`rowToRun` 把无约束的 SQL 文本列裸 cast 成本联合),而消费点
167
+ * 据此做的是不可逆动作。修在此处一次,而不是在每个消费点各写一句 `Object.hasOwn` —— 后者要靠每个消费点
168
+ * 各记得一次,属主只有一个。
169
+ *
170
+ * 🔴 运行期未知词是**真可达**的([ref] codex R1 真缺陷,红先复现过):`plugins/run-store-sql.ts` 的
171
+ * `rowToRun` 把无约束的 SQL 文本列**裸 cast** 成本联合,滚动升级里一个更新的副本写入的新状态词会被
172
+ * 旧副本读到。三张派生判据对 `undefined` 一律答 `false`(保守方向,逐条理由见各自头注)——不是静默
173
+ * 兜底,而是「本进程读不懂这个词,少动一次远好过错杀」的显式表态;真正的执法点是上面那张表的编译期
174
+ * 双向钉,新词进词表的**那一刻**就红。
175
+ */
176
+ export declare function runStatusClassOf(status: string): RunStatusClass | undefined;
177
+ /**
178
+ * 「这一行已经走到**终局**了吗」——终局 = 成功 or 非成功(completed / failed / blocked);
179
+ * 非终局 = running / suspended / needs_review(后两者都还能再动)。
180
+ *
181
+ * 未知词判**非终局**:判成「终局」的代价不可逆 —— `approval-reconciler` 判据 2 会把一张还活着的 ask
182
+ * 永久打成 DENIED/routing_failure,取消路由也会把这一行当终局上报。
127
183
  */
128
184
  export declare function isTerminalRunStatus(status: RunRecord["status"]): boolean;
129
185
  /**
130
- * 「这一行**停在门上**(park)了吗」——`RunRecord["status"]` 的第二张穷举判据,与 {@link isTerminalRunStatus}
131
- * 同住词表属主处(扫描P2,2026-08-12)。
186
+ * 「这一行的终局是**非成功**的吗」(S-213③ 的共享判据)——`failed` 与 `blocked` 两词。
187
+ *
188
+ * 🔴 它回答的问题与 {@link isTerminalRunStatus} **不同**:后者只问「还会不会再动」,本判据问「这次任务
189
+ * 到底成没成」。凡是「这条 run 该不该在失败面上出声/离场」的判断都读这一条,不许再手抄
190
+ * `=== "failed" || === "blocked"`(那正是 `blocked` 长出两个互斥读法的成因)。
191
+ *
192
+ * 未知词判**不是**非成功:对读不懂的词保持沉默,好过按 error 级刷屏。
193
+ */
194
+ export declare function isUnsuccessfulRunStatus(status: RunRecord["status"]): boolean;
195
+ /**
196
+ * 「这一行**停在门上**(park)了吗」(扫描P2,2026-08-12)。
132
197
  *
133
198
  * 🔴 为什么必须有名字:park 是**两个词**,不是一个 —— `suspended`(tool_approval / policy_ask / human /
134
199
  * resource_limit 门)与 `needs_review`(`plan_review` / dry-run 拦截门,写点 = 各 store 的
@@ -139,8 +204,6 @@ export declare function isTerminalRunStatus(status: RunRecord["status"]): boolea
139
204
  *
140
205
  * 未知词的方向与 {@link isTerminalRunStatus} 同向保守:判**不是** park。理由是消费点会据此做**不可逆**
141
206
  * 动作(cancel 的恢复把手会 CAS-expire 一张 checkpoint);对一个本进程读不懂的状态,少动一次远好过错杀。
142
- * 真正的执法点是下面那条 `never`:core 的 `TaskStatus` 一加成员就 tsc 红,加成员的人必须当场回答
143
- * 「这个新态算不算 park」。
144
207
  */
145
208
  export declare function isParkedRunStatus(status: RunRecord["status"]): boolean;
146
209
  /** One row of the GET /v1/sessions list (CC /resume picker): a DISTINCT session (sessionId) aggregated from its
@@ -1,37 +1,25 @@
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
+ const RUN_STATUS_CLASS_LOOKUP = Object.assign(Object.create(null), RUN_STATUS_CLASS);
11
+ export function runStatusClassOf(status) {
12
+ return RUN_STATUS_CLASS_LOOKUP[status];
13
+ }
2
14
  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
- }
15
+ const cls = runStatusClassOf(status);
16
+ return cls === "succeeded" || cls === "unsuccessful";
17
+ }
18
+ export function isUnsuccessfulRunStatus(status) {
19
+ return runStatusClassOf(status) === "unsuccessful";
18
20
  }
19
21
  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
- }
22
+ return runStatusClassOf(status) === "parked";
35
23
  }
36
24
  export function mapBakeRow(r) {
37
25
  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 {
@@ -30,7 +30,7 @@ export function mintCancelledResult(args) {
30
30
  sessionId: args.sessionId,
31
31
  terminal: { kind: "failed", code: CANCELLED_CODE, message: args.message },
32
32
  result: "",
33
- stats: { turns: 0, tokens: 0 },
33
+ stats: { turns: 0, tokens: 0, usageMissing: true },
34
34
  ...(ctx ? { cancelContext: ctx } : {}),
35
35
  };
36
36
  }
package/dist/run-local.js CHANGED
@@ -15,10 +15,11 @@ import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js
15
15
  import { taskWallClockSec } from "./task-workflow.js";
16
16
  import { applyCatalogToSource } from "./capabilities/center-prompts.js";
17
17
  import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
18
- import { loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
18
+ import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
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";
@@ -197,6 +198,9 @@ export async function runLocal(argv, deps = {}) {
197
198
  catch (err) {
198
199
  logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
199
200
  }
201
+ const unpricedCeiling = buildUnpricedCeilingNotice(config.models, config);
202
+ if (unpricedCeiling)
203
+ logger.warn(unpricedCeiling.event, unpricedCeiling.fields);
200
204
  try {
201
205
  const fileRx = await loadRemoteExec(root);
202
206
  if (fileRx?.provider === "host") {
@@ -273,6 +277,7 @@ export async function runLocal(argv, deps = {}) {
273
277
  const executionEnvFactory = hostRx
274
278
  ? hostExecutionEnvFactory({
275
279
  workspaceDir,
280
+ ...((h) => (h === undefined ? {} : { homeDir: h }))(executionLaneHomeDir(config)),
276
281
  backgroundShell: config.requirePrincipal !== true,
277
282
  ...(hostRx.commandTimeoutMs != null ? { commandTimeoutMs: hostRx.commandTimeoutMs } : {}),
278
283
  })
@@ -369,6 +374,7 @@ export async function runLocal(argv, deps = {}) {
369
374
  ],
370
375
  });
371
376
  const sharedRunnerDeps = createSharedRunnerDeps({
377
+ metrics,
372
378
  config,
373
379
  governanceSeams,
374
380
  logger,
package/dist/runs.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { defaultTaskRegistry, type CascadeConfig, type CheckpointToken, type Runner, type SubagentSteerHandle, type TaskResult, type TaskSpec, type TaskStream, type TerminalCause } from "@sema-agent/core";
1
+ import { defaultTaskRegistry, type CascadeConfig, type CheckpointToken, type RunInternals, type Runner, type SubagentSteerHandle, type TaskResult, type TaskSpec, type TaskStream, type TerminalCause } from "@sema-agent/core";
2
2
  import type { SendUserFileEmitter } from "./capabilities/send-user-file-tool.js";
3
3
  import type { RunStore } from "./plugins/store-backend.js";
4
4
  import type { Metrics } from "./observability/metrics.js";
@@ -282,7 +282,21 @@ pendingEngineNotices?: readonly {
282
282
  message: string;
283
283
  detail?: Record<string, unknown>;
284
284
  sessionId?: unknown;
285
- }[]): Promise<void>;
285
+ }[],
286
+ /**
287
+ * 🔴 **S-218:部署方铸的 TRUSTED run internals**(今天唯一成员 `workflowParkedResume`)。
288
+ *
289
+ * 它**不是** `TaskSpec` 字段、不是工具参数、不进请求体 —— 模型永远不能自己决定一次审批。铸点只有
290
+ * 一个:`/decide` 的 workflow 第三条车道(判据属主 = `parked-decide.ts` 的 `planWorkflowParkedDecide`
291
+ * 顶注 + `http/durable-run-leg.ts` 的 `TrustedRunInternals`)。本腿只负责把它摊进**下面那只**共享
292
+ * 供给座席,core 的 `prepareTask` 据它给 Workflow 工具挂 `parkedResume` dep。
293
+ *
294
+ * 位置在座席对象的**最后**:座席里那几位(onActivity/onForwardEvent/onTaskNotification/onSubagentSpawn)
295
+ * 是本腿自己的执行面供给,调用方无权覆盖它们 —— 真要撞名,撞的也只能是本腿自己新加的位,而那时
296
+ * `TrustedRunInternals` 的白名单会在编译期先红一次(它只开部署方可以决定的那一格)。
297
+ * 缺席 ⇒ 座席与本位存在之前逐字相同。
298
+ */
299
+ trustedInternals?: Pick<RunInternals, "workflowParkedResume">): Promise<void>;
286
300
  /** How often a running instance refreshes its run's updated_at (liveness, independent of events).
287
301
  * Must stay strictly below `runStaleSec` (asserted at startup AND on every hot config candidate) or the
288
302
  * reaper would race live runs. [ref]:数值的单源是 `config-invariants.ts`(那条不变量的属主),两个消费面
package/dist/runs.js CHANGED
@@ -160,7 +160,7 @@ export function evictIfConflict(runner, sessionId, result) {
160
160
  runner.sessions.evict?.(sessionId);
161
161
  }
162
162
  }
163
- export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices) {
163
+ export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices, trustedInternals) {
164
164
  const startedAt = Date.now();
165
165
  metrics?.addGauge("runs_active", 1);
166
166
  fleetPublisher?.onStart();
@@ -311,6 +311,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
311
311
  },
312
312
  }
313
313
  : {}),
314
+ ...(trustedInternals ?? {}),
314
315
  };
315
316
  const statusSeat = {
316
317
  onStatusEvent: (e) => {
@@ -45,6 +45,8 @@ export function toCoreRuntimeCaps(ent) {
45
45
  caps.allowObservers = ent.allowObservers;
46
46
  if (typeof autoMode === "boolean")
47
47
  caps.autoMode = autoMode;
48
+ if (caps.autoMode === false)
49
+ caps.autoModeDenySource = "org";
48
50
  if (typeof ent.allowMemoryOptOut === "boolean")
49
51
  caps.allowMemoryOptOut = ent.allowMemoryOptOut;
50
52
  return Object.keys(caps).length ? caps : undefined;
@@ -142,7 +144,7 @@ export function createPrincipalEntitlementsClient(opts) {
142
144
  catch {
143
145
  e = new Error("center fetch failed with an unstringifiable rejection reason");
144
146
  }
145
- const entry = { caps: { allowWorkflows: false, allowFork: false, autoMode: false }, expiresAt: now() + denyTtlMs };
147
+ const entry = { caps: { allowWorkflows: false, allowFork: false, autoMode: false, autoModeDenySource: "org" }, expiresAt: now() + denyTtlMs };
146
148
  if (!recordDeny)
147
149
  readMintedDeny.add(entry);
148
150
  putCache(principal, entry);
@@ -231,7 +233,7 @@ export function applyLocalAutoModeDeny(base, deny) {
231
233
  if (!deny)
232
234
  return base;
233
235
  if (base === undefined)
234
- return () => ({ autoMode: false });
235
- return async (principal) => ({ ...(await base(principal)), autoMode: false });
236
+ return () => ({ autoMode: false, autoModeDenySource: "local" });
237
+ return async (principal) => ({ ...(await base(principal)), autoMode: false, autoModeDenySource: "local" });
236
238
  }
237
239
  //# sourceMappingURL=runtime-caps-resolver.js.map