@sema-agent/server 7.103.2 → 7.105.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 (204) hide show
  1. package/MIGRATION.md +378 -2
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +320 -16
  5. package/deploy/sema-up/README.md +57 -7
  6. package/deploy/sema-up/cluster-up.sh +18 -4
  7. package/deploy/sema-up/kube-up.sh +16 -0
  8. package/deploy/sema-up/lib/dataroot.sh +142 -0
  9. package/deploy/sema-up/lib/flagshape.sh +69 -0
  10. package/deploy/sema-up/lib/lanes.py +14 -4
  11. package/deploy/sema-up/lib/shell-access.sh +41 -0
  12. package/deploy/sema-up/preflight.sh +14 -1
  13. package/deploy/sema-up/sema-up.sh +134 -37
  14. package/deploy/sema-up/smoke.sh +5 -0
  15. package/dist/auto-mode-face.js +3 -4
  16. package/dist/boot/budget-tracing.js +1 -1
  17. package/dist/boot/config-center.d.ts +6 -2
  18. package/dist/boot/config-center.js +5 -59
  19. package/dist/boot/execution-env.d.ts +11 -0
  20. package/dist/boot/execution-env.js +14 -0
  21. package/dist/boot/execution-lane.d.ts +25 -0
  22. package/dist/boot/execution-lane.js +95 -0
  23. package/dist/boot/memory-consolidation.d.ts +185 -9
  24. package/dist/boot/memory-consolidation.js +192 -32
  25. package/dist/boot/resolve-spec.d.ts +2 -0
  26. package/dist/boot/resolve-spec.js +28 -24
  27. package/dist/boot/runner-deps.d.ts +6 -2
  28. package/dist/boot/runner-deps.js +6 -1
  29. package/dist/boot/session-faces.d.ts +5 -0
  30. package/dist/boot/session-faces.js +18 -2
  31. package/dist/boot/session-incarnation-seat.d.ts +41 -0
  32. package/dist/boot/session-incarnation-seat.js +69 -0
  33. package/dist/boot/stage-01-config.js +2 -2
  34. package/dist/boot/stage-02-registry-pull.js +3 -1
  35. package/dist/boot/stage-04-stores.d.ts +2 -0
  36. package/dist/boot/stage-04-stores.js +3 -1
  37. package/dist/boot/stage-05-execution-env.d.ts +4 -0
  38. package/dist/boot/stage-05-execution-env.js +2 -2
  39. package/dist/boot/stage-06-runners.d.ts +4 -1
  40. package/dist/boot/stage-06-runners.js +7 -14
  41. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  42. package/dist/boot/stage-08-reapers.d.ts +4 -1
  43. package/dist/boot/stage-09-leader.d.ts +6 -1
  44. package/dist/boot/stage-09-leader.js +2 -1
  45. package/dist/boot/stage-10-http-server.d.ts +6 -1
  46. package/dist/boot/stage-10-http-server.js +4 -1
  47. package/dist/brain.d.ts +1 -1
  48. package/dist/config-catalog.d.ts +16 -1
  49. package/dist/config-catalog.js +9 -9
  50. package/dist/config-center/restart-signal.d.ts +4 -4
  51. package/dist/config-invariants.d.ts +7 -1
  52. package/dist/config-invariants.js +37 -6
  53. package/dist/config-provider.d.ts +42 -10
  54. package/dist/config-provider.js +18 -16
  55. package/dist/config-types.d.ts +37 -16
  56. package/dist/config-types.js +1 -0
  57. package/dist/config.d.ts +28 -30
  58. package/dist/config.js +95 -77
  59. package/dist/env-facts.d.ts +13 -3
  60. package/dist/env-facts.js +15 -3
  61. package/dist/execution-lane-caps.d.ts +15 -0
  62. package/dist/execution-lane-caps.js +8 -0
  63. package/dist/file-history-capture.d.ts +42 -35
  64. package/dist/file-history-capture.js +15 -10
  65. package/dist/file-history-disclosure.d.ts +7 -31
  66. package/dist/file-history-disclosure.js +12 -0
  67. package/dist/fleet/fleet-bus.d.ts +5 -1
  68. package/dist/http/admission.d.ts +12 -5
  69. package/dist/http/admission.js +5 -1
  70. package/dist/http/dispatch.d.ts +18 -0
  71. package/dist/http/dispatch.js +19 -6
  72. package/dist/http/principal-gate.d.ts +22 -0
  73. package/dist/http/principal-gate.js +20 -0
  74. package/dist/http/resume-legs.d.ts +1 -1
  75. package/dist/http/resume-legs.js +9 -5
  76. package/dist/http/route-table.d.ts +15 -4
  77. package/dist/http/route-table.js +6 -5
  78. package/dist/http/routes/agents-roster.d.ts +7 -0
  79. package/dist/http/routes/agents-roster.js +6 -2
  80. package/dist/http/routes/approvals-assistant.d.ts +48 -19
  81. package/dist/http/routes/approvals-assistant.js +117 -50
  82. package/dist/http/routes/capabilities.js +3 -2
  83. package/dist/http/routes/config-catalog.js +1 -0
  84. package/dist/http/routes/fleet.d.ts +3 -2
  85. package/dist/http/routes/images.js +0 -1
  86. package/dist/http/routes/memory-consolidation.d.ts +3 -2
  87. package/dist/http/routes/memory-policy.js +3 -3
  88. package/dist/http/routes/rules.js +2 -2
  89. package/dist/http/routes/runs.js +29 -10
  90. package/dist/http/routes/session-sync.js +14 -15
  91. package/dist/http/routes/sessions-list.js +3 -2
  92. package/dist/http/routes/sessions.js +49 -25
  93. package/dist/http/routes/shared-memory.js +2 -2
  94. package/dist/http/routes/side-query.js +8 -1
  95. package/dist/http/routes/tasks.js +22 -19
  96. package/dist/http/routes/trace-usage.d.ts +2 -0
  97. package/dist/http/routes/trace-usage.js +8 -13
  98. package/dist/http/send.d.ts +12 -0
  99. package/dist/http/send.js +12 -1
  100. package/dist/http/server.d.ts +30 -8
  101. package/dist/http/server.js +11 -5
  102. package/dist/http/sse-lifecycle.d.ts +1 -1
  103. package/dist/http/sse-log.d.ts +12 -9
  104. package/dist/http/sse-log.js +1 -8
  105. package/dist/http/sync-submit-body.d.ts +43 -0
  106. package/dist/http/sync-submit-body.js +4 -0
  107. package/dist/http/wire-types.d.ts +23 -5
  108. package/dist/index.d.ts +1 -0
  109. package/dist/index.js +1 -0
  110. package/dist/leader/merge.d.ts +8 -3
  111. package/dist/memory-incident-sink.js +2 -1
  112. package/dist/model-select.d.ts +9 -0
  113. package/dist/model-select.js +5 -1
  114. package/dist/observability/corrupt-read-seat.d.ts +151 -16
  115. package/dist/observability/corrupt-read-seat.js +158 -15
  116. package/dist/observability/fail-open.d.ts +17 -1
  117. package/dist/observability/fail-open.js +17 -1
  118. package/dist/observability/metrics.js +1 -1
  119. package/dist/observability/run-terminal.d.ts +60 -10
  120. package/dist/observability/run-terminal.js +119 -9
  121. package/dist/parked-decide.d.ts +8 -3
  122. package/dist/parked-decide.js +56 -20
  123. package/dist/parked-revive-surface.d.ts +14 -7
  124. package/dist/parked-revive-surface.js +5 -1
  125. package/dist/plugins/approval-ask-store-file.d.ts +4 -1
  126. package/dist/plugins/approval-ask-store-file.js +9 -12
  127. package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
  128. package/dist/plugins/approval-ask-store-sql.js +4 -1
  129. package/dist/plugins/approval-exemption-store.d.ts +21 -1
  130. package/dist/plugins/approval-exemption-store.js +36 -7
  131. package/dist/plugins/approval-nonce-store.d.ts +16 -2
  132. package/dist/plugins/approval-nonce-store.js +17 -7
  133. package/dist/plugins/caching-session-store.d.ts +5 -0
  134. package/dist/plugins/caching-session-store.js +3 -0
  135. package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
  136. package/dist/plugins/file-resume-anchor-store.js +19 -5
  137. package/dist/plugins/file-run-store.d.ts +21 -8
  138. package/dist/plugins/file-run-store.js +60 -38
  139. package/dist/plugins/image-bake-store-sql.d.ts +0 -2
  140. package/dist/plugins/image-bake-store-sql.js +0 -5
  141. package/dist/plugins/local-session-store.d.ts +22 -0
  142. package/dist/plugins/local-session-store.js +74 -11
  143. package/dist/plugins/memory-run-store.d.ts +7 -5
  144. package/dist/plugins/memory-run-store.js +11 -4
  145. package/dist/plugins/one-time-migrations.d.ts +38 -1
  146. package/dist/plugins/one-time-migrations.js +143 -15
  147. package/dist/plugins/permission-rule-store-file.d.ts +9 -2
  148. package/dist/plugins/permission-rule-store-file.js +37 -51
  149. package/dist/plugins/pg-session-storage.d.ts +3 -0
  150. package/dist/plugins/pg-session-storage.js +19 -10
  151. package/dist/plugins/retention-store-sql.js +1 -0
  152. package/dist/plugins/run-store-sql.d.ts +15 -6
  153. package/dist/plugins/run-store-sql.js +15 -13
  154. package/dist/plugins/schema-indexes.js +1 -0
  155. package/dist/plugins/send-file-ledger.d.ts +5 -1
  156. package/dist/plugins/send-file-ledger.js +11 -6
  157. package/dist/plugins/session-event-payload.d.ts +14 -4
  158. package/dist/plugins/session-event-payload.js +5 -2
  159. package/dist/plugins/session-policy-store-sql.js +18 -5
  160. package/dist/plugins/store-backend.d.ts +13 -7
  161. package/dist/plugins/store-backend.js +10 -6
  162. package/dist/plugins/store-contracts.d.ts +7 -2
  163. package/dist/plugins/tidb-pool.js +12 -5
  164. package/dist/plugins/tidb-session-store.d.ts +6 -1
  165. package/dist/plugins/tidb-session-store.js +17 -10
  166. package/dist/project-identity.d.ts +4 -6
  167. package/dist/read-face-posture.d.ts +21 -0
  168. package/dist/read-face-posture.js +9 -0
  169. package/dist/request-key-closure.js +1 -1
  170. package/dist/request-value-closure.d.ts +72 -3
  171. package/dist/request-value-closure.js +12 -4
  172. package/dist/roster-bound-http-status.d.ts +6 -0
  173. package/dist/roster-bound-http-status.js +6 -0
  174. package/dist/rules-consent.js +0 -11
  175. package/dist/run-local.js +22 -27
  176. package/dist/runs.d.ts +22 -2
  177. package/dist/runs.js +29 -7
  178. package/dist/sealed-key.d.ts +31 -4
  179. package/dist/sealed-key.js +9 -0
  180. package/dist/security.d.ts +5 -1
  181. package/dist/session-children-retire.d.ts +92 -0
  182. package/dist/session-children-retire.js +85 -0
  183. package/dist/session-error-wire.d.ts +58 -0
  184. package/dist/session-error-wire.js +38 -0
  185. package/dist/session-incarnation.d.ts +61 -0
  186. package/dist/session-incarnation.js +43 -0
  187. package/dist/session-policy-wire.d.ts +98 -12
  188. package/dist/session-policy-wire.js +71 -8
  189. package/dist/session-records-skipped-notice.d.ts +20 -0
  190. package/dist/session-records-skipped-notice.js +27 -0
  191. package/dist/tool-approval.d.ts +65 -0
  192. package/dist/tool-approval.js +36 -9
  193. package/dist/trace/core-keyset-guard.d.ts +8 -7
  194. package/dist/trace/engine-notice-wire.d.ts +3 -1
  195. package/dist/trace/engine-notice-wire.js +2 -0
  196. package/dist/trace/project.d.ts +11 -6
  197. package/dist/trace/project.js +12 -3
  198. package/dist/trace/redact.d.ts +7 -0
  199. package/dist/trace/redact.js +153 -61
  200. package/dist/trace/wire-projection-faces.d.ts +10 -3
  201. package/dist/trace/wire-projection-faces.js +8 -2
  202. package/package.json +2 -2
  203. package/dist/plugins/memory-session-policy-store.d.ts +0 -46
  204. package/dist/plugins/memory-session-policy-store.js +0 -52
@@ -5,6 +5,7 @@ import { jsonColumnText } from "./sql-json-column.js";
5
5
  import { escapeLike } from "./sql-escape.js";
6
6
  import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
7
7
  import { MANAGED_RETENTION } from "./retention-store-sql.js";
8
+ import { carriedIncarnation, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
8
9
  import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
9
10
  import { assertPlacementAdmission, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SQL_SESSION_PLACEMENTS, } from "./session-placement.js";
10
11
  import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
@@ -52,9 +53,9 @@ export class TiDBSessionStore {
52
53
  }
53
54
  async register(sessionId, owner, placement) {
54
55
  const now = new Date();
55
- await this.pool.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, " +
56
+ await this.pool.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation, " +
56
57
  "placement_kind, placement_scope, placement_parent_session_id, placement_root_session_id, placement_handle, placed_at) " +
57
- "VALUES (?,?,?,?,NULL,0,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE session_id = session_id", [sessionId, owner, now, now, ...placementInsertValues(placement, now)]);
58
+ "VALUES (?,?,?,?,NULL,0,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE session_id = session_id", [sessionId, owner, now, now, mintSessionIncarnation(), ...placementInsertValues(placement, now)]);
58
59
  }
59
60
  async placementOf(sessionId) {
60
61
  const [rows] = await this.pool.query(`SELECT ${PLACEMENT_SELECT_COLUMNS} FROM session_meta WHERE session_id = ?`, [sessionId]);
@@ -82,6 +83,12 @@ export class TiDBSessionStore {
82
83
  return undefined;
83
84
  return rows[0].owner ?? null;
84
85
  }
86
+ async sessionFaceOf(sessionId) {
87
+ const [rows] = await this.pool.query("SELECT owner, incarnation FROM session_meta WHERE session_id = ?", [sessionId]);
88
+ if (rows.length === 0)
89
+ return undefined;
90
+ return sessionFaceFromRow(sessionId, rows[0].owner ?? null, rows[0].incarnation);
91
+ }
85
92
  async touch(sessionId) {
86
93
  await this.pool.query("UPDATE session_meta SET updated_at = ? WHERE session_id = ?", [
87
94
  new Date(),
@@ -213,7 +220,7 @@ export class TiDBSessionStore {
213
220
  const payload = String(r.payload);
214
221
  await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES (?,?,?,?,?,?,?)", [newId, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
215
222
  }
216
- await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES (?,?,?,?,?,?)", [newId, owner, now, now, leafId, eventRows.length]);
223
+ await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,?,?,?)", [newId, owner, now, now, leafId, eventRows.length, mintSessionIncarnation()]);
217
224
  await conn.commit();
218
225
  return newId;
219
226
  }
@@ -267,7 +274,7 @@ export class TiDBSessionStore {
267
274
  const conn = await this.pool.getConnection();
268
275
  try {
269
276
  await conn.beginTransaction();
270
- await this.insertValidatedLog(conn, sessionId, owner, validated);
277
+ await this.insertValidatedLog(conn, sessionId, owner, validated, mintSessionIncarnation());
271
278
  await conn.commit();
272
279
  }
273
280
  catch (err) {
@@ -290,14 +297,14 @@ export class TiDBSessionStore {
290
297
  const conn = await this.pool.getConnection();
291
298
  try {
292
299
  await conn.beginTransaction();
293
- const [placedRows] = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = ? FOR UPDATE", [sessionId]);
300
+ const [placedRows] = await conn.query("SELECT placement_kind, owner, incarnation FROM session_meta WHERE session_id = ? FOR UPDATE", [sessionId]);
294
301
  if (placedRows[0]?.placement_kind != null) {
295
302
  await conn.rollback();
296
303
  throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${String(placedRows[0].placement_kind)} placement partition — a session-sync replace cannot overwrite a delegation transcript`);
297
304
  }
298
305
  await conn.query("DELETE FROM session_event WHERE session_id = ?", [sessionId]);
299
306
  await conn.query("DELETE FROM session_meta WHERE session_id = ? AND owner <=> ? AND placement_kind IS NULL", [sessionId, owner]);
300
- await this.insertValidatedLog(conn, sessionId, owner, validated);
307
+ await this.insertValidatedLog(conn, sessionId, owner, validated, carriedIncarnation(placedRows[0] === undefined ? undefined : { owner: placedRows[0].owner, incarnation: placedRows[0].incarnation }, owner));
301
308
  await conn.commit();
302
309
  }
303
310
  catch (err) {
@@ -314,7 +321,7 @@ export class TiDBSessionStore {
314
321
  conn.release();
315
322
  }
316
323
  }
317
- async insertValidatedLog(conn, sessionId, owner, validated) {
324
+ async insertValidatedLog(conn, sessionId, owner, validated, incarnation) {
318
325
  let leafId = null;
319
326
  for (const e of validated)
320
327
  leafId = leafIdAfterEntry(e);
@@ -323,7 +330,7 @@ export class TiDBSessionStore {
323
330
  const e = validated[seq];
324
331
  await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES (?,?,?,?,?,?,?)", [sessionId, seq, e.id, e.parentId ?? null, e.type, JSON.stringify(e), new Date(e.timestamp)]);
325
332
  }
326
- await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES (?,?,?,?,?,?)", [sessionId, owner, now, now, leafId, validated.length]);
333
+ await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,?,?,?)", [sessionId, owner, now, now, leafId, validated.length, incarnation]);
327
334
  }
328
335
  beginImportStaging(realSessionId, token) {
329
336
  return new TiDBStagingHandle(this.pool, realSessionId, stagingIdFor(realSessionId, token));
@@ -450,8 +457,8 @@ class TiDBStagingHandle {
450
457
  if (lastRow[0])
451
458
  leafId = leafIdAfterEntry(decodeSessionEventPayload(lastRow[0].payload, this.realSessionId, "last staged row"));
452
459
  const now = new Date();
453
- await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES (?,?,?,?,?,?) " +
454
- "ON DUPLICATE KEY UPDATE owner = VALUES(owner), updated_at = VALUES(updated_at), leaf_id = VALUES(leaf_id), leaf_seq = VALUES(leaf_seq)", [this.realSessionId, owner, now, now, leafId, srcIds.length]);
460
+ await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES (?,?,?,?,?,?,?) " +
461
+ "ON DUPLICATE KEY UPDATE owner = VALUES(owner), updated_at = VALUES(updated_at), leaf_id = VALUES(leaf_id), leaf_seq = VALUES(leaf_seq)", [this.realSessionId, owner, now, now, leafId, srcIds.length, mintSessionIncarnation()]);
455
462
  const [afterMeta] = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = ?", [this.realSessionId]);
456
463
  if (afterMeta[0]?.placement_kind != null) {
457
464
  await conn.rollback();
@@ -1,10 +1,8 @@
1
- import type { TaskSpec } from "@sema-agent/core";
2
- /** core 的项目身份声明席(`TaskSpec.memory.projectKey`)—— 从 core 类型派生,不另写一份。 */
3
- export type MemoryProjectKey = NonNullable<NonNullable<TaskSpec["memory"]>["projectKey"]>;
1
+ import type { MemoryProjectKey, MemoryProjectKeyAbsentReason } from "@sema-agent/core";
2
+ /** core 的项目身份声明席(`TaskSpec.memory.projectKey`)—— core 包根导出的型,原样转出(本仓调用方从这里取)。 */
3
+ export type { MemoryProjectKey };
4
4
  /** 本仓会说的缺席词。`not_git` 刻意排除:裁 D 起非 git 目录按目录键控,不是缺席(core 闭集仍收它,本仓不铸)。 */
5
- export type ProjectIdentityAbsentReason = Exclude<Extract<MemoryProjectKey, {
6
- absent: unknown;
7
- }>["absent"], "not_git">;
5
+ export type ProjectIdentityAbsentReason = Exclude<MemoryProjectKeyAbsentReason, "not_git">;
8
6
  /** 一个任务根的项目身份:canonical 项目根,或说得出理由的缺席。 */
9
7
  export type ProjectIdentity = {
10
8
  readonly root: string;
@@ -65,4 +65,25 @@ export declare function buildReadFaceWiring(facts: ReadFacePostureFacts): {
65
65
  source: ReadFaceSource;
66
66
  note: string;
67
67
  };
68
+ /**
69
+ * S-176 —— posture 派生的档位在 **boot 期推送面**的那一行(`config.ts` 经既有 `CONFIG_NOTICES` 以 info 级排空)。
70
+ *
71
+ * 为什么要有:S-167 的补偿项是「升级即放开,得让运维看得见并知道怎么钉回」,可此前唯一看得见的是**拉取**面
72
+ * (`GET /v1/diagnostics/wiring`)—— 只对会去拉的人成立。单机 boot 日志里零字。
73
+ * 为什么只在 `posture` 一词上说:另外三词都不是「没人表态、服务替它选了一个更宽的档」——`env` 是运维自己写的,
74
+ * `engine-default` 是本服务没铸(引擎默认接管),`center` 在 boot 这一拍还没到货(它有自己的换席通告)。
75
+ * 为什么是 info 不是 warn:每一台照常配置的单机每次启动都成立,是常态事实不是异常(与执行车道落点同级)。
76
+ *
77
+ * 字段与极性表的 posture 行同名(`knob` / `value` / `source`),另带指路句;三格由 {@link buildReadFaceWiring} 投影出 ——
78
+ * 拉取面与推送面**同一只函数**,不各算一次。`value` 是字符串:只有 `face` 在场的那一形才会走到这里。
79
+ */
80
+ export declare function buildReadFacePostureNotice(facts: ReadFacePostureFacts): {
81
+ event: "config_knob_posture_derived";
82
+ fields: {
83
+ knob: "READ_FACE";
84
+ value: string;
85
+ source: ReadFaceSource;
86
+ note: string;
87
+ };
88
+ } | undefined;
68
89
  //# sourceMappingURL=read-face-posture.d.ts.map
@@ -14,4 +14,13 @@ export function readFaceSourceNote(source) {
14
14
  export function buildReadFaceWiring(facts) {
15
15
  return { face: facts.readFace ?? null, source: facts.readFaceSource, note: readFaceSourceNote(facts.readFaceSource) };
16
16
  }
17
+ export function buildReadFacePostureNotice(facts) {
18
+ if (facts.readFaceSource !== "posture")
19
+ return undefined;
20
+ const wiring = buildReadFaceWiring(facts);
21
+ if (wiring.face === null) {
22
+ throw new Error("read-face posture notice: source is `posture` but no face was minted — readFaceForPosture's invariant broke");
23
+ }
24
+ return { event: "config_knob_posture_derived", fields: { knob: "READ_FACE", value: wiring.face, source: wiring.source, note: wiring.note } };
25
+ }
17
26
  //# sourceMappingURL=read-face-posture.js.map
@@ -21,7 +21,7 @@ const TASK_BODY_DECLARED_KEYS = [
21
21
  "a2aPeers", "acceptPartialRestore", "additionalDirectories", "additionalReadDirectories", "agents",
22
22
  "appendSystemPrompt", "approverPosture", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
23
23
  "compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "deviceId", "enableFork",
24
- "excludeAllTools", "excludeTools",
24
+ "excludeAllTools", "excludeTools", "fileHistory",
25
25
  "finalVerification", "forwardSubagentEvents", "images", "interactiveTools", "jobId", "limits", "maxCostUsd",
26
26
  "maxTokens", "mcpServers", "memory", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries",
27
27
  "outputSchema", "permissionMode", "projectId", "promptProfile", "reasoningEffort", "requireExistingSession",
@@ -1,21 +1,90 @@
1
+ /**
2
+ * 提交体的**值闭集门** —— `request-key-closure.ts`(键名闭集)的姊妹,同一条纪律的另一半。
3
+ *
4
+ * ## 这里解决的是哪一类病
5
+ *
6
+ * 键名对了、**值**拼错了:`memoryCapture: "OFF"`、`memory: "on"`、`memory: true`、`fileHistory: "off"`。这一族键有一个
7
+ * 共同的形 —— **单成员闭集的关断词**:合法值只有**一个**关断拼法(记忆族是 `"off"`;core 在 `fileHistory` /
8
+ * `checkpointStore` 上的拼法是 `"disabled"`),「开」的拼法**刻意不存在**(缺席就是「照常」的唯一形,声明只能单向收紧)。
9
+ * 于是坏值有且只有一种正确处置:**响亮拒**。按 truthiness 读成任一态都是灾难的一半 ——
10
+ * 一个敲错的隐私/收紧请求被静默丢掉(用户以为关了,其实没关),或者一个敲错的键把功能静默关掉。
11
+ *
12
+ * 🔴 **判别是「值域恰好是一个关断词」,不是「值域恰好是一个字面量」**(S-724 加 `fileHistory` 时定形):
13
+ * `approverPosture` 的值域也只有一个词(`"bypass"`),但它是一句**姿态声明**、闭集属主在 core、本仓刻意**零判据**
14
+ * (`http/wire-types.ts` 那一段逐字成文);把判别放宽成「单字面量」会把它也拖进本表 = 同一语义面第二个判官。
15
+ * 关断词全集 {@link OffSwitchWord} 是 core 的两种关断拼法,新增第三种拼法 = 加一个词(并逐键重判)。
16
+ *
17
+ * ## 为什么是一只判据函数,不是每处一抄
18
+ *
19
+ * 这道门必须落在**两腿**上:fresh 提交的 HTTP admission(400 `request.field_invalid`)与 **resume 重放**
20
+ * 共用的解析路径(`boot/resolve-spec.ts`;从这里逃出的 `HttpError` 被 `http/resume-legs.ts` 折成 409
21
+ * `resume_blocked_by_policy`(带 `blockedBy: e.code`)= 一个 typed 的「续跑被挡」,比静默放行诚实 —— 亲读
22
+ * `resume-legs.ts:776` / `:2366`,**不是**邻处旧注说的 `server.ts`,那句是切片搬家后的陈旧指路)。
23
+ * ⚠️ 本族**刻意不**照抄「fresh fail-loud / resume 宽容降级」那条双层姿势(`settings` / `outputStyle` /
24
+ * `permissionMode` 走的是它):那三处降级的是配置/名单类的**可恢复量**,而把一条收紧声明按打字错误折成缺席是
25
+ * **不可逆方向**的静默降级 —— 一条旧 worker 持久化的 `memoryCapture:'OFF'` 会在 resume 时静默恢复采集。
26
+ *
27
+ * 两腿各抄一份判据 + 一份文案,就是同一条 wire 语义上的**两个写者**;`memoryCapture` 从 [ref] 起正是这么长的
28
+ * (`http/admission.ts` 与 `boot/resolve-spec.ts` 各一份逐字手抄的 150 字文案)。S-405 给这一族添第二个键
29
+ * (`memory`)时把它收成**一张表、一只判据、一处文案属主**:新增一个同形键 = 加一行,而不是四处改。
30
+ * S-724 加第三个键 `fileHistory` 正是这么加的 —— 一行(带它自己的关断词),零处改判据。
31
+ * 文案的冻结职责随之从「桶扫描」搬到 `test/api-error-text-freeze.test.ts` 的常量锚格(与 `RUN_NOT_FOUND_MESSAGE` /
32
+ * `PERSIST_RULE_*` 两格同形、同理由)。
33
+ *
34
+ * ## 边界(与姊妹门同律)
35
+ *
36
+ * · 只判**本表登记的键**的值形;别的值形校验归各自既有的门,本门零抢话。
37
+ * 唯一的跨键句:`fileHistory` 关断词与还原同体({@link FILE_HISTORY_DISABLED_NO_RESTORE}),文案属主随关断词住在这里,
38
+ * 只在 fresh 受理门执行(理由在那一格)。
39
+ * · 只判**值**,不判键名(键名闭集是 `request-key-closure.ts` 的活)。
40
+ * · 体本身非对象 ⇒ 本门不判(留给「missing 'objective' string」那道根形门,免得一个体被两句话抢答)。
41
+ */
42
+ import type { TaskRequestBody } from "./http/wire-types.js";
1
43
  /** [ref] §2.1([ref]):`memoryCapture` 的拼写门文案。**wire 可见文本** —— 改一个字就是对端契约漂移
2
44
  * (SDK/壳/文档锚它),字节冻结在 `test/api-error-text-freeze.test.ts` 的常量锚格。 */
3
45
  export declare const MEMORY_CAPTURE_SPELLING_MESSAGE = "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual \u2014 there is no 'on' spelling)";
4
46
  /** core [ref] r5 §5 W3(S-405):请求级「记忆整面关」`memory` 的拼写门文案。同上,wire 可见、字节冻结。
5
47
  * 文案刻意点明 capture 的分工 —— 两个键都在记忆轴上、形又一模一样,拿错的人要在同一条回执里读到该用哪个。 */
6
48
  export declare const MEMORY_OFF_SPELLING_MESSAGE = "memory must be the string 'off' when present (the request-level declaration that this run mounts no memory face at all; absent = memory as usual \u2014 there is no 'on' spelling; to keep memory mounted but stop this session's content from entering long-term memory, send memoryCapture: 'off' instead)";
49
+ /** S-724(core 7.31.0 #1048):请求级「文件历史关断」`fileHistory` 的拼写门文案。同上,wire 可见、字节冻结。
50
+ * 唯一词是 core 的拼法 `'disabled'`(与 `checkpointStore` 同),**不是**记忆族的 `'off'` —— 句子点名它,
51
+ * 顺带说清同一提交里不能再要还原(那一对的拒句是下面的 {@link FILE_HISTORY_DISABLED_NO_RESTORE})。 */
52
+ export declare const FILE_HISTORY_OFF_SPELLING_MESSAGE = "fileHistory must be the string 'disabled' when present (the request-level declaration that this run records no file history \u2014 no backup of an edited file, and no file restore in the same request; absent = the deployment's setting \u2014 there is no 'enabled' spelling)";
53
+ /**
54
+ * S-724 b′(主会话裁,车PW codex r1 [high] 复现后)—— `fileHistory: "disabled"` 与还原**同体**的拒句(`request.field_conflict`)。
55
+ * 与 `tool-approval.ts` 的 `SETTLED_BY_DENY_ONLY` / `READ_ROOT_GRANT_ALLOW_ONLY` **同一族**:「只看体就能判的矛盾,在裁决之前响亮说」。
56
+ * 这一对是调用方自己写出的矛盾(关掉这条 run 的文件历史,又要从文件历史还原);core 也拒(`rewind.capture_disabled`),但它的拒
57
+ * 落在 prepare 的**会话分叉之后**(`prepare-acquire-reconcile` 先按 `resumeAt` 把活动分支移过去,`prepare-file-history` 才判),
58
+ * 且拒后不回滚 —— 拒传不回来。所以本层在任何副作用之前拒。**这不是替 core 再判一次「捕获关了能不能还原」**:判的是体里两个键
59
+ * 能不能共存,不看部署、不看店。
60
+ * 判据:{@link fileHistoryRestoreConflict}。**只在 fresh 受理门**(`http/admission.ts`);续跑腿不过这道门(RESUME 重放不过受理门,
61
+ * 同律 —— 且续跑腿重建时整族剥掉一次性历史动作,矛盾在盘上的体里本就留不下来)。wire 可见、字节冻结。
62
+ */
63
+ export declare const FILE_HISTORY_DISABLED_NO_RESTORE = "fileHistory: 'disabled' cannot ride with a file restore (restoreFiles, rewindFilesTo, or the retired rewindFiles) \u2014 the request turns this run's file history off and asks to restore files from it in the same breath; the engine would refuse it (rewind.capture_disabled) only after the session had already been branched at resumeAt, which it does not undo, so it is refused here, before any side effect. Drop fileHistory to restore, or drop the restore to run without file history";
7
64
  /**
8
- * **单成员闭集键表** —— 合法值只有 `"off"`,缺席=照常,没有 `"on"`。
65
+ * 「关断词 ∧ 还原」这一对的判据(纯函数,只看体)。还原 = 三个键里任一个在请求:`restoreFiles: true`、非空串 `rewindFilesTo`
66
+ * (与 `boot/resolve-spec.ts` 的读法同:空串 / 非串按缺席)、退役拼法 `rewindFiles: true`(restore 义要一次还原;capture 义要的
67
+ * 正是这个词关掉的记录 —— 两义都与关断词矛盾)。调用前值形已由上面的单成员表与既有布尔门判过。
68
+ */
69
+ export declare function fileHistoryRestoreConflict(body: Pick<TaskRequestBody, "fileHistory" | "restoreFiles" | "rewindFiles" | "rewindFilesTo">): boolean;
70
+ /**
71
+ * **单成员闭集键表** —— 每键合法值只有它自己那一个关断词(`word`),缺席=照常,没有「开」的拼法。
9
72
  *
10
- * 🔴 顺序 = **裁决顺序**,且是**既存键在前**:一个两键都畸形的存量体,判决与加 `memory` 之前逐字节相同
73
+ * 🔴 顺序 = **裁决顺序**,且是**既存键在前**:一个多键都畸形的存量体,判决与加新键之前逐字节相同
11
74
  * (新键不许改写既有体的回执)。新增同形键一律**追加在尾部**。
12
75
  */
13
76
  declare const SINGLE_MEMBER_OFF_FIELDS: readonly [{
14
77
  readonly field: "memoryCapture";
78
+ readonly word: "off";
15
79
  readonly message: "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual — there is no 'on' spelling)";
16
80
  }, {
17
81
  readonly field: "memory";
82
+ readonly word: "off";
18
83
  readonly message: "memory must be the string 'off' when present (the request-level declaration that this run mounts no memory face at all; absent = memory as usual — there is no 'on' spelling; to keep memory mounted but stop this session's content from entering long-term memory, send memoryCapture: 'off' instead)";
84
+ }, {
85
+ readonly field: "fileHistory";
86
+ readonly word: "disabled";
87
+ readonly message: "fileHistory must be the string 'disabled' when present (the request-level declaration that this run records no file history — no backup of an edited file, and no file restore in the same request; absent = the deployment's setting — there is no 'enabled' spelling)";
19
88
  }];
20
89
  /** 一次值闭集裁决的产物:出问题的键名(机读)+ 一句 wire 文案。 */
21
90
  export interface SingleMemberOffFieldIssue {
@@ -23,7 +92,7 @@ export interface SingleMemberOffFieldIssue {
23
92
  message: string;
24
93
  }
25
94
  /**
26
- * 单成员闭集键的值形裁决:表内任一键在场且不是字面 `"off"` ⇒ 返回拒体(调用方 400 `request.field_invalid`);
95
+ * 单成员闭集键的值形裁决:表内任一键在场且不是它自己那一个关断词 ⇒ 返回拒体(调用方 400 `request.field_invalid`);
27
96
  * 全部合法或全部缺席 ⇒ `null`。**两腿(fresh admission / resume 重放)共用这一只**。
28
97
  */
29
98
  export declare function singleMemberOffFieldIssue(raw: unknown): SingleMemberOffFieldIssue | null;
@@ -1,16 +1,24 @@
1
1
  export const MEMORY_CAPTURE_SPELLING_MESSAGE = "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual — there is no 'on' spelling)";
2
2
  export const MEMORY_OFF_SPELLING_MESSAGE = "memory must be the string 'off' when present (the request-level declaration that this run mounts no memory face at all; absent = memory as usual — there is no 'on' spelling; to keep memory mounted but stop this session's content from entering long-term memory, send memoryCapture: 'off' instead)";
3
+ export const FILE_HISTORY_OFF_SPELLING_MESSAGE = "fileHistory must be the string 'disabled' when present (the request-level declaration that this run records no file history — no backup of an edited file, and no file restore in the same request; absent = the deployment's setting — there is no 'enabled' spelling)";
4
+ export const FILE_HISTORY_DISABLED_NO_RESTORE = "fileHistory: 'disabled' cannot ride with a file restore (restoreFiles, rewindFilesTo, or the retired rewindFiles) — the request turns this run's file history off and asks to restore files from it in the same breath; the engine would refuse it (rewind.capture_disabled) only after the session had already been branched at resumeAt, which it does not undo, so it is refused here, before any side effect. Drop fileHistory to restore, or drop the restore to run without file history";
5
+ export function fileHistoryRestoreConflict(body) {
6
+ if (body.fileHistory !== "disabled")
7
+ return false;
8
+ return body.restoreFiles === true || body.rewindFiles === true || (typeof body.rewindFilesTo === "string" && body.rewindFilesTo.length > 0);
9
+ }
3
10
  const SINGLE_MEMBER_OFF_FIELDS = [
4
- { field: "memoryCapture", message: MEMORY_CAPTURE_SPELLING_MESSAGE },
5
- { field: "memory", message: MEMORY_OFF_SPELLING_MESSAGE },
11
+ { field: "memoryCapture", word: "off", message: MEMORY_CAPTURE_SPELLING_MESSAGE },
12
+ { field: "memory", word: "off", message: MEMORY_OFF_SPELLING_MESSAGE },
13
+ { field: "fileHistory", word: "disabled", message: FILE_HISTORY_OFF_SPELLING_MESSAGE },
6
14
  ];
7
15
  export function singleMemberOffFieldIssue(raw) {
8
16
  if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
9
17
  return null;
10
18
  const body = raw;
11
- for (const { field, message } of SINGLE_MEMBER_OFF_FIELDS) {
19
+ for (const { field, word, message } of SINGLE_MEMBER_OFF_FIELDS) {
12
20
  const value = body[field];
13
- if (value !== undefined && value !== "off")
21
+ if (value !== undefined && value !== word)
14
22
  return { field, message };
15
23
  }
16
24
  return null;
@@ -0,0 +1,6 @@
1
+ export declare const ROSTER_BOUND_HTTP_STATUS: {
2
+ readonly "config.tool_name_too_long": 503;
3
+ readonly "config.tool_contract_too_long": 503;
4
+ readonly "config.tool_roster_bound": 503;
5
+ };
6
+ //# sourceMappingURL=roster-bound-http-status.d.ts.map
@@ -0,0 +1,6 @@
1
+ export const ROSTER_BOUND_HTTP_STATUS = {
2
+ "config.tool_name_too_long": 503,
3
+ "config.tool_contract_too_long": 503,
4
+ "config.tool_roster_bound": 503,
5
+ };
6
+ //# sourceMappingURL=roster-bound-http-status.js.map
@@ -736,17 +736,6 @@ export function createRuleConsentLane(stores, opts) {
736
736
  adds: [],
737
737
  };
738
738
  const rowOf = (rows) => rows.find((r) => sameWireRuleIdentity(r, { behavior: input.behavior, rule: written.rule.rule, scope: row.scope }));
739
- let before;
740
- try {
741
- before = await lane.listRules(input.principal);
742
- }
743
- catch (e) {
744
- localImportLogger.warn("direct_rule_write_pre_read_failed", { behavior: input.behavior, err: e instanceof Error ? e.message : String(e) });
745
- return { status: "failed", detail: `the rule store could not be read before writing: ${e instanceof Error ? e.message : String(e)}`, committed: "no" };
746
- }
747
- const standing = rowOf(before.rows);
748
- if (standing !== undefined)
749
- return { status: "no-op", rev: before.rev, stillLive: "yes", row: standing };
750
739
  const screened = screenLocalImportRow(row);
751
740
  if ("reason" in screened)
752
741
  return { status: "rejected", reason: screened.reason, detail: screened.detail };
package/dist/run-local.js CHANGED
@@ -3,12 +3,10 @@ import { DEFAULT_MODEL_REF, resolveTaskModel } from "./model-select.js";
3
3
  import { defaultModelRefusal, modelUnavailableMessage } from "./model-entry-refusal.js";
4
4
  import { buildDurableAbsenceNote, createCorruptReadSeat } from "./observability/corrupt-read-seat.js";
5
5
  import { outwardPlane } from "./terminal.js";
6
- import { homedir } from "node:os";
7
6
  import { join } from "node:path";
8
7
  import { realpathSync } from "node:fs";
9
8
  import { createInterface } from "node:readline/promises";
10
9
  import { fileURLToPath } from "node:url";
11
- import { loadRemoteExec } from "@sema-agent/settings-schema/node";
12
10
  import { Runner, uuidv7, FileStorageBackend, NodeExecutionEnv, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
13
11
  import { singleUserAutoAcceptBaselineApplies } from "./approval.js";
14
12
  import { createBrain } from "./brain.js";
@@ -21,7 +19,9 @@ import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-val
21
19
  import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
22
20
  import { buildStoreOpenRefusalTail } from "./plugins/store-open-refusal.js";
23
21
  import { acquireLocalBackendWithWait } from "./plugins/store-lock-wait.js";
24
- import { createConfigProvider } from "./config-provider.js";
22
+ import { configLocalRoot, configProviderKind, createConfigProvider } from "./config-provider.js";
23
+ import { composeExecutionLane } from "./boot/execution-lane.js";
24
+ import { judgeConsolidationValve } from "./boot/memory-consolidation.js";
25
25
  import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
26
26
  import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
27
27
  import { executionLaneHomeDir, executionLaneOf } from "./execution-lane-caps.js";
@@ -259,20 +259,25 @@ export async function runLocal(argv, deps = {}) {
259
259
  process.env.CONFIG_PROVIDER = "local";
260
260
  process.env.SESSION_BACKEND = "memory";
261
261
  process.env.REQUIRE_PRINCIPAL = "false";
262
- const root = args.root ?? process.env.CONFIG_LOCAL_DIR ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
263
262
  if (args.root)
264
263
  process.env.CONFIG_LOCAL_DIR = args.root;
265
264
  const config = loadConfig();
265
+ const root = configLocalRoot(config);
266
266
  const logger = deps.logger ?? createLogger(config.logLevel);
267
267
  const metrics = createMetrics();
268
268
  const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
269
269
  installSecretEnvScrubRecorder({ logger, metrics });
270
270
  logConfigDiagnostics(logger);
271
271
  installOutboundDispatcher(logger);
272
- const configProvider = createConfigProvider({
273
- provider: config.configProvider ?? "local",
274
- ...(config.configLocalDir ? { localDir: config.configLocalDir } : { localDir: root }),
275
- });
272
+ const providerInput = { provider: config.configProvider ?? "local", localDir: root };
273
+ try {
274
+ await composeExecutionLane(config, { configSource: configProviderKind(providerInput), localRoot: root, logger, drivesFromFile: ["host"] });
275
+ }
276
+ catch (err) {
277
+ printErr(err instanceof Error ? err.message : String(err));
278
+ return 2;
279
+ }
280
+ const configProvider = createConfigProvider(providerInput);
276
281
  let effective;
277
282
  try {
278
283
  const r = await configProvider.fetchEffective(undefined);
@@ -303,25 +308,6 @@ export async function runLocal(argv, deps = {}) {
303
308
  const unpricedCeiling = buildUnpricedCeilingNotice(config.models, config);
304
309
  if (unpricedCeiling)
305
310
  logger.warn(unpricedCeiling.event, unpricedCeiling.fields);
306
- try {
307
- const fileRx = await loadRemoteExec(root);
308
- if (fileRx?.provider === "host") {
309
- config.remoteExec = {
310
- provider: "host",
311
- ...(fileRx.workdir ? { workspaceBase: fileRx.workdir } : {}),
312
- ...(fileRx.commandTimeoutMs != null ? { commandTimeoutMs: fileRx.commandTimeoutMs } : {}),
313
- };
314
- logger.info("remote_exec_from_file", { provider: "host", root });
315
- }
316
- else if (fileRx) {
317
- logger.warn("remote_exec_file_isolated_lane_unwired", { provider: fileRx.provider, note: "run-local is the host lane; isolated lanes stay env-sourced" });
318
- }
319
- }
320
- catch (err) {
321
- logger.error("remote_exec_file_invalid", { root, err: String(err) });
322
- printErr(`config.d/remote-exec.json is invalid: ${String(err)}`);
323
- return 2;
324
- }
325
311
  try {
326
312
  assertGuardPatternsUsable(config);
327
313
  }
@@ -377,6 +363,15 @@ export async function runLocal(argv, deps = {}) {
377
363
  : undefined;
378
364
  const lspManager = hostRx && config.lspHostEnabled && config.requirePrincipal !== true ? createHostLspManager(config, (event, fields) => logger.info(event, fields)) : undefined;
379
365
  const memoryEngine = memoryEngineBackendFor(config, root, memoryEngineIncident);
366
+ try {
367
+ judgeConsolidationValve({ config, memoryEngine });
368
+ }
369
+ catch (err) {
370
+ printErr(err instanceof Error ? err.message : String(err));
371
+ void lspManager?.dispose();
372
+ await fileBackend.dispose();
373
+ return 2;
374
+ }
380
375
  let memorySync;
381
376
  if (config.memorySync && memoryEngine) {
382
377
  memorySync = createMemorySyncRunner({
package/dist/runs.d.ts CHANGED
@@ -145,12 +145,32 @@ export declare function isSessionConflictResult(result: {
145
145
  * 🔴 **可达性如实**:本仓的 HTTP 面**今天铸不出**这条腿 —— 子代由 core 内部的 Agent 工具派生,
146
146
  * `fileHistoryLineage` 是 core prepare 的 internals(不在 `TaskSpec` 上),server 从不直接 prepare 一条带
147
147
  * lineage 的子代腿。本臂是**分诊面的完备性**(core 说 X ⇒ wire 形是 Y),不是一条今天在跑的路;将来的
148
- * 委派入口接线那天零改动即生效。同族其余五码今天**仍落 200-with-failed-body**,那是本批**之前**就存在的
149
- * 缺口(不是本批回归),已在上述对表格里逐码显式登记为债务,并由那道门看住不许陈腐。
148
+ * 委派入口接线那天零改动即生效。同族其余码(`rewind_files_retired` / `invalid_spec` / `conflicting_targets` /
149
+ * `restore_failed`)今天**仍落 200-with-failed-body**,那是各自提货批**之前**就存在的缺口(不是回归),已在上述对表格里
150
+ * 逐码显式登记为债务,并由那道门看住不许陈腐。
151
+ *
152
+ * S-724(core 7.31.0 #1048 / #1050):`rewind.capture_disabled` → **422**(关断词与还原同体 = 自相矛盾;本服务的请求面上
153
+ * 走不到它 —— fresh 受理门先 400 `request.field_conflict`(core 这一拒落在会话分叉之后、不回滚);映射是分诊面完备性)、`rewind.store_unconfigured`
154
+ * → **409**(部署没有历史店;`FILE_HISTORY_CAPTURE=false` 自本版不再摘店 ⇒ 可达面只剩没有后端的部署形,那里
155
+ * `capabilities.rewindFiles` 恒报 false —— 三问在 CHANGELOG 7.104.0)。
150
156
  */
151
157
  export declare function resumeAtHttpStatus(result: {
152
158
  terminal: TerminalCause;
153
159
  }): number | undefined;
160
+ /**
161
+ * 同步提交腿的**拒因状态**(S-271 车内 codex r1 [high] 以类修):`resumeAtHttpStatus` 的逐码档,只对**零工作的拒**生效。
162
+ *
163
+ * 那张分诊表(`resume_at.*` / 治理闭集 / 名册超界闭集 / 开集 if 链)的每一格都立在同一个前提上:这个码是 prepare 期的拒,
164
+ * 整次提交一个 turn 都没跑、一个 token 都没烧 ⇒ 答 4xx / 5xx、**不走** `finalizeTaskResult`、不进幂等缓存(只缓存 < 400)。
165
+ * 可终局码证明不了这个前提:verify / cascade 的聚合结果 = **末轮**终局 + **各轮累计** stats(core `runCascade` /
166
+ * `runWithVerification`),前一轮真跑了 turn、真执行了工具,末轮才在 prepare 期被拒,码照样是那个 prepare 码。
167
+ * 真案(车内 codex r1 验真):`modelGate` 把带门类的超界工具从第一级模型的名册上摘掉,第一级跑了一只有副作用的工具,第二级
168
+ * 名册门拒 ⇒ 修前答 503,跳过计费,同一 Idempotency-Key 重发把第一级的工具再执行一遍。
169
+ * ⇒ 判据是**这次提交做没做过工作**(累计 `turns` / `tokens` 皆零),不是码:做过工作的失败无论码是什么,都是「跑了然后失败了」
170
+ * (200 + failed 体、计费、可重放)。入参要求带 `stats`,所以同步腿拿不到「只看码」的那一半 —— 只看码的逐码表留给
171
+ * `resumeAtHttpStatus` 自己(单元测试直接喂码)。
172
+ */
173
+ export declare function syncRefusalHttpStatus(result: Pick<TaskResult, "terminal" | "stats">): number | undefined;
154
174
  /**
155
175
  * E18 resume-at — per-turn anchor capture, shared by runInBackground AND the resume-leg driveResume so the (single,
156
176
  * tricky) capture rule can't drift between the two stream-drain loops.
package/dist/runs.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { runWithVerification, runCascade, defaultTaskRegistry, taskNotificationDedupKey, DURABLE_AGENT_HANDLE_RE } from "@sema-agent/core";
2
2
  import { failureCodeOf, sealResultForExit, outwardPlane } from "./terminal.js";
3
3
  import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
4
+ import { ROSTER_BOUND_HTTP_STATUS } from "./roster-bound-http-status.js";
5
+ import { isSessionErrorCode, sessionErrorHttp } from "./session-error-wire.js";
4
6
  import { redactErrorMessage } from "./observability/run-terminal.js";
5
7
  import { withPrincipal } from "./observability/principal-context.js";
6
8
  import { redactSecrets } from "./trace/redact.js";
@@ -74,12 +76,24 @@ export function resumeAtHttpStatus(result) {
74
76
  if (Object.hasOwn(GOVERNANCE_HTTP_STATUS, code)) {
75
77
  return GOVERNANCE_HTTP_STATUS[code] ?? undefined;
76
78
  }
79
+ if (Object.hasOwn(ROSTER_BOUND_HTTP_STATUS, code)) {
80
+ return ROSTER_BOUND_HTTP_STATUS[code];
81
+ }
82
+ if (isSessionErrorCode(code)) {
83
+ const wire = sessionErrorHttp(code, "open");
84
+ if (wire !== undefined)
85
+ return wire.status;
86
+ }
77
87
  if (code === "resume.session_not_found")
78
88
  return 404;
79
89
  if (code === "rewind_snapshot.unresolvable")
80
90
  return 422;
81
91
  if (code === "rewind.child_scope_unsupported")
82
92
  return 422;
93
+ if (code === "rewind.capture_disabled")
94
+ return 422;
95
+ if (code === "rewind.store_unconfigured")
96
+ return 409;
83
97
  if (code === "memory.capture_optout_unpersisted")
84
98
  return 503;
85
99
  if (code === "config.memory_capture_spelling")
@@ -94,6 +108,11 @@ export function resumeAtHttpStatus(result) {
94
108
  return undefined;
95
109
  return code === "resume_at.not_found" ? 404 : 422;
96
110
  }
111
+ export function syncRefusalHttpStatus(result) {
112
+ if (result.stats.turns !== 0 || result.stats.tokens !== 0)
113
+ return undefined;
114
+ return resumeAtHttpStatus(result);
115
+ }
97
116
  export class TurnAnchorCapture {
98
117
  capture;
99
118
  onCaptureFail;
@@ -221,8 +240,9 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
221
240
  const parkNeedsReview = async (res) => {
222
241
  await sink.appendParked("needs_review", res);
223
242
  await flushModelUsage();
224
- reached = { kind: "needs_review" };
225
- await runStore.setNeedsReview(taskId);
243
+ const sealed = sealResultForExit(res);
244
+ reached = { kind: "needs_review", result: sealed };
245
+ await runStore.setNeedsReview(taskId, sealed);
226
246
  metrics?.addGauge("runs_active", -1);
227
247
  metrics?.inc("tasks_total", { status: "needs_review" });
228
248
  };
@@ -361,10 +381,12 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
361
381
  await flush();
362
382
  const parkStatus = ev.result.terminal.kind === "paused" ? outwardPlane(ev.result.terminal).status : undefined;
363
383
  if (parkStatus === "suspended") {
364
- await sink.appendParked("suspended", await graftParkToolCallId(ev.result, parkCheckpointReader));
384
+ const grafted = await graftParkToolCallId(ev.result, parkCheckpointReader);
385
+ await sink.appendParked("suspended", grafted);
365
386
  await flushModelUsage();
366
- reached = { kind: "suspended" };
367
- await runStore.setSuspended(taskId);
387
+ const sealed = sealResultForExit(grafted);
388
+ reached = { kind: "suspended", result: sealed };
389
+ await runStore.setSuspended(taskId, sealed);
368
390
  metrics?.addGauge("runs_active", -1);
369
391
  metrics?.inc("tasks_total", { status: "suspended" });
370
392
  continue;
@@ -456,10 +478,10 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
456
478
  await runStore.setTerminal(taskId, plane.status, reached.result, plane.errorMessage ?? null);
457
479
  }
458
480
  else if (reached?.kind === "suspended") {
459
- await runStore.setSuspended(taskId);
481
+ await runStore.setSuspended(taskId, reached.result);
460
482
  }
461
483
  else if (reached?.kind === "needs_review") {
462
- await runStore.setNeedsReview(taskId);
484
+ await runStore.setNeedsReview(taskId, reached.result);
463
485
  }
464
486
  else if (cancelCtrl.signal.aborted && observedDone === undefined) {
465
487
  const c = cancelledResult();
@@ -13,16 +13,43 @@ export interface SealedKeyOpener {
13
13
  * 🔴 The returned plaintext is a live secret: memory-only, never log it, never persist it. */
14
14
  open(ciphertextB64: string, publicKeyId: string): string;
15
15
  }
16
- export interface SealedKeyStore extends SealedKeyOpener {
16
+ /**
17
+ * S-737 —— the store's **discovery-safe face**: ONLY the newest public key. This is what the HTTP layer is handed
18
+ * (`ServiceDeps.sealedKeyPublic` → `GET /v1/capabilities` `sealedKey`) — never the whole store — so private-key
19
+ * material, the custody path and the held-id list are structurally unreachable from that layer (not merely
20
+ * "not read"). Built from a live store by {@link buildSealedKeyPublicFace}, which copies out `latest` alone.
21
+ */
22
+ export interface SealedKeyPublicFace {
17
23
  /** The keypair NEW ciphertexts should be sealed against (max `createdAt`, ties broken by id) —
18
- * this is the key we report/print for center registration. `publicKey` is base64 (32 raw bytes,
19
- * 44 chars): public by definition, safe to log. */
20
- latest: {
24
+ * this is the key we report/print for center registration and project onto `GET /v1/capabilities`.
25
+ * `publicKey` is base64 (32 raw bytes, 44 chars): public by definition, safe to log. */
26
+ readonly latest: {
21
27
  publicKeyId: string;
22
28
  publicKey: string;
23
29
  createdAt: string;
24
30
  };
25
31
  }
32
+ /** The custody store = the private half ({@link SealedKeyOpener}: open / has / ids) + the public face. */
33
+ export interface SealedKeyStore extends SealedKeyOpener, SealedKeyPublicFace {
34
+ }
35
+ /** Copy the public face out of a live store — a NEW plain object carrying `latest` only (no `open` / `has` / `ids`
36
+ * reachable through it, whatever the caller later casts it to). Plain data ⇒ `build*`. */
37
+ export declare function buildSealedKeyPublicFace(store: SealedKeyPublicFace): SealedKeyPublicFace;
38
+ /**
39
+ * S-737 —— the `GET /v1/capabilities` `sealedKey` segment: exactly three keys — the algorithm (the pinned v1
40
+ * {@link SEALED_KEY_ALG}, never a hand-copied literal), the id to write into `sealedApiKey.publicKeyId`, and the
41
+ * base64 X25519 public key to `crypto_box_seal` against. `createdAt` is deliberately NOT projected (it answers
42
+ * nothing a sealer needs; the wire keeps to the three keys the contract names).
43
+ */
44
+ export interface SealedKeyCapability {
45
+ readonly alg: typeof SEALED_KEY_ALG;
46
+ readonly publicKeyId: string;
47
+ readonly publicKey: string;
48
+ }
49
+ /** The ONE projection from the public face to the caps segment. `undefined` in ⇒ `undefined` out: the caps route
50
+ * omits the key entirely (present-iff this process built its custody store — see the route for the two meanings
51
+ * of absence). */
52
+ export declare function projectSealedKeyCapability(face: SealedKeyPublicFace | undefined): SealedKeyCapability | undefined;
26
53
  /** Why a model's sealed key could not be unsealed (the poison-pill diagnosis, see SealedKeyPoison). CUSTODY failures only:
27
54
  * a sealedApiKey that is malformed (alg other than the pinned v1 SEALED_KEY_ALG, non-base64 ciphertext) makes the whole
28
55
  * catalog entry unavailable instead (S-704 `config.model_entry_invalid`, settings-schema `SealedApiKey` is the shape
@@ -8,6 +8,15 @@ import { centerFetch } from "./config-center/center-request.js";
8
8
  export const SEALED_KEY_ALG = "libsodium-sealedbox-x25519";
9
9
  const SEAL_OVERHEAD_BYTES = 48;
10
10
  const BASE64_STRICT = /^[A-Za-z0-9+/]+={0,2}$/;
11
+ export function buildSealedKeyPublicFace(store) {
12
+ const { publicKeyId, publicKey, createdAt } = store.latest;
13
+ return { latest: { publicKeyId, publicKey, createdAt } };
14
+ }
15
+ export function projectSealedKeyCapability(face) {
16
+ if (face === undefined)
17
+ return undefined;
18
+ return { alg: SEALED_KEY_ALG, publicKeyId: face.latest.publicKeyId, publicKey: face.latest.publicKey };
19
+ }
11
20
  export function sealedKeyPoison(publicKeyId, reason, errKind) {
12
21
  return { sealedKeyPoison: true, publicKeyId, reason, ...(errKind ? { errKind } : {}), at: new Date().toISOString() };
13
22
  }