@sema-agent/server 7.142.0 → 7.143.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 (258) hide show
  1. package/MIGRATION.md +442 -0
  2. package/USAGE.md +41 -24
  3. package/dist/adoption/plan.js +22 -0
  4. package/dist/approval-ask-audit-store.d.ts +7 -5
  5. package/dist/approval-ask-audit-store.js +12 -6
  6. package/dist/approval-ask-machine.d.ts +15 -7
  7. package/dist/approval-ask-machine.js +10 -4
  8. package/dist/approval-card.d.ts +124 -25
  9. package/dist/approval-card.js +103 -23
  10. package/dist/approval-queue-kind.d.ts +27 -0
  11. package/dist/approval-queue-kind.js +6 -0
  12. package/dist/approval-reconciler.d.ts +8 -8
  13. package/dist/approval-reconciler.js +3 -3
  14. package/dist/audit.js +4 -2
  15. package/dist/boot/budget-tracing.d.ts +1 -1
  16. package/dist/boot/governance-seams.d.ts +26 -41
  17. package/dist/boot/governance-seams.js +61 -28
  18. package/dist/boot/late-binds.d.ts +7 -4
  19. package/dist/boot/late-binds.js +3 -1
  20. package/dist/boot/leader.js +2 -4
  21. package/dist/boot/memory-consolidation.d.ts +0 -8
  22. package/dist/boot/memory-consolidation.js +3 -3
  23. package/dist/boot/reapers.js +5 -4
  24. package/dist/boot/resolve-spec.d.ts +1 -1
  25. package/dist/boot/resolve-spec.js +8 -8
  26. package/dist/boot/runner-deps.d.ts +10 -7
  27. package/dist/boot/runner-deps.js +5 -17
  28. package/dist/boot/shutdown.d.ts +9 -0
  29. package/dist/boot/shutdown.js +16 -2
  30. package/dist/boot/stage-02-web-admin-pull.js +3 -0
  31. package/dist/boot/stage-03-store-backend.d.ts +4 -1
  32. package/dist/boot/stage-03-store-backend.js +1 -2
  33. package/dist/boot/stage-04-stores.d.ts +5 -2
  34. package/dist/boot/stage-04-stores.js +3 -5
  35. package/dist/boot/stage-05-execution-env.d.ts +5 -2
  36. package/dist/boot/stage-06-runners.d.ts +5 -2
  37. package/dist/boot/stage-06-runners.js +1 -3
  38. package/dist/boot/stage-07-capability-layer.d.ts +5 -2
  39. package/dist/boot/stage-07-capability-layer.js +5 -1
  40. package/dist/boot/stage-08-reapers.d.ts +5 -2
  41. package/dist/boot/stage-09-leader.d.ts +5 -2
  42. package/dist/boot/stage-10-http-server.d.ts +5 -2
  43. package/dist/boot/stage-10-http-server.js +9 -1
  44. package/dist/boot/stores.d.ts +5 -2
  45. package/dist/boot/stores.js +7 -7
  46. package/dist/boot/web-admin.d.ts +5 -1
  47. package/dist/boot/web-admin.js +31 -19
  48. package/dist/capabilities/hands-lane.d.ts +9 -39
  49. package/dist/capabilities/hands-lane.js +8 -13
  50. package/dist/capabilities/mcp-probe.d.ts +3 -2
  51. package/dist/capabilities/scenarios.d.ts +11 -1
  52. package/dist/capabilities/scenarios.js +1 -0
  53. package/dist/capabilities/skills.d.ts +1 -17
  54. package/dist/capabilities/skills.js +0 -8
  55. package/dist/capability-facts.d.ts +8 -8
  56. package/dist/capability-facts.js +4 -4
  57. package/dist/card-void.d.ts +6 -9
  58. package/dist/config-catalog.js +1 -1
  59. package/dist/config-dir-trust.js +58 -29
  60. package/dist/config-invariants.d.ts +7 -1
  61. package/dist/config-invariants.js +7 -0
  62. package/dist/config-provider.d.ts +10 -0
  63. package/dist/config-provider.js +15 -4
  64. package/dist/config-root-migration.d.ts +13 -0
  65. package/dist/config-root-migration.js +99 -0
  66. package/dist/config-types.d.ts +5 -5
  67. package/dist/config.d.ts +4 -3
  68. package/dist/config.js +1 -1
  69. package/dist/delegated-session-owner.d.ts +5 -3
  70. package/dist/delegated-session-owner.js +1 -1
  71. package/dist/device-ws-protocol.d.ts +1 -0
  72. package/dist/device-ws-protocol.js +2 -1
  73. package/dist/env-model-table-gate.d.ts +53 -0
  74. package/dist/env-model-table-gate.js +53 -0
  75. package/dist/fleet/fleet-bus.d.ts +28 -52
  76. package/dist/fleet/fleet-bus.js +35 -100
  77. package/dist/fleet/fleet-reconciler.d.ts +4 -4
  78. package/dist/fleet/fleet-reconciler.js +1 -1
  79. package/dist/fleet/subagent-tail-bus.d.ts +14 -11
  80. package/dist/fleet/subagent-tail-bus.js +15 -28
  81. package/dist/hooks/hook-runner.js +28 -7
  82. package/dist/host-decision.d.ts +54 -20
  83. package/dist/host-decision.js +42 -14
  84. package/dist/http/admission.d.ts +1 -1
  85. package/dist/http/admission.js +7 -3
  86. package/dist/http/approval-sweeps.js +11 -8
  87. package/dist/http/caller-trust.d.ts +1 -1
  88. package/dist/http/durable-run-leg.js +2 -2
  89. package/dist/http/engine-refusal.d.ts +16 -0
  90. package/dist/http/engine-refusal.js +17 -0
  91. package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
  92. package/dist/http/memory-compliance-failure-reply.js +4 -6
  93. package/dist/http/owner-access.d.ts +11 -0
  94. package/dist/http/owner-access.js +13 -0
  95. package/dist/http/resume-legs.d.ts +21 -7
  96. package/dist/http/resume-legs.js +71 -49
  97. package/dist/http/resume-restatement.d.ts +39 -0
  98. package/dist/http/resume-restatement.js +25 -0
  99. package/dist/http/route-ctx.d.ts +35 -10
  100. package/dist/http/route-ctx.js +1 -1
  101. package/dist/http/routes/approvals-assistant.d.ts +9 -1
  102. package/dist/http/routes/approvals-assistant.js +142 -31
  103. package/dist/http/routes/capabilities.js +11 -2
  104. package/dist/http/routes/memory-policy.d.ts +2 -2
  105. package/dist/http/routes/memory-policy.js +1 -1
  106. package/dist/http/routes/notify-wake.js +23 -25
  107. package/dist/http/routes/rules.js +48 -13
  108. package/dist/http/routes/runs.js +9 -8
  109. package/dist/http/routes/session-memory-sync.d.ts +34 -6
  110. package/dist/http/routes/session-memory-sync.js +27 -10
  111. package/dist/http/routes/session-sync.js +13 -20
  112. package/dist/http/routes/sessions.d.ts +2 -2
  113. package/dist/http/routes/sessions.js +22 -23
  114. package/dist/http/routes/tasks.js +27 -49
  115. package/dist/http/server.d.ts +13 -7
  116. package/dist/http/server.js +2 -2
  117. package/dist/http/stored-body-replay.js +15 -4
  118. package/dist/http/wire-types.d.ts +17 -7
  119. package/dist/leader/leader.js +5 -2
  120. package/dist/leader/wire.d.ts +7 -1
  121. package/dist/leader/wire.js +6 -5
  122. package/dist/leg-facts.d.ts +37 -0
  123. package/dist/leg-facts.js +15 -0
  124. package/dist/memory-bundle-engine.d.ts +1 -4
  125. package/dist/memory-bundle-engine.js +2 -2
  126. package/dist/memory-operator-faces.d.ts +30 -66
  127. package/dist/memory-operator-faces.js +17 -88
  128. package/dist/memory-scope-stores.d.ts +38 -40
  129. package/dist/memory-scope-stores.js +51 -36
  130. package/dist/memory-scope.d.ts +0 -9
  131. package/dist/memory-scope.js +0 -9
  132. package/dist/memory-sync-client.js +14 -3
  133. package/dist/memory-sync.d.ts +20 -4
  134. package/dist/memory-sync.js +23 -4
  135. package/dist/model-plane-face.d.ts +13 -7
  136. package/dist/model-plane-face.js +2 -1
  137. package/dist/observability/fail-open.d.ts +20 -4
  138. package/dist/observability/fail-open.js +17 -4
  139. package/dist/observability/run-terminal.js +4 -0
  140. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
  141. package/dist/orchestration/workflow-completion-inbox.js +0 -12
  142. package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
  143. package/dist/orchestration/workflow-park-decision.d.ts +8 -3
  144. package/dist/orchestration/workflow-park-decision.js +5 -3
  145. package/dist/org-delivery-gate.d.ts +11 -0
  146. package/dist/org-delivery-gate.js +19 -1
  147. package/dist/parked-decide.d.ts +13 -9
  148. package/dist/parked-decide.js +7 -7
  149. package/dist/parked-revive-surface.js +1 -1
  150. package/dist/peer-inbound-face.d.ts +127 -0
  151. package/dist/peer-inbound-face.js +158 -0
  152. package/dist/permission-rule-list.d.ts +13 -9
  153. package/dist/permission-rule-list.js +7 -3
  154. package/dist/plugins/approval-ask-store-file.js +18 -6
  155. package/dist/plugins/approval-ask-store-memory.js +8 -6
  156. package/dist/plugins/approval-ask-store-sql.d.ts +28 -13
  157. package/dist/plugins/approval-ask-store-sql.js +30 -11
  158. package/dist/plugins/checkpoint-store-sql.d.ts +4 -4
  159. package/dist/plugins/checkpoint-store-sql.js +9 -7
  160. package/dist/plugins/file-history-store-sql.d.ts +24 -18
  161. package/dist/plugins/file-history-store-sql.js +24 -19
  162. package/dist/plugins/file-run-store.d.ts +29 -0
  163. package/dist/plugins/file-run-store.js +97 -8
  164. package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
  165. package/dist/plugins/imported-layer-root-retirement.js +96 -0
  166. package/dist/plugins/local-checkpoint-store.d.ts +1 -1
  167. package/dist/plugins/local-checkpoint-store.js +2 -2
  168. package/dist/plugins/local-session-store.d.ts +6 -2
  169. package/dist/plugins/local-session-store.js +8 -2
  170. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
  171. package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
  172. package/dist/plugins/mailbox-store-sql.d.ts +12 -8
  173. package/dist/plugins/mailbox-store-sql.js +76 -7
  174. package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
  175. package/dist/plugins/memory-sync-store-pg.js +21 -0
  176. package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
  177. package/dist/plugins/memory-sync-store-tidb.js +13 -1
  178. package/dist/plugins/one-time-migrations.d.ts +32 -2
  179. package/dist/plugins/one-time-migrations.js +106 -3
  180. package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
  181. package/dist/plugins/org-delivery-quota-store.js +2 -12
  182. package/dist/plugins/park-commit-watch.d.ts +1 -1
  183. package/dist/plugins/permission-rule-store-file.d.ts +10 -1
  184. package/dist/plugins/permission-rule-store-file.js +15 -2
  185. package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
  186. package/dist/plugins/permission-rule-store-sql.js +35 -2
  187. package/dist/plugins/remote-env-device.d.ts +8 -11
  188. package/dist/plugins/remote-env-device.js +6 -3
  189. package/dist/plugins/remote-env-k8s.d.ts +37 -24
  190. package/dist/plugins/remote-env-k8s.js +50 -17
  191. package/dist/plugins/retention-store-sql.d.ts +2 -2
  192. package/dist/plugins/sandbox-read-floor.d.ts +5 -7
  193. package/dist/plugins/sandbox-read-floor.js +2 -7
  194. package/dist/plugins/sql-errors.d.ts +10 -1
  195. package/dist/plugins/sql-errors.js +13 -0
  196. package/dist/plugins/store-backend.d.ts +23 -28
  197. package/dist/plugins/store-backend.js +9 -1
  198. package/dist/plugins/store-contracts.d.ts +29 -5
  199. package/dist/plugins/store-contracts.js +4 -1
  200. package/dist/plugins/task-attachment-store.d.ts +2 -2
  201. package/dist/plugins/task-attachment-store.js +37 -52
  202. package/dist/plugins/tool-result-store-sql.d.ts +9 -8
  203. package/dist/plugins/tool-result-store-sql.js +23 -2
  204. package/dist/plugins/wiring-section-codec.d.ts +32 -0
  205. package/dist/plugins/wiring-section-codec.js +55 -0
  206. package/dist/plugins/workspace-fill.d.ts +1 -1
  207. package/dist/rule-entry-list.d.ts +3 -2
  208. package/dist/rule-entry-list.js +5 -1
  209. package/dist/rules-consent.d.ts +28 -5
  210. package/dist/rules-consent.js +40 -28
  211. package/dist/run-local.js +9 -5
  212. package/dist/runs.d.ts +12 -8
  213. package/dist/runs.js +6 -7
  214. package/dist/runtime-governance.d.ts +1 -1
  215. package/dist/server-secret-env.d.ts +4 -0
  216. package/dist/session-error-wire.d.ts +12 -0
  217. package/dist/session-error-wire.js +6 -0
  218. package/dist/session-incarnation.d.ts +1 -1
  219. package/dist/session-notify-door.d.ts +106 -0
  220. package/dist/session-notify-door.js +93 -0
  221. package/dist/session-sync.d.ts +9 -14
  222. package/dist/session-sync.js +5 -5
  223. package/dist/session-workspace.d.ts +3 -13
  224. package/dist/session-workspace.js +0 -9
  225. package/dist/standing-answer-grant.d.ts +5 -0
  226. package/dist/standing-answer-grant.js +10 -0
  227. package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
  228. package/dist/stores-sql/session-policy-store-sql.js +3 -13
  229. package/dist/task-mcp.d.ts +1 -1
  230. package/dist/task-settings.d.ts +11 -8
  231. package/dist/task-settings.js +31 -11
  232. package/dist/tool-approval.d.ts +52 -34
  233. package/dist/tool-approval.js +66 -57
  234. package/dist/trace/core-keyset-guard.d.ts +11 -8
  235. package/dist/trace/engine-notice-wire.d.ts +1 -1
  236. package/dist/trace/engine-notice-wire.js +3 -1
  237. package/dist/trace/exit-seat.d.ts +7 -1
  238. package/dist/trace/exit-seat.js +1 -0
  239. package/dist/trace/ledger-sink.d.ts +2 -2
  240. package/dist/trace/model-segment.d.ts +55 -25
  241. package/dist/trace/model-segment.js +101 -33
  242. package/dist/trace/project.d.ts +7 -4
  243. package/dist/trace/project.js +6 -0
  244. package/dist/trace/redact.d.ts +61 -9
  245. package/dist/trace/redact.js +1490 -468
  246. package/dist/trace/self-minted-wire.d.ts +510 -38
  247. package/dist/trace/self-minted-wire.js +165 -17
  248. package/dist/trace/wire-projection-faces.d.ts +1 -1
  249. package/dist/trace/wire-projection-faces.js +1 -1
  250. package/dist/verifier-pause-card.d.ts +33 -5
  251. package/dist/verifier-pause-card.js +15 -10
  252. package/dist/web-admin-client/apply-effective.d.ts +12 -16
  253. package/dist/web-admin-client/apply-effective.js +90 -10
  254. package/dist/workspace-space.d.ts +19 -3
  255. package/dist/workspace-space.js +8 -4
  256. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
  257. package/package.json +3 -2
  258. package/wire-changes/server@7.143.0.json +1167 -0
@@ -41,6 +41,14 @@ export declare class FileRunStore implements InProcessClaimFence {
41
41
  private readonly events;
42
42
  /** taskId → the open append log (one O_APPEND fd per run, lazily opened, held for lifetime). */
43
43
  private readonly eventLogs;
44
+ /** `<root>/run-blobs/` — one section-blob ledger per session (plane 4 of the header). */
45
+ private readonly blobsDir;
46
+ /** sessionId → (digest → section body): the index of plane 4 (hydrated per session that has run rows). */
47
+ private readonly sectionBlobs;
48
+ /** sessionId → the open section-blob ledger (lazily opened, held until the session is deleted / dispose). */
49
+ private readonly blobLogs;
50
+ /** `(taskId, seq)` of manifest rows already reported as unresolvable (one report per row per store instance = per process). */
51
+ private readonly unresolvedReported;
44
52
  /** [ref] the checkpoint-table stand-in (see memory-run-store.ts {@link RunStoreCheckpointProbe} — the full
45
53
  * rationale lives on the interface); absent ⇒ the suspended-run reapers stay honest NO-OPs. */
46
54
  private checkpointProbe?;
@@ -61,6 +69,11 @@ export declare class FileRunStore implements InProcessClaimFence {
61
69
  private runJsonPath;
62
70
  private eventsPath;
63
71
  private activePath;
72
+ /** A session's section-blob ledger. The file name is core's `sanitizeScope` form (readable prefix + sha256 of the id), not the
73
+ * bare id: two ids that differ only in letter case are one file on a case-insensitive file system (macOS by default), and a
74
+ * deleted session must never take another session's blocks with it. `undefined` only for a run row read back from a damaged
75
+ * run.json whose session id is not a string — such a row has no ledger (its manifest rows are stored whole). */
76
+ private blobLedgerPath;
64
77
  private spaceCommitPath;
65
78
  /** One space's commit record, or `undefined` (no file). A record that is not this store's shape is a loud read failure
66
79
  * (the reader — the next run's connect — refuses; it never guesses that no commit is in flight). */
@@ -129,6 +142,22 @@ export declare class FileRunStore implements InProcessClaimFence {
129
142
  isPreemptRequested(taskId: string, owner: string | null): Promise<boolean>;
130
143
  heartbeat(taskId: string, owner: string | null): Promise<void>;
131
144
  appendEvent(taskId: string, seq: number, type: LedgerEventType, data: unknown): Promise<void>;
145
+ /**
146
+ * 写口里第四个盘面的那一段(与事件行的追加在**同一同步调用**里,块在前):帧经编解码拆成引用形 + 段体,本会话块账本里还没有的段体先追加
147
+ * (一批一次 fsync:事件行本身 fsync:false,而一只块被同会话之后每一条清单行引用 —— 丢一行丢一帧,丢一块丢一串帧),再把引用形交回给
148
+ * 事件行的追加。块追加抛 ⇒ 原样上抛:事件行不写,不退回整帧(与事件行自己写失败同一条路,汇点的串行写口留 seq 洞)。
149
+ * 这条 run 没有行(会话不详)⇒ 没有块账本可挂 ⇒ 帧原样存:和类型里「没有一段用引用」的合法实例,读口原样交出。
150
+ */
151
+ private storeSections;
152
+ private blobLogFor;
153
+ /** Drop a session's section-blob ledger: the file first, then the open fd and the index. The file removal is NOT best-effort
154
+ * (unlike {@link dropRun}'s directory): it is the delete's first destructive step, so a removal that fails throws before
155
+ * anything else changed — the session delete answers an error and a retry starts from the same state, instead of leaving
156
+ * an index that no longer matches the disk. (`AppendLog.close` never throws: it swallows its own `closeSync`.) */
157
+ private dropSectionBlobs;
158
+ /** 读口的悬空引用臂:清单行有一段装不回 ⇒ 这一帧这一次不交出(永不交出缺段的清单),每 `(taskId, seq)` 每只店实例(= 每进程)留一次 F 类痕;
159
+ * 帧仍在索引里,`maxSeq` / `retainedFrom` 照旧按存位算(下一个写者的 seq 不会撞上它)。 */
160
+ private reportUnresolved;
132
161
  maxSeq(taskId: string): Promise<number>;
133
162
  retainedFrom(taskId: string): Promise<number>;
134
163
  getEvents(taskId: string, afterSeq: number): Promise<RunEvent[]>;
@@ -4,9 +4,10 @@ import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
4
4
  import { join } from "node:path";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
6
6
  import { randomBytes } from "node:crypto";
7
- import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
7
+ import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, sanitizePathComponent, sanitizeScope, writeThenLink } from "@sema-agent/core";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
10
+ import { joinFromStore, splitForStore } from "./wiring-section-codec.js";
10
11
  import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError, readLegacyHolderDeathCode, isTerminalRunStatus, settledLegObservationOf } from "./store-contracts.js";
11
12
  import { notifyRunTerminal } from "../observability/run-terminal.js";
12
13
  import { readRunIdentityCells, runIdentityCells } from "../identity-source.js";
@@ -26,6 +27,13 @@ const EVENT_RECORD_SHAPE = {
26
27
  return typeof record.seq === "number" ? record : undefined;
27
28
  },
28
29
  };
30
+ const SECTION_BLOB_SHAPE = {
31
+ what: "wiring-section blob record without a string digest and an object/array body",
32
+ read(record) {
33
+ const r = record;
34
+ return typeof r.digest === "string" && typeof r.body === "object" && r.body !== null ? record : undefined;
35
+ },
36
+ };
29
37
  const CLAIM_RECORD_SHAPE = {
30
38
  what: "active-run claim record without a string sessionId / taskId",
31
39
  read(record) {
@@ -61,6 +69,10 @@ export class FileRunStore {
61
69
  byIdem = new Map();
62
70
  events = new Map();
63
71
  eventLogs = new Map();
72
+ blobsDir;
73
+ sectionBlobs = new Map();
74
+ blobLogs = new Map();
75
+ unresolvedReported = new Set();
64
76
  checkpointProbe;
65
77
  claimOwner = { nonce: randomBytes(12).toString("hex"), ...processFingerprint() };
66
78
  claimLocks = new Map();
@@ -72,6 +84,7 @@ export class FileRunStore {
72
84
  this.runsDir = join(root, "runs");
73
85
  this.activeDir = join(root, "runs", "active");
74
86
  this.tmpDir = join(root, "tmp");
87
+ this.blobsDir = join(root, "run-blobs");
75
88
  this.spaceCommitsDir = join(root, "space-commits");
76
89
  this.spaceCommits = createInProcessSpaceCommitTable({
77
90
  load: (spaceKey) => this.loadSpaceCommit(spaceKey),
@@ -109,6 +122,9 @@ export class FileRunStore {
109
122
  activePath(sessionId) {
110
123
  return join(this.activeDir, sanitizePathComponent(sessionId));
111
124
  }
125
+ blobLedgerPath(sessionId) {
126
+ return typeof sessionId === "string" ? join(this.blobsDir, `${sanitizeScope(sessionId)}.jsonl`) : undefined;
127
+ }
112
128
  spaceCommitPath(spaceKey) {
113
129
  return join(this.spaceCommitsDir, `${sanitizePathComponent(spaceKey)}.json`);
114
130
  }
@@ -144,6 +160,7 @@ export class FileRunStore {
144
160
  const { identitySource, identityScope, identityPrincipalInc, ...rest } = d;
145
161
  return {
146
162
  ...rest,
163
+ result: d.result ?? null,
147
164
  idemKey: typeof d.idemKey === "string" ? d.idemKey : null,
148
165
  idemBodyFingerprint: typeof d.idemBodyFingerprint === "string" ? d.idemBodyFingerprint : null,
149
166
  identity: readRunIdentityCells(identitySource, identityScope, identityPrincipalInc),
@@ -261,6 +278,19 @@ export class FileRunStore {
261
278
  this.events.set(disk.taskId, list);
262
279
  }
263
280
  }
281
+ for (const sessionId of new Set([...this.runs.values()].map((r) => r.sessionId))) {
282
+ const path = this.blobLedgerPath(sessionId);
283
+ if (path === undefined)
284
+ continue;
285
+ const recs = this.replay(path, SECTION_BLOB_SHAPE).records;
286
+ if (recs.length === 0)
287
+ continue;
288
+ const index = new Map();
289
+ for (const b of recs)
290
+ if (!index.has(b.digest))
291
+ index.set(b.digest, b.body);
292
+ this.sectionBlobs.set(sessionId, index);
293
+ }
264
294
  let claims;
265
295
  try {
266
296
  claims = readdirSync(this.activeDir);
@@ -435,10 +465,58 @@ export class FileRunStore {
435
465
  return;
436
466
  const clone = data == null ? null : structuredClone(data);
437
467
  const ts = new Date().toISOString();
438
- this.eventLogFor(taskId).append({ seq, type, data: clone, ts }, false);
439
- list.push({ seq, type, data: clone, ts });
468
+ const stored = this.storeSections(taskId, type, clone);
469
+ this.eventLogFor(taskId).append({ seq, type, data: stored, ts }, false);
470
+ list.push({ seq, type, data: stored, ts });
440
471
  this.events.set(taskId, list);
441
472
  }
473
+ storeSections(taskId, type, data) {
474
+ const { stored, blobs } = splitForStore(type, data);
475
+ if (blobs.length === 0)
476
+ return stored;
477
+ const sessionId = this.runs.get(taskId)?.sessionId;
478
+ const path = sessionId === undefined ? undefined : this.blobLedgerPath(sessionId);
479
+ if (sessionId === undefined || path === undefined)
480
+ return data;
481
+ let index = this.sectionBlobs.get(sessionId);
482
+ if (index === undefined) {
483
+ index = new Map();
484
+ this.sectionBlobs.set(sessionId, index);
485
+ }
486
+ const fresh = blobs.filter((b) => !index.has(b.digest));
487
+ if (fresh.length > 0) {
488
+ const log = this.blobLogFor(sessionId, path);
489
+ for (const [i, b] of fresh.entries())
490
+ log.append({ digest: b.digest, body: b.body }, i === fresh.length - 1);
491
+ for (const b of fresh)
492
+ index.set(b.digest, b.body);
493
+ }
494
+ return stored;
495
+ }
496
+ blobLogFor(sessionId, path) {
497
+ let log = this.blobLogs.get(sessionId);
498
+ if (!log) {
499
+ log = new AppendLog(path);
500
+ this.blobLogs.set(sessionId, log);
501
+ }
502
+ return log;
503
+ }
504
+ dropSectionBlobs(sessionId) {
505
+ const path = this.blobLedgerPath(sessionId);
506
+ if (path !== undefined)
507
+ rmSync(path, { force: true });
508
+ this.blobLogs.get(sessionId)?.close();
509
+ this.blobLogs.delete(sessionId);
510
+ this.sectionBlobs.delete(sessionId);
511
+ }
512
+ reportUnresolved(taskId, seq, missing) {
513
+ const key = `${taskId}\u0000${seq}`;
514
+ if (this.unresolvedReported.has(key))
515
+ return;
516
+ this.unresolvedReported.add(key);
517
+ const sections = missing.map((m) => `${m.section}:${m.digest}`).join(",");
518
+ recordFailOpen("server.run-store.corrupt-record-skipped", `kind=section-ref taskId=${taskId} seq=${seq} sections=${sections} reason=wiring_manifest row references section blobs its session's ledger does not hold (row withheld from reads)`);
519
+ }
442
520
  async maxSeq(taskId) {
443
521
  const list = this.events.get(taskId);
444
522
  return list && list.length ? Math.max(...list.map((e) => e.seq)) : 0;
@@ -449,10 +527,19 @@ export class FileRunStore {
449
527
  }
450
528
  async getEvents(taskId, afterSeq) {
451
529
  const list = this.events.get(taskId) ?? [];
452
- return list
453
- .filter((e) => e.seq > afterSeq)
454
- .sort((a, b) => a.seq - b.seq)
455
- .map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
530
+ const sessionId = this.runs.get(taskId)?.sessionId;
531
+ const blobs = sessionId === undefined ? undefined : this.sectionBlobs.get(sessionId);
532
+ const lookup = (digest) => blobs?.get(digest);
533
+ const out = [];
534
+ for (const e of list.filter((x) => x.seq > afterSeq).sort((a, b) => a.seq - b.seq)) {
535
+ const joined = joinFromStore(e.type, e.data, lookup);
536
+ if (joined.kind === "unresolved") {
537
+ this.reportUnresolved(taskId, e.seq, joined.missing);
538
+ continue;
539
+ }
540
+ out.push({ ...e, data: joined.data == null ? null : structuredClone(joined.data) });
541
+ }
542
+ return out;
456
543
  }
457
544
  async setTerminal(taskId, status, result, error) {
458
545
  await fenceSpaceCommitAtTerminal(taskId, async (spaceCommit) => this.writeTerminal(taskId, status, result, error, spaceCommit));
@@ -661,6 +748,7 @@ export class FileRunStore {
661
748
  const claimed = this.active.get(sessionId);
662
749
  if (claimed !== undefined)
663
750
  return { active: claimed };
751
+ this.dropSectionBlobs(sessionId);
664
752
  let removed = 0;
665
753
  for (const [taskId, r] of [...this.runs]) {
666
754
  if (r.sessionId !== sessionId)
@@ -1024,7 +1112,7 @@ export class FileRunStore {
1024
1112
  return new Set([...this.runs.values()].map((r) => r.sessionId));
1025
1113
  }
1026
1114
  dispose() {
1027
- for (const log of this.eventLogs.values()) {
1115
+ for (const log of [...this.eventLogs.values(), ...this.blobLogs.values()]) {
1028
1116
  try {
1029
1117
  log.close();
1030
1118
  }
@@ -1032,6 +1120,7 @@ export class FileRunStore {
1032
1120
  }
1033
1121
  }
1034
1122
  this.eventLogs.clear();
1123
+ this.blobLogs.clear();
1035
1124
  }
1036
1125
  }
1037
1126
  //# sourceMappingURL=file-run-store.js.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 🔴 一次性迁移 M36(窗口见 `plugins/one-time-migrations.ts` 登记表)—— **引擎 7.61.0 之前导入的、在每个项目根放行的全局 allow,按引擎写下的那一句判据退役**。
3
+ *
4
+ * ## 为什么
5
+ * 引擎 7.61.0 之前的设置导入门把用户层(用户配置目录下的那份 settings)里从 `/` 写起的路径规则存成**全局行**。全局行在每个任务的**项目根**读 `/x`,
6
+ * 用户层却在**它自己的配置目录**读 —— 同一串字落店之后守着 / 放着的是另一个地方:一条本意是「配置目录下的 notes」的放行 `Edit(/notes/**)`
7
+ * 在每个项目里放行 `notes/` 的编辑、不问。引擎 7.61.0 起导入门不再这样存(`preview.skipped`,理由前缀 `unsupported.layer_root:`);
8
+ * **存量行**引擎不迁、也不在读侧丢 —— 判据只写成一句话(引擎 INTEGRATION-CORE §5.14),由店的属主跑。本文件就是那一次跑。
9
+ *
10
+ * ## 判据(逐字照引擎那一句;不加条件、不减条件)
11
+ * 一个已存的 add 被退役,当且仅当四条都成立:行的态是 `allow`;add 的出处是 `imported-cc`;行的 scope 是 `global`;`ruleBasesNeeded(row)` 点名 `root`。
12
+ * **按 add 退役、不按行**:每只选中的行写**一只** observed-remove 墓碑,`removedDots` 恰是选中的那几只 add —— 同一行另有一只 `user` add 的,那一行照旧活着
13
+ * (人亲手同意过的那一份不跟着导入一起走)。deny / ask 的 add **不退役**:它们按店的读法(`<task>/x`)只收紧,退役会把部署有意带进来的收紧放掉。
14
+ *
15
+ * ## 读哪些桶(店自己答;答不全的那一种如实写下)
16
+ * 一律问**店**:它能叫出名字的每一只桶,判据在桶里判(不在桶外按「谁可能有」预筛)。
17
+ * · SQL:规则表里的每一只桶(`listRuleBucketOwners`,按 `owner_key` 分页)。不从审批记录推 —— 收编把规则桶整桶改键到新身份、审批史留在旧身份名下,
18
+ * 从记录推出的属主集在收编之后漏掉新桶。
19
+ * · File:引擎的 File 分区后端**没有**列桶口(桶文件按属主名的单向摘要命名),本步能叫出名字的只有本店导入记录点名过的属主
20
+ * (`FileRuleApprovalRecordStore.importRecordOwners`;`imported-cc` 只由导入记录派生,记录是只增日志)。**已知缺口(如实)**:引擎的本地数据根收编
21
+ * 把一只桶改名到一个没有任何导入记录点名的身份之后,那只桶本步叫不出、不读 —— 等引擎给 File 分区后端一个列桶口再收。
22
+ *
23
+ * ## 写在哪里(一处)
24
+ * 墓碑经**这只桶自己的写面**落(`writerOf(partition).apply({ kind: "tighten-delete" })`,引擎的写面契约)—— SQL 两孪生走 `SqlDurableRulePartition` 的那只
25
+ * (店内唯一写法 `writeBucket`),File 走引擎 File 分区的那只。同一个函数、同一次调用,不分叉。删除点(`deletedBy`)由这只桶的写面 `nextDot()` 铸
26
+ * —— 与引擎撤销口 `removePersistedRule` 同一约定(本副本的 actor,一次删除一枚)。OCC:读到的 rev 交给 `apply`,输了就重读重选(另一只副本可能刚退役过,
27
+ * 重选为空 ⇒ 零写),有界次数后点名跳过、下一次起服再来。
28
+ *
29
+ * ## 失败方向
30
+ * 列桶 / 解析桶 / 读桶 / 取点 / 写面**抛** ⇒ 原样抛:起服拒,收据账里已提交的墓碑随拒因交出(与本段其余各步同一条路)。例外两条,都由店的判官按抛出物的**类**判(不按文案):
31
+ * 取点时店说这只桶**在写它的那一步里没了**(`source.isVanishedBucket`:另一只副本删桶恰落在这一窗)⇒ 无可退役、点名跳过 `vanished`(桶都没了,它什么都不放行);
32
+ * 另一条是解析桶(属主 → 分区)或读桶时**店的唯一判官整桶拒**了
33
+ * 这只桶(解析与读同一条律、同一只判官;`source.isWholeBucketRefusal`:确定性判决,引擎读侧对它同样整桶拒、它什么都不放行;SQL = 读腿真往外交的那两类 —— 判官四道的拒与某只 JSON 列的体读不出)
34
+ * ⇒ 点名跳过 `bucket_unreadable`,接着读下一只桶(一只读不出的桶不挡别的桶,也不挡起服);一次读失败(连接 / 超时)
35
+ * 证明不了以后也读不出,所以它**不**归这一类 —— 跳过它就是让那几行在读恢复之后照旧放行。File 店没有这种判决证据(引擎的 File 分区对损坏与 I/O 失败抛同一种错,
36
+ * 解析时读收编标记也是:一次 EIO 与一份坏标记同一种错)⇒ 那一侧解析 / 读桶抛一律起服拒。另两种店**答了**却做不了的(没有写面或写面拒 / 并发争不过)⇒ 点名跳过,下一次起服再试,其间那几行照旧放行,每次起服都点名。
37
+ */
38
+ import { type DurableRulePartitionProvider, type PersistedRule, type RuleAdd, type RuleOwner } from "@sema-agent/core";
39
+ /** 一只被退役的导入 add 所在的行(一行一条读数)。**不带规则正文**(与 M11 同一条纪律:规则正文不出店)—— 桶键 + 工具名足够让运维去列举面对上它。 */
40
+ export interface ImportedLayerRootAllowRetired {
41
+ /** 桶键(`buildRuleOwnerKey`:属主的单向摘要;两种店同一把,日志里不出属主名)。 */
42
+ readonly ownerKey: string;
43
+ readonly tool: string;
44
+ /** 这一行被退役的导入 add 数(≥ 1)。 */
45
+ readonly addsRetired: number;
46
+ /** 退役之后这一行是否仍活着 —— 它另有别的出处的 add(人亲手同意过的那一份)。`false` = 这一行不再生效。 */
47
+ readonly stillLive: boolean;
48
+ }
49
+ /**
50
+ * 跳过一只桶的成因(闭集;日志句按它穷尽分)。
51
+ * · `bucket_unreadable` —— 解析或读这只桶时,店的唯一判官整桶拒了它(`isWholeBucketRefusal` 真)。引擎读侧对它同样整桶拒:它什么都不放行。
52
+ * · `vanished` —— 读到了要退役的行,写它的那一步里这只桶被并发删掉了(店的判官按类说)。桶没了 ⇒ 无可退役,什么都不放行;不拒启。
53
+ * · `not_writable` —— 选中了行,而这只桶没有写面(只读视图)或写面拒了这只墓碑。那几行照旧放行,每次起服都点名。
54
+ * · `contended` —— 每一次写都输给并发写者,有界次数用完。下一次起服再来;其间照旧放行。
55
+ */
56
+ export type ImportedLayerRootSkipReason = "bucket_unreadable" | "vanished" | "not_writable" | "contended";
57
+ export interface ImportedLayerRootBucketSkipped {
58
+ readonly ownerKey: string;
59
+ readonly reason: ImportedLayerRootSkipReason;
60
+ /** 一句不带行内容的话:读 / 解析桶抛出来的东西只报它的种类(原句可能引到桶里的行 —— 与 M11 `contentFreeRefusal` 同一条纪律;
61
+ * 原句由店自己的披露口说,File 店的 `permission_rule_store_disclosure`、SQL 店的整桶拒句);写面拒的是闭集词,原样带。 */
62
+ readonly why: string;
63
+ }
64
+ /** 本步记进收据账的两格(`OneTimeMigrationReport` 的两只键,随提交追加)。 */
65
+ export interface ImportedLayerRootReadings {
66
+ importedLayerRootAllowsRetired: ImportedLayerRootAllowRetired[];
67
+ importedLayerRootBucketsSkipped: ImportedLayerRootBucketSkipped[];
68
+ }
69
+ /** 一种店交给本步的三样东西:它能叫出名字的桶的属主(见头注「读哪些桶」)、它的 durable 分区后端(读桶、写墓碑)、它的整桶拒判别(见头注「失败方向」)。 */
70
+ export interface ImportedLayerRootRetirementSource {
71
+ candidateOwners(): Promise<readonly RuleOwner[]>;
72
+ readonly durable: DurableRulePartitionProvider;
73
+ /** 这只读桶抛出物是不是店的唯一判官的**确定性**整桶拒(真 ⇒ 点名跳过、读下一只桶;假 ⇒ 原样抛、起服拒)。判的是读面**真往外交的**抛出物的类,不按文案。 */
74
+ isWholeBucketRefusal(e: unknown): boolean;
75
+ /** 这只取点抛出物是不是店说的「桶在写它的那一步里没了」(真 ⇒ 点名跳过 `vanished`;假 ⇒ 原样抛、起服拒)。 */
76
+ isVanishedBucket(e: unknown): boolean;
77
+ }
78
+ /** 判据(引擎 §5.14 那一句,逐字):这一行里该退役的 add;不选 ⇒ 空。 */
79
+ export declare function importedLayerRootAddsOf(row: PersistedRule): readonly RuleAdd[];
80
+ /** 跑一遍:读记录点名的每只桶(按桶键去重),退役判据选中的 add。读数随每次提交记进 `into`。 */
81
+ export declare function retireImportedLayerRootAllows(source: ImportedLayerRootRetirementSource, into: ImportedLayerRootReadings): Promise<void>;
82
+ //# sourceMappingURL=imported-layer-root-retirement.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { ruleBasesNeeded, writerOf } from "@sema-agent/core";
2
+ import { buildRuleOwnerKey } from "./permission-rule-store-sql.js";
3
+ const RETIREMENT_OCC_ATTEMPTS = 5;
4
+ export function importedLayerRootAddsOf(row) {
5
+ if (row.behavior !== "allow" || row.scope.kind !== "global" || !ruleBasesNeeded(row).includes("root"))
6
+ return [];
7
+ return row.adds.filter((a) => a.origin === "imported-cc");
8
+ }
9
+ export async function retireImportedLayerRootAllows(source, into) {
10
+ const seen = new Set();
11
+ for (const owner of await source.candidateOwners()) {
12
+ const ownerKey = buildRuleOwnerKey(owner);
13
+ if (seen.has(ownerKey))
14
+ continue;
15
+ seen.add(ownerKey);
16
+ await retireInBucket(source, owner, ownerKey, into);
17
+ }
18
+ }
19
+ async function retireInBucket(source, owner, ownerKey, into) {
20
+ const durable = source.durable;
21
+ const skip = (reason, why) => void into.importedLayerRootBucketsSkipped.push({ ownerKey, reason, why });
22
+ const unreadable = (what, e) => {
23
+ if (!source.isWholeBucketRefusal(e))
24
+ throw e;
25
+ skip("bucket_unreadable", withheld(what, e));
26
+ };
27
+ let partition;
28
+ try {
29
+ partition = partitionOf(durable, owner);
30
+ }
31
+ catch (e) {
32
+ return unreadable("the store refused this bucket as a whole while resolving it", e);
33
+ }
34
+ for (let attempt = 0; attempt < RETIREMENT_OCC_ATTEMPTS; attempt++) {
35
+ let stored;
36
+ try {
37
+ stored = await partition.list();
38
+ }
39
+ catch (e) {
40
+ return unreadable("the store refused this bucket as a whole", e);
41
+ }
42
+ const selected = stored.rules.flatMap((row) => {
43
+ const adds = importedLayerRootAddsOf(row);
44
+ return adds.length > 0 ? [{ row, adds }] : [];
45
+ });
46
+ if (selected.length === 0)
47
+ return;
48
+ const writer = writerOf(partition);
49
+ if (writer === undefined)
50
+ return skip("not_writable", "this bucket is served through a read-only view (it has no write face)");
51
+ let rev = stored.rev;
52
+ let lost = false;
53
+ for (const { row, adds } of selected) {
54
+ let deletedBy;
55
+ try {
56
+ deletedBy = await writer.nextDot();
57
+ }
58
+ catch (e) {
59
+ if (!source.isVanishedBucket(e))
60
+ throw e;
61
+ return skip("vanished", "the bucket was deleted while this step was writing to it (a concurrent delete); nothing is left to retire");
62
+ }
63
+ const tombstone = { behavior: row.behavior, rule: row.rule, scope: row.scope, removedDots: adds.map((a) => a.dot), deletedBy };
64
+ const out = await writer.apply({ kind: "tighten-delete", tombstone }, { expectedRev: rev });
65
+ if ("conflict" in out) {
66
+ lost = true;
67
+ break;
68
+ }
69
+ if ("refused" in out)
70
+ return skip("not_writable", `the store refused the tombstone (${out.refused})`);
71
+ rev = out.rev;
72
+ into.importedLayerRootAllowsRetired.push({ ownerKey, tool: row.tool, addsRetired: adds.length, stillLive: row.adds.length > adds.length });
73
+ }
74
+ if (!lost)
75
+ return;
76
+ }
77
+ skip("contended", `every write lost to a concurrent writer ${RETIREMENT_OCC_ATTEMPTS} times`);
78
+ }
79
+ function partitionOf(durable, owner) {
80
+ switch (owner.kind) {
81
+ case "principal":
82
+ return durable.forPrincipal(owner.principal);
83
+ case "local-owner":
84
+ if (durable.forLocalOwner === undefined)
85
+ throw new Error("this provider has no local-owner bucket");
86
+ return durable.forLocalOwner();
87
+ default: {
88
+ const unknown = owner;
89
+ throw new Error(`M36: an owner of a kind this pass does not know: ${JSON.stringify(unknown)}`);
90
+ }
91
+ }
92
+ }
93
+ function withheld(what, e) {
94
+ return `${what} threw ${e instanceof Error ? e.name : typeof e} (its text is withheld here: it may quote the bucket's rows)`;
95
+ }
96
+ //# sourceMappingURL=imported-layer-root-retirement.js.map
@@ -151,7 +151,7 @@ export declare class LocalCheckpointStore {
151
151
  * 唯一的读不动形是账本重放级(整店同时读不动)。所以 SQL 孪生那道 `version > MAX_SUPPORTED` 的
152
152
  * `unparseable` 臂在这里没有对应物;本口的 `unparseable` 只由**窄读器**判(blob 形不合)。
153
153
  * · **窄读器是同一只** `pendingActionToolCallId`(与 SQL 孪生共用,零第二份手抄):身份三元组的第一维
154
- * `sourceTaskId`、`boundInputHash`、以及「读不出 vs 确定不匹配」的三态,全部由它一家判。
154
+ * `sourceSessionId`、`boundInputHash`、以及「读不出 vs 确定不匹配」的三态,全部由它一家判。
155
155
  */
156
156
  findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
157
157
  /**
@@ -278,12 +278,12 @@ export class LocalCheckpointStore {
278
278
  const base = { token: t, status: cp.status, createdAtMs: cp.createdAt };
279
279
  const derived = pendingActionToolCallId(cp);
280
280
  if (!derived.readable) {
281
- out.push({ ...base, sourceTaskId: null, boundCallId: null, boundInputHash: null, unparseable: true });
281
+ out.push({ ...base, sourceSessionId: null, boundCallId: null, boundInputHash: null, unparseable: true });
282
282
  continue;
283
283
  }
284
284
  if (derived.toolCallId !== toolCallId)
285
285
  continue;
286
- out.push({ ...base, sourceTaskId: derived.sourceTaskId, boundCallId: derived.toolCallId, boundInputHash: derived.boundInputHash });
286
+ out.push({ ...base, sourceSessionId: derived.sourceSessionId, boundCallId: derived.toolCallId, boundInputHash: derived.boundInputHash });
287
287
  }
288
288
  return out.sort((a, b) => a.createdAtMs - b.createdAtMs || (a.token < b.token ? -1 : a.token > b.token ? 1 : 0));
289
289
  }
@@ -272,6 +272,8 @@ export declare class LocalSessionStore implements SessionStore {
272
272
  /**
273
273
  * 改写面与执行席的读法({@link placementProbe} 的一形):placed ⇒ 那份元组;普通 ⇒ `undefined`;行头读不出(`corrupt_session` / `invalid_session`)⇒ `undefined` —— 没有可读的
274
274
  * placement 记录,这一行按普通行答,内容读不读得出归真正读内容的那一读(坏档照样换得掉,与 forking 探针同一条规矩);说不出(`storage`)⇒ 抛那只故障。
275
+ * core 7.61.0 起 repo 的探针按行头的元组自己读、不整头解码:placed 行的属主格坏了探针照答元组(本臂不再接得住 placed 行)—— 探针拒只剩「行头记录整个读不出」,
276
+ * 那时没有任何元组可守(`test/local-session-placement.test.ts` 的 [ref] 三格)。
275
277
  */
276
278
  private placedRecordOf;
277
279
  /** {@link placedRecordOf} 的是非形。 */
@@ -288,7 +290,8 @@ export declare class LocalSessionStore implements SessionStore {
288
290
  private metaOf;
289
291
  /**
290
292
  * {@link metaOf} 的本体(三态,不抛「用不了」):边档里有那一格 ⇒ 读它;没有 ⇒ 问**唯一探针** {@link probeArchive} 一次 —— 不在 ⇒ 无此会话;在而读得出 ⇒ 补铸({@link cellOfPresent});
291
- * 读不出 ⇒ {@link UnusableArchive}(**不补铸**:不替一份读不出的存档铸化身)。要动存档的读者({@link metaOf})把它上抛;不动存档、只要「有没有」的读者
293
+ * 读不出 ⇒ {@link UnusableArchive}(**不补铸**:不替一份读不出的存档铸化身;`corrupt_session` / `invalid_session` 两词的故障换成具名拒 {@link SessionOwnerUnreadableError} ——
294
+ * 格缺 ∧ 存档用不了 = 这只会话没有可读的属主,见 {@link ownerUnreadableArchive})。要动存档的读者({@link metaOf})把它上抛;不动存档、只要「有没有」的读者
292
295
  * ({@link executionPlacementBackfill})原样收下。
293
296
  */
294
297
  private metaRead;
@@ -304,7 +307,8 @@ export declare class LocalSessionStore implements SessionStore {
304
307
  * 叶子出自会话存档(本店的**内容**;core `SessionRepo.headOf`:File repo 读日志首记录与尾、不重放 —— 存档首行的属主 / 令牌本店不读)。
305
308
  * 存档读不出(会话头坏了、I/O 故障)⇒ 头照答行上的属主与住客,叶子那一格带着那只故障(`UnreadableLeaf`,读叶子的人收到它):只判归属的读者
306
309
  * 不因坏存档改判,坏存档只让读内容的那一位看见(SQL 孪生的头不碰 `session_event`,同一条律;规则在 `session-incarnation.ts` 头注)。
307
- * 行本身读不出(边档里还没有这一格、要从存档推出这只会话在不在,而存档读不出 —— {@link metaOf} 的探针)⇒ 整只拒。
310
+ * 行本身读不出(边档里还没有这一格、要从存档推出这只会话在不在,而存档读不出 —— {@link metaOf} 的探针)⇒ 整只拒:会话头坏 / 日志结构走不通 ⇒ 具名拒
311
+ * {@link SessionOwnerUnreadableError}(没有可读的属主;码与句子是原故障的),说不出在不在的 I/O 级故障 ⇒ 原故障。
308
312
  * 两样住在两只文件里 ⇒ 先读那一格、读叶子、再同步复读那一格:格没了(读间被删)或换了主 / 换了住客 ⇒ `undefined`(调用方按删处理),
309
313
  * 所以一只头从不把这一代的属主配上另一代的叶子([ref] 六轮复审同一条)。`undefined` = 无此会话。
310
314
  * 化身那一格经 `headFromRow` 的 `absent` 读法(边档对象形恒有令牌;读不出的边档整只严格拒在 `metaOf` 之前)。
@@ -4,7 +4,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
7
- import { StampedSessionArchiveError, sessionUnreadableOf } from "../session-error-wire.js";
7
+ import { SessionOwnerUnreadableError, StampedSessionArchiveError, sessionUnreadableOf } from "../session-error-wire.js";
8
8
  import { FileSessionRepo, SessionError, StoredSession, admitSessionPlacement, atomicWriteFile, sessionLogMarkOf, settledForkOptions, uuidv7, validateEntriesForImport } from "@sema-agent/core";
9
9
  import { createPlacedSessionRow, sourceFacesVia } from "../delegated-session-owner.js";
10
10
  import { placementCapabilityOf } from "./session-placement.js";
@@ -54,6 +54,12 @@ function unusableArchiveOf(word, fault) {
54
54
  const code = fault instanceof SessionError ? undefined : fault?.code;
55
55
  return { state: "unusable", word, fault, ...(word === "storage" && typeof code === "string" ? { errno: code } : {}) };
56
56
  }
57
+ function ownerUnreadableArchive(probe) {
58
+ const { fault } = probe;
59
+ if (probe.word === "storage" || !(fault instanceof SessionError))
60
+ return probe;
61
+ return { ...probe, fault: new SessionOwnerUnreadableError(probe.word, fault) };
62
+ }
57
63
  function importRowRefused(sessionId, landing) {
58
64
  return new SessionError("conflict", landing === "create"
59
65
  ? `session ${sessionId} already exists — importEntries only creates a session (replaceEntries rewrites one); nothing was written`
@@ -597,7 +603,7 @@ export class LocalSessionStore {
597
603
  return { state: "found", meta: readSessionMetaCell(cell) };
598
604
  const probe = await this.probeArchive(sessionId);
599
605
  if (probe.state === "unusable")
600
- return probe;
606
+ return ownerUnreadableArchive(probe);
601
607
  if (probe.state === "absent")
602
608
  return { state: "absent" };
603
609
  const meta = await this.cellOfPresent(sessionId);
@@ -1,11 +1,8 @@
1
1
  /**
2
- * 信箱**收件人生命周期面**([ref])—— core `MailboxStore` 契约里被点名、但机制**刻意不由 core 命名**的
3
- * 那一半:「这个收件人正在被部署删掉」。
4
- *
5
- * core 的原话(`core/store-contracts/mailbox-store-contract.ts` 的 `MailboxTombstonedRecipientContractHooks`
6
- * 逐字):「Put `(scope, handle)` into the deployment's PRE-DELETE state — whatever that is for this backend
7
- * (a cascade marking the session row, a `deleting` column, a tombstone table). Core does not name the
8
- * mechanism, only what `append` must then do.」 ⇒ 本文件就是 server 侧那个 "whatever"。
2
+ * 信箱**收件人生命周期面**([ref])—— 「这个收件人正在被部署删掉」。core 7.61.0 起它是 `MailboxStore` 契约里的一对可选成员
3
+ * (`retireRecipient` / `listRetiredRecipients`,一个能力:两个都在或都不在),经 core 的唯一判官 `mailboxRecipientLifecycleOf` 读
4
+ * (缺一半 ⇒ 判官拒)。本仓不另立型:{@link MailboxRecipientLifecycle} 就是那只判官答的形,{@link recipientLifecycleOf} 是本仓唯一的读法 ——
5
+ * 店带那一对 ⇒ 判官的答;不带 ⇒ {@link createDropOnlyRecipientLifecycle}(见文末「缺席形」)。
9
6
  *
10
7
  * ## 两个动词,各答一个问题
11
8
  *
@@ -50,30 +47,11 @@
50
47
  * 后端选择点(`boot/stores.ts` 的 `mailbox_store_enabled` 行)逐次落 `recipientTombstones` 读数,
51
48
  * 披露在 `docs/DEPLOY-PREREQS.md`。方向上它也不放宽任何东西 —— 两种形都**不收**给已删会话的信。
52
49
  */
53
- import type { MailboxStore } from "@sema-agent/core";
54
- /** 一条墓碑(= 一个已退役的收件人)。 */
55
- export interface MailboxRecipientTombstone {
56
- /** 盒句柄(会话盒 = `session.<小写 id>`)。 */
57
- readonly handle: string;
58
- /** 退役时刻(epoch ms)——目录据此铸 `diedAt`。 */
59
- readonly retiredAt: number;
60
- /** 退役前的显示名(会话的删前 `title`);缺席 = 当时没有可用的名字。 */
61
- readonly label?: string;
62
- }
63
- export interface MailboxRecipientLifecycle {
64
- /**
65
- * 把 `(scope, handle)` 置入**预删除态**:此后 `append` 一律以 `MAILBOX_TOMBSTONED_RECIPIENT_CODE` 拒。
66
- * **只打标**——不删消息、不动租约、不碰盒行的高水位(理由 = core T2 试剂盒的两格副作用判据,见模块头)。
67
- * 幂等(重复 retire 只更新时刻/名字)。
68
- *
69
- * @returns 是否真的落了一张耐久墓碑。`false` 有两种,都是「没落」:① 本后端没有预删除态,箱已被直接 `drop`
70
- * (拒的方向不变,退化的只有拒绝措辞与「正在删除中」那一行的可见性);② 这只键收件箱的键判官不收(SQL 两孪生:没有箱可打标,
71
- * 这只键的 `append` 本来就以判官那一句拒,不带墓碑码)。
72
- */
73
- retireRecipient(scope: string, handle: string, at: number, label?: string): Promise<boolean>;
74
- /** 本 scope 下句柄以 `handlePrefix` 开头的墓碑。无预删除态的后端恒返回空。 */
75
- listRetiredRecipients(scope: string, handlePrefix: string): Promise<readonly MailboxRecipientTombstone[]>;
76
- }
50
+ import { mailboxRecipientLifecycleOf, type MailboxStore } from "@sema-agent/core";
51
+ /** 收件人生命周期面 = core 判官 `mailboxRecipientLifecycleOf` 答的那一对(本仓零份第二型;墓碑行型直接用 core 的 `MailboxRecipientTombstone`)。 */
52
+ export type MailboxRecipientLifecycle = NonNullable<ReturnType<typeof mailboxRecipientLifecycleOf>>;
53
+ /** 一只店的收件人生命周期面 —— 本仓唯一的读法:店带那一对 ⇒ core 判官的答(缺一半判官就拒,`config.invalid_argument`);不带 ⇒ drop-only 形。 */
54
+ export declare function recipientLifecycleOf(store: MailboxStore): MailboxRecipientLifecycle;
77
55
  /**
78
56
  * 看不见收件人生命周期的后端的诚实形:retire = 直接把箱 `drop` 掉(**不**打标,因为没有可打标的地方),
79
57
  * 墓碑清单恒空。
@@ -1,3 +1,7 @@
1
+ import { mailboxRecipientLifecycleOf } from "@sema-agent/core";
2
+ export function recipientLifecycleOf(store) {
3
+ return mailboxRecipientLifecycleOf(store) ?? createDropOnlyRecipientLifecycle(store);
4
+ }
1
5
  export function createDropOnlyRecipientLifecycle(store) {
2
6
  return {
3
7
  retireRecipient: async (scope, handle) => {
@@ -1,8 +1,7 @@
1
1
  import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import type { MailboxAckResult, MailboxAppendMessage, MailboxDecision, MailboxDecisionOutcome, MailboxLease, MailboxStore } from "@sema-agent/core";
3
+ import type { MailboxAckResult, MailboxAppendMessage, MailboxDecision, MailboxDecisionOutcome, MailboxLease, MailboxRecipientTombstone, MailboxStore } from "@sema-agent/core";
4
4
  import { type IndexSpec } from "./ensure-index.js";
5
- import type { MailboxRecipientLifecycle, MailboxRecipientTombstone } from "./mailbox-recipient-lifecycle.js";
6
5
  import type { SqlDialect } from "./sql-driver.js";
7
6
  export declare const MAILBOX_TABLE = "mailbox";
8
7
  export declare const MAILBOX_MSG_TABLE = "mailbox_message";
@@ -12,6 +11,11 @@ export declare const MAILBOX_MSG_TABLE = "mailbox_message";
12
11
  * —— `mailbox` 行是**盒**事实(next_seq / lease),墓碑是**收件人**事实,两者的生命周期本来就不同
13
12
  * (`drop` 同时终结两者 —— 墓碑随箱亡,理由全文见 `mailbox-recipient-lifecycle.ts` 模块头)。 */
14
13
  export declare const MAILBOX_TOMBSTONE_TABLE = "mailbox_tombstone";
14
+ /** 盒的**已收窗**(core 7.61.0):`ack` 删掉的带身份记录各留一行 `{ record_id, seq, digests }`,每盒恰留按 `seq` 最新的 `MAILBOX_CONSUMED_ID_WINDOW` 行;
15
+ * `append` 在盒行锁之内连同持有行一起交引擎的 `judgeMailboxAppendById` 判(同记录 ⇒ 答它当时的 `seq`、零写;不同 ⇒ `mailbox.duplicate_record_id`)。
16
+ * 是**盒**事实(与 `next_seq` 同生命周期):`drop` 随盒删、`reap` 留着(它清掉的东西从没被收过)。新表而不是 `mailbox` 加列,理由同墓碑表(存量库零运维动作)。
17
+ * `digests` = 引擎 `mailboxReplayDigests` 的四面摘要 JSON(只读那一只函数,不另算)。 */
18
+ export declare const MAILBOX_CONSUMED_TABLE = "mailbox_consumed";
15
19
  /**
16
20
  * 身份键(`scope` / `handle` / lease `owner`)的 UTF-8 字节上界 = 键列宽(TiDB `VARBINARY(190)`;PG 列按字符,本判官在两方言上都按字节拒)。
17
21
  */
@@ -35,10 +39,10 @@ export type MailboxKeyColumn = "scope" | "handle" | "owner";
35
39
  /** 库里读回的一对键列判官拒不拒(与入口同一只判官;字节先无损读成串,读不成 = 不良构)。`reap` 的候选句柄与一次性迁移 M27 都按它判。 */
36
40
  export declare function storedMailboxKeyless(scopeKey: unknown, handle: unknown): boolean;
37
41
  /**
38
- * 收件箱表族:三张表的键都是 `(scope_key, handle)`。次序 = 锁序(盒行 → 消息行 → 墓碑行;盒行先删的理由见 `drop`)。
42
+ * 收件箱表族:四张表的键都是 `(scope_key, handle)`。次序 = 锁序(盒行 → 消息行 → 已收窗行 → 墓碑行;盒行先删的理由见 `drop`)。
39
43
  * 「丢掉一只箱」= 按这个次序逐表按键删 —— `drop` 与一次性迁移 M27 是同一组语句({@link mailboxKeyDeleteSql})。
40
44
  */
41
- export declare const MAILBOX_KEYED_TABLES: readonly ["mailbox", "mailbox_message", "mailbox_tombstone"];
45
+ export declare const MAILBOX_KEYED_TABLES: readonly ["mailbox", "mailbox_message", "mailbox_consumed", "mailbox_tombstone"];
42
46
  export type MailboxKeyedTable = (typeof MAILBOX_KEYED_TABLES)[number];
43
47
  /** 按键删收件箱表族的一张表(两方言;参数 = [scope_key, handle])。 */
44
48
  export declare function mailboxKeyDeleteSql(dialect: SqlDialect, table: MailboxKeyedTable): string;
@@ -55,7 +59,7 @@ export declare function ensureTiDBMailboxSchema(pool: MySqlPool): Promise<void>;
55
59
  /** [ref]:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgMailboxSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
56
60
  export declare const MAILBOX_INDEXES: readonly IndexSpec[];
57
61
  export declare function ensurePgMailboxSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
58
- export declare class TiDBMailboxStore implements MailboxStore, MailboxRecipientLifecycle {
62
+ export declare class TiDBMailboxStore implements MailboxStore {
59
63
  private readonly pool;
60
64
  private readonly logger?;
61
65
  /** [ref]([ref] §1.2⑥):跨进程安全**显式**声明 —— core 的 `mailboxCrossProcessMountVerdict` 只对字面 `true` 放行
@@ -82,15 +86,15 @@ export declare class TiDBMailboxStore implements MailboxStore, MailboxRecipientL
82
86
  reap(scope: string, now: number, opts?: {
83
87
  maxAgeMs?: number;
84
88
  }): Promise<number>;
85
- /** 见 {@link MailboxRecipientLifecycle.retireRecipient}。**只打标**:不删消息、不动租约、不碰 next_seq。
89
+ /** 见 core `MailboxStore.retireRecipient`。**只打标**:不删消息、不动租约、不碰 next_seq。
86
90
  * 锁序 = 盒行先(与 append / claim / ack / drop / reap 五面统一 —— ABBA 死锁形不许在这里重开),
87
91
  * 于是 retire 与并发 append 在同一把行锁上串行:要么 append 先拿到 seq、要么它读到墓碑被拒,没有中间态。 */
88
92
  retireRecipient(scope: string, handle: string, at: number, label?: string): Promise<boolean>;
89
- /** 见 {@link MailboxRecipientLifecycle.listRetiredRecipients}。前缀查带 `ESCAPE`(句柄是开集串,
93
+ /** 见 core `MailboxStore.listRetiredRecipients`。前缀查带 `ESCAPE`(句柄是开集串,
90
94
  * `_`/`%` 若被当通配会把**别的**收件人一起列出来 —— 与本仓 `?q=` 面同一条 escapeLike 纪律)。 */
91
95
  listRetiredRecipients(scope: string, handlePrefix: string): Promise<readonly MailboxRecipientTombstone[]>;
92
96
  }
93
- export declare class PgMailboxStore implements MailboxStore, MailboxRecipientLifecycle {
97
+ export declare class PgMailboxStore implements MailboxStore {
94
98
  private readonly pool;
95
99
  private readonly logger?;
96
100
  /** [ref] 跨进程安全显式声明(TiDB 孪生同注:事务 + 盒行 FOR UPDATE 锁序是真值前提;终验 = test/mailbox-cross-process-kit.test.ts pg 腿)。 */