@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
@@ -1,13 +1,25 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync, readdirSync, rmSync } from "node:fs";
3
- import { AppendLog, ensureDir, readJsonlRecords, sanitizePathComponent } from "@sema-agent/core";
3
+ import { AppendLog, ensureDir, sanitizePathComponent } from "@sema-agent/core";
4
+ import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
4
5
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
6
+ const ANCHOR_RECORD_SHAPE = {
7
+ what: "resume-anchor record without a string eventId / entryId",
8
+ read(record) {
9
+ const { eventId, entryId, owner } = record;
10
+ if (typeof eventId !== "string" || typeof entryId !== "string")
11
+ return undefined;
12
+ return { eventId, entryId, owner: typeof owner === "string" ? owner : null, ownerReadable: owner === undefined || owner === null || typeof owner === "string" };
13
+ },
14
+ };
5
15
  export class FileResumeAnchorStore {
6
16
  dir;
7
17
  bySession = new Map();
8
18
  logs = new Map();
9
- constructor(root) {
19
+ warn;
20
+ constructor(root, opts) {
10
21
  this.dir = join(root, "resume-anchors");
22
+ this.warn = opts?.warn;
11
23
  ensureDir(this.dir);
12
24
  this.hydrate();
13
25
  }
@@ -26,14 +38,16 @@ export class FileResumeAnchorStore {
26
38
  if (!name.endsWith(".jsonl"))
27
39
  continue;
28
40
  const key = name.slice(0, -".jsonl".length);
29
- const records = readJsonlRecords(join(this.dir, name));
41
+ const { records } = replayJsonlSkipping(join(this.dir, name), "resume-anchor", ANCHOR_RECORD_SHAPE, this.warn);
30
42
  if (!records.length)
31
43
  continue;
32
44
  const m = this.bySession.get(key) ?? new Map();
33
45
  for (const r of records) {
34
- if (!r || typeof r.eventId !== "string" || typeof r.entryId !== "string")
46
+ if (!r.ownerReadable) {
47
+ m.delete(r.eventId);
35
48
  continue;
36
- m.set(r.eventId, { entryId: r.entryId, owner: r.owner ?? null });
49
+ }
50
+ m.set(r.eventId, { entryId: r.entryId, owner: r.owner });
37
51
  }
38
52
  this.bySession.set(key, m);
39
53
  }
@@ -1,5 +1,6 @@
1
1
  import { type UsageRow } from "../usage-analytics.js";
2
2
  import type { TaskStatus } from "@sema-agent/core";
3
+ import type { Logger } from "../observability/logger.js";
3
4
  import { StoreBootRefusalError, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
4
5
  import type { RunStoreCheckpointProbe } from "./memory-run-store.js";
5
6
  import type { LedgerEventType } from "../trace/ledger-events.js";
@@ -45,9 +46,12 @@ export declare class FileRunStore {
45
46
  /** [ref] 件B(census 第 29 行终局):`RUN_STORE_STRICT_HYDRATE` 的**部署级**表态。
46
47
  * `true` ⇒ hydrate 的 readdir 失败拒启;缺席/`false` ⇒ 件6 的记债腿,行为字节不变。 */
47
48
  private readonly strictHydrate;
49
+ /** S-582:三个盘面坏行的腐读披露座(`LocalBackend` 接 `discloseWarn`;缺席 ⇒ 重放属主退回 `console.warn`)。 */
50
+ private readonly corruptReadWarn;
48
51
  constructor(root: string, opts?: {
49
52
  error?(msg: string, fields?: Record<string, unknown>): void;
50
53
  strictHydrate?: boolean;
54
+ warn?: Logger["warn"];
51
55
  });
52
56
  private runDir;
53
57
  private runJsonPath;
@@ -58,8 +62,9 @@ export declare class FileRunStore {
58
62
  private toRecord;
59
63
  /** Replay the on-disk registry/claims/event-logs into the in-memory index on boot. Each plane is torn-tail-/
60
64
  * partial-write-safe (run.json via atomicWriteFile can never be half-written; the active file via writeThenLink is
61
- * all-or-nothing; events.jsonl via readJsonlRecords drops a crash tail). A corrupt run.json (failed parse) is
62
- * skipped — that run is lost, not a poison that fails the boot. */
65
+ * all-or-nothing; events.jsonl via the JSONL replay drops a crash tail). A corrupt run.json (failed parse) is
66
+ * skipped — that run is lost, not a poison that fails the boot — and since S-582 the skip is disclosed and counted
67
+ * (`server.run-store.corrupt-record-skipped`), never silent. */
63
68
  /**
64
69
  * hydrate 期**逐文件**读盘的统一口([ref] 件B / codex 对抗复审 R4-[high])。
65
70
  *
@@ -73,6 +78,12 @@ export declare class FileRunStore {
73
78
  * 吞、也不改形),既有的「坏行跳过、不毒 boot」宽容契约与既有的抛法一起原封保留。
74
79
  */
75
80
  private replay;
81
+ /**
82
+ * S-582 —— 本店**唯一**的盘上记录读口(hydrate 经 {@link replay} 包一层 strict 档,运行期读点直调它):
83
+ * 交形判据给重放属主,坏行按登记名 `run-store` 跳过 + 披露 + 计数。strict 档的射程是「文件读不出」,
84
+ * 不含「文件里有坏行」—— 后者在两档下都走这里的 F 臂(普查表第 96 行)。
85
+ */
86
+ private readRecords;
76
87
  /**
77
88
  * strict 档下的**存在性判别**(codex R4-[high] 的前半):`existsSync` 对 ENOENT 与 EACCES 同答 false,
78
89
  * 而这两句话在本门下必须分开 —— 前者是常态(`runs/` 下的非目录杂物、还没落盘的目录),后者是「我读
@@ -110,10 +121,12 @@ export declare class FileRunStore {
110
121
  /** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
111
122
  * [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
112
123
  setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
113
- /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard). */
114
- setSuspended(taskId: string): Promise<void>;
115
- /** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review. */
116
- setNeedsReview(taskId: string): Promise<void>;
124
+ /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
125
+ * S-173:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。 */
126
+ setSuspended(taskId: string, result: PersistedTaskResult | null): Promise<void>;
127
+ /** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review.
128
+ * S-173:同 {@link setSuspended} 落封口脱敏后的 `result`。 */
129
+ setNeedsReview(taskId: string, result: PersistedTaskResult | null): Promise<void>;
117
130
  /** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags.
118
131
  * [ref](codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改。 */
119
132
  markResuming(taskId: string, instanceId: string): Promise<boolean>;
@@ -192,7 +205,7 @@ export declare class FileRunStore {
192
205
  /** [ref] wire the checkpoint probe (LocalBackend.checkpoint(), where checkpoint + run store share one root). */
193
206
  setCheckpointProbe(probe: RunStoreCheckpointProbe): void;
194
207
  /** [ref] shared tail of both suspended-run reapers: flip one parked row → failed('approval.expired') and
195
- * PERSIST it — the file twin of the SQL UPDATE (`error`/`error_code` set directly, no result blob). */
208
+ * PERSIST it — the file twin of the SQL UPDATE (`error`/`error_code` set directly, no result blob; S-173: the park-time `result` is cleared, same as the SQL `result = NULL`). */
196
209
  private failExpiredPark;
197
210
  /**
198
211
  * [ref] Durable F4 expiry — the file twin of TiDBRunStore.reapSuspended, driven by the injected checkpoint
@@ -294,7 +307,7 @@ export declare class FileRunStore {
294
307
  /** Drop a run entirely (registry + event log + open fd + on-disk dir) — used by deleteBySession. */
295
308
  private dropRun;
296
309
  /**
297
- * 🔴 **一次性迁移(随 7.75.0 连同 `one-time-migrations.ts` 整删)** —— 退役词旧拼法的账本改写,
310
+ * 🔴 **一次性迁移 M3 的 local 腿(窗口见 `one-time-migrations.ts` 登记表,随 M3 一起删)** —— 退役词旧拼法的账本改写,
298
311
  * SQL 两孪生那条整表 UPDATE 的 local 形。逐条走**同一只**纯改写函数,所以三条车道改出来的字节一致。
299
312
  *
300
313
  * 为什么是 store 上的一只动词、而不是外面一个文件走查:本店的账本有**两份**在场 —— 盘上的 jsonl
@@ -1,21 +1,45 @@
1
- import { redactErrorMessage, redactLedgerEventData, redactTerminalResult } from "../observability/run-terminal.js";
1
+ import { redactErrorMessage, redactLedgerEventData, sealRunRowResult } from "../observability/run-terminal.js";
2
2
  import { outwardPlane } from "../terminal.js";
3
3
  import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
4
4
  import { join } from "node:path";
5
5
  import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
6
6
  import { randomBytes } from "node:crypto";
7
- import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, readJsonlRecords, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
7
+ import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
+ import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
9
10
  import { StoreBootRefusalError } from "./store-contracts.js";
10
11
  import { notifyRunTerminal } from "../observability/run-terminal.js";
11
12
  import { migrateRetiredAskOrigin } from "./one-time-migrations.js";
12
13
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
14
+ const isParseableDate = (v) => typeof v === "string" && !Number.isNaN(Date.parse(v));
15
+ const RUN_ROW_SHAPE = {
16
+ what: "run.json record without a string taskId and parseable createdAt / updatedAt",
17
+ read(record) {
18
+ const r = record;
19
+ return typeof r.taskId === "string" && isParseableDate(r.createdAt) && isParseableDate(r.updatedAt) ? record : undefined;
20
+ },
21
+ };
22
+ const EVENT_RECORD_SHAPE = {
23
+ what: "run event record without a numeric seq",
24
+ read(record) {
25
+ return typeof record.seq === "number" ? record : undefined;
26
+ },
27
+ };
28
+ const CLAIM_RECORD_SHAPE = {
29
+ what: "active-run claim record without a string sessionId / taskId",
30
+ read(record) {
31
+ const r = record;
32
+ return typeof r.sessionId === "string" && typeof r.taskId === "string" ? record : undefined;
33
+ },
34
+ };
13
35
  const CLAIM_TAKEOVER_ATTEMPTS = 3;
14
36
  const SAFE_PATH_COMPONENT = /^[A-Za-z0-9_.-]+$/;
15
37
  const isSafePathComponent = (s) => s !== "" && s !== "." && s !== ".." && SAFE_PATH_COMPONENT.test(s);
16
- function readClaimRecord(recs, sessionId) {
17
- const c = recs[recs.length - 1];
18
- if (!c || typeof c.sessionId !== "string" || typeof c.taskId !== "string" || c.sessionId !== sessionId)
38
+ function singleRecordOf(read) {
39
+ return read.skipped > 0 ? undefined : read.records[read.records.length - 1];
40
+ }
41
+ function readClaimRecord(c, sessionId) {
42
+ if (!c || c.sessionId !== sessionId)
19
43
  return undefined;
20
44
  if (!isSafePathComponent(c.taskId))
21
45
  return undefined;
@@ -38,9 +62,11 @@ export class FileRunStore {
38
62
  claimLocks = new Map();
39
63
  logger;
40
64
  strictHydrate;
65
+ corruptReadWarn;
41
66
  constructor(root, opts) {
42
67
  this.logger = opts;
43
68
  this.strictHydrate = opts?.strictHydrate === true;
69
+ this.corruptReadWarn = opts?.warn;
44
70
  this.runsDir = join(root, "runs");
45
71
  this.activeDir = join(root, "runs", "active");
46
72
  this.tmpDir = join(root, "tmp");
@@ -96,9 +122,9 @@ export class FileRunStore {
96
122
  updatedAt: r.updatedAt.toISOString(),
97
123
  };
98
124
  }
99
- replay(path) {
125
+ replay(path, shape) {
100
126
  try {
101
- return readJsonlRecords(path);
127
+ return this.readRecords(path, shape);
102
128
  }
103
129
  catch (err) {
104
130
  if (!this.strictHydrate)
@@ -110,6 +136,9 @@ export class FileRunStore {
110
136
  "Fix the permissions/mount on that path, point LOCAL_DATA_ROOT at a readable dir, or unset RUN_STORE_STRICT_HYDRATE to boot anyway (the pre-existing behavior).");
111
137
  }
112
138
  }
139
+ readRecords(path, shape) {
140
+ return replayJsonlSkipping(path, "run-store", shape, this.corruptReadWarn);
141
+ }
113
142
  refuseIfUnreadable(jsonPath, dir) {
114
143
  try {
115
144
  statSync(jsonPath);
@@ -159,21 +188,17 @@ export class FileRunStore {
159
188
  this.refuseIfUnreadable(jsonPath, dir);
160
189
  continue;
161
190
  }
162
- const rows = this.replay(jsonPath);
163
- const disk = rows[rows.length - 1];
164
- if (!disk || typeof disk.taskId !== "string")
165
- continue;
166
- if (typeof disk.createdAt !== "string" || Number.isNaN(Date.parse(disk.createdAt)) ||
167
- typeof disk.updatedAt !== "string" || Number.isNaN(Date.parse(disk.updatedAt)))
191
+ const disk = singleRecordOf(this.replay(jsonPath, RUN_ROW_SHAPE));
192
+ if (!disk)
168
193
  continue;
169
194
  this.runs.set(disk.taskId, this.toRow(disk));
170
195
  const eventsPath = join(dir, "events.jsonl");
171
- const recs = this.replay(eventsPath);
196
+ const recs = this.replay(eventsPath, EVENT_RECORD_SHAPE).records;
172
197
  if (recs.length) {
173
198
  const list = [];
174
199
  const seen = new Set();
175
200
  for (const e of recs) {
176
- if (!e || typeof e.seq !== "number" || seen.has(e.seq))
201
+ if (seen.has(e.seq))
177
202
  continue;
178
203
  seen.add(e.seq);
179
204
  list.push({ seq: e.seq, type: String(e.type), data: e.data ?? null, ts: String(e.ts) });
@@ -215,9 +240,8 @@ export class FileRunStore {
215
240
  }
216
241
  for (const sanitized of claims) {
217
242
  const path = join(this.activeDir, sanitized);
218
- const recs = this.replay(path);
219
- const claim = recs[recs.length - 1];
220
- if (claim && typeof claim.sessionId === "string" && typeof claim.taskId === "string" && isSafePathComponent(claim.taskId)) {
243
+ const claim = singleRecordOf(this.replay(path, CLAIM_RECORD_SHAPE));
244
+ if (claim && isSafePathComponent(claim.taskId)) {
221
245
  this.active.set(claim.sessionId, claim.taskId);
222
246
  continue;
223
247
  }
@@ -355,7 +379,7 @@ export class FileRunStore {
355
379
  .map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
356
380
  }
357
381
  async setTerminal(taskId, status, result, error) {
358
- result = redactTerminalResult(result);
382
+ result = sealRunRowResult(result);
359
383
  error = error === null ? null : (redactErrorMessage(error) ?? null);
360
384
  const r = this.runs.get(taskId);
361
385
  if (!r) {
@@ -391,18 +415,22 @@ export class FileRunStore {
391
415
  if (pending)
392
416
  notifyRunTerminal(pending);
393
417
  }
394
- async setSuspended(taskId) {
418
+ async setSuspended(taskId, result) {
419
+ const sealed = sealRunRowResult(result);
395
420
  const r = this.runs.get(taskId);
396
421
  if (r && (r.status === "running" || r.status === "suspended")) {
397
422
  r.status = "suspended";
423
+ r.result = sealed;
398
424
  r.updatedAt = new Date();
399
425
  this.persist(r);
400
426
  }
401
427
  }
402
- async setNeedsReview(taskId) {
428
+ async setNeedsReview(taskId, result) {
429
+ const sealed = sealRunRowResult(result);
403
430
  const r = this.runs.get(taskId);
404
431
  if (r && (r.status === "running" || r.status === "needs_review")) {
405
432
  r.status = "needs_review";
433
+ r.result = sealed;
406
434
  r.updatedAt = new Date();
407
435
  this.persist(r);
408
436
  }
@@ -412,6 +440,7 @@ export class FileRunStore {
412
440
  if (!r || (r.status !== "suspended" && r.status !== "needs_review"))
413
441
  return false;
414
442
  r.status = "running";
443
+ r.result = null;
415
444
  r.instanceId = instanceId;
416
445
  r.cancelRequested = false;
417
446
  r.preemptRequested = false;
@@ -551,6 +580,7 @@ export class FileRunStore {
551
580
  for (const r of this.runs.values()) {
552
581
  if (r.status === "running" && pred(r)) {
553
582
  r.status = "failed";
583
+ r.result = null;
554
584
  r.error = reason;
555
585
  r.updatedAt = new Date();
556
586
  this.persist(r);
@@ -572,6 +602,7 @@ export class FileRunStore {
572
602
  }
573
603
  for (const r of orphans) {
574
604
  r.status = "failed";
605
+ r.result = null;
575
606
  r.error = "run orphaned by engine restart";
576
607
  r.updatedAt = new Date();
577
608
  this.persist(r);
@@ -584,6 +615,7 @@ export class FileRunStore {
584
615
  }
585
616
  failExpiredPark(r) {
586
617
  r.status = "failed";
618
+ r.result = null;
587
619
  r.error = "approval expired before decision";
588
620
  r.errorCode = "approval.expired";
589
621
  r.updatedAt = new Date();
@@ -642,14 +674,8 @@ export class FileRunStore {
642
674
  const path = this.runJsonPath(taskId);
643
675
  if (!existsSync(path))
644
676
  return undefined;
645
- const rows = readJsonlRecords(path);
646
- const disk = rows[rows.length - 1];
647
- if (!disk || typeof disk.taskId !== "string")
648
- return undefined;
649
- if (typeof disk.createdAt !== "string" || Number.isNaN(Date.parse(disk.createdAt)) ||
650
- typeof disk.updatedAt !== "string" || Number.isNaN(Date.parse(disk.updatedAt)))
651
- return undefined;
652
- return this.toRow(disk);
677
+ const disk = singleRecordOf(this.readRecords(path, RUN_ROW_SHAPE));
678
+ return disk === undefined ? undefined : this.toRow(disk);
653
679
  }
654
680
  newestNonTerminalRowOnDisk(sessionId) {
655
681
  const names = readdirSync(this.runsDir);
@@ -660,12 +686,8 @@ export class FileRunStore {
660
686
  const path = join(this.runsDir, name, "run.json");
661
687
  if (!existsSync(path))
662
688
  continue;
663
- const rows = readJsonlRecords(path);
664
- const disk = rows[rows.length - 1];
665
- if (!disk || typeof disk.taskId !== "string" || disk.sessionId !== sessionId)
666
- continue;
667
- if (typeof disk.createdAt !== "string" || Number.isNaN(Date.parse(disk.createdAt)) ||
668
- typeof disk.updatedAt !== "string" || Number.isNaN(Date.parse(disk.updatedAt)))
689
+ const disk = singleRecordOf(this.readRecords(path, RUN_ROW_SHAPE));
690
+ if (!disk || disk.sessionId !== sessionId)
669
691
  continue;
670
692
  if (disk.status !== "running" && disk.status !== "suspended" && disk.status !== "needs_review")
671
693
  continue;
@@ -705,7 +727,7 @@ export class FileRunStore {
705
727
  catch (e) {
706
728
  if (e.code !== "EEXIST")
707
729
  throw e;
708
- const claim = readClaimRecord(readJsonlRecords(this.activePath(sessionId)), sessionId);
730
+ const claim = readClaimRecord(singleRecordOf(this.readRecords(this.activePath(sessionId), CLAIM_RECORD_SHAPE)), sessionId);
709
731
  const holder = claim?.taskId ?? this.active.get(sessionId) ?? "";
710
732
  const verdict = await this.judgeClaim(sessionId, claim, holder);
711
733
  if (verdict.kind === "live")
@@ -721,7 +743,7 @@ export class FileRunStore {
721
743
  if (e.code !== "EEXIST")
722
744
  throw e;
723
745
  }
724
- const finalClaim = readClaimRecord(readJsonlRecords(this.activePath(sessionId)), sessionId);
746
+ const finalClaim = readClaimRecord(singleRecordOf(this.readRecords(this.activePath(sessionId), CLAIM_RECORD_SHAPE)), sessionId);
725
747
  return { ok: false, activeTaskId: finalClaim?.taskId ?? this.active.get(sessionId) ?? "" };
726
748
  }
727
749
  async judgeClaim(sessionId, claim, holderTaskId) {
@@ -789,7 +811,7 @@ export class FileRunStore {
789
811
  return { kind: "stale", reason: `run orphaned by engine restart (claim held by ${who}, which is gone)`, snapshot };
790
812
  }
791
813
  takeOverClaim(sessionId, reason, snapshot) {
792
- const nowClaim = readClaimRecord(readJsonlRecords(this.activePath(sessionId)), sessionId);
814
+ const nowClaim = readClaimRecord(singleRecordOf(this.readRecords(this.activePath(sessionId), CLAIM_RECORD_SHAPE)), sessionId);
793
815
  if (snapshot.torn) {
794
816
  if (nowClaim !== undefined)
795
817
  return false;
@@ -122,8 +122,6 @@ export declare class SqlImageBake {
122
122
  private lineOrdExists;
123
123
  /** Highest event seq for a bake (0 if none) — the ++seq allocator + the resume cursor head. */
124
124
  maxSeq(bakeId: string): Promise<number>;
125
- /** Earliest retained event seq (0 if none) → the SSE 416 boundary (a resume below this was evicted, P2.9). */
126
- retainedFrom(bakeId: string): Promise<number>;
127
125
  /** Events with seq strictly greater than `afterSeq` (0 for the whole stream), ascending. */
128
126
  getEvents(bakeId: string, afterSeq: number): Promise<BakeEvent[]>;
129
127
  /**
@@ -379,11 +379,6 @@ export class SqlImageBake {
379
379
  const m = rows[0]?.m;
380
380
  return m == null ? 0 : Number(m);
381
381
  }
382
- async retainedFrom(bakeId) {
383
- const { rows } = await this.db.query(this.q("SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = ?", "SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = $1"), [bakeId]);
384
- const m = rows[0]?.m;
385
- return m == null ? 0 : Number(m);
386
- }
387
382
  async getEvents(bakeId, afterSeq) {
388
383
  const { rows } = await this.db.query(this.q(`SELECT seq, line_ord, kind, level, ${jsonColumnText("tidb", "data")}, ts FROM image_bake_event WHERE bake_id = ? AND seq > ? ORDER BY seq ASC`, `SELECT seq, line_ord, kind, level, ${jsonColumnText("pg", "data")}, ts FROM image_bake_event WHERE bake_id = $1 AND seq > $2 ORDER BY seq ASC`), [bakeId, afterSeq]);
389
384
  return rows.map((r) => ({
@@ -1,5 +1,6 @@
1
1
  import { type AcquiredSession, type SessionStore, type SessionRepo, type SessionTreeEntry } from "@sema-agent/core";
2
2
  import type { SessionSummary } from "./store-contracts.js";
3
+ import { type SessionFace } from "../session-incarnation.js";
3
4
  import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
4
5
  import { type StagingHandle } from "../session-sync-kernel.js";
5
6
  export declare class LocalSessionStore implements SessionStore {
@@ -59,11 +60,30 @@ export declare class LocalSessionStore implements SessionStore {
59
60
  * run selected, encoded like the SQL twins' `session_meta.instruction_source` column ('' = none; missing key = never
60
61
  * recorded). Its own file for the same reason titles have theirs: one small map per concern, no format coupling. */
61
62
  private readonly instructionSources;
63
+ /** S-790 / S-506([ref] §2):会话化身(`session-incarnations.json`,`{ [sessionId]: token }`)。SQL 孪生的
64
+ * `session_meta.incarnation` 列在本店的对位。🔴 **必须落盘**(不能像 `owners` 那样只活在内存):化身的口径是「一位住客一生
65
+ * 稳定」,而记忆引擎按它绑定会话级记录(opt-out / 污染标记)—— 重启后换一只值 = 同一位住客的 opt-out 不再绑它 = 静默恢复采集。
66
+ * 设计稿原说「存进属主边车」,但本店**没有**落盘的属主边车(`owners` 是纯内存 Map,S3 裁定),所以落在同一族的第四只边档
67
+ * (同一只 `SidecarStringMap`:同写链、失败回滚)。铸点只在新建会话;盘上旧会话没有这一格 ⇒ 第一次读补铸并落盘。 */
68
+ private readonly incarnations;
62
69
  /** @param repo the core SessionRepo this store projects into the §0.5 SessionStore shape.
63
70
  * @param dataDir the SAME data dir the file-backed repo persists under (LocalBackend passes `fileBackend.root`).
64
71
  * When given, lastActivity is persisted to `<dataDir>/session-last-activity.json` so it survives a
65
72
  * restart (the file-backed deployment); omit it for the in-memory deployment (nothing survives). */
66
73
  constructor(repo: SessionRepo, dataDir?: string);
74
+ /**
75
+ * S-790:把 `sessionId` 的化身写成 `next(before)` 的结果并**落盘**(写链里判、写、失败回滚 —— 与 instruction-source 同一只
76
+ * `commit`)。`next` 返回 `undefined` = 不改。落不了盘 ⇒ 抛(化身是正确性事实,不做「内存里先记着」的降级:一只没落盘的化身
77
+ * 重启后会被换掉,那就是另一位住客)。
78
+ */
79
+ private writeIncarnation;
80
+ /** S-790:新建会话 ⇒ 铸一只新的(覆盖同 id 上任何残留 —— 新建行就是新住客)。 */
81
+ private mintIncarnation;
82
+ /** S-790:已存在的会话缺一只化身(盘上 7.104.0 之前的会话)⇒ 补铸;已有的一个字节不动。 */
83
+ private ensureIncarnation;
84
+ /** S-790:属主 + 化身一次读(SQL 孪生同义;`undefined` = 无此会话)。盘上有会话而边档里没有它的化身 ⇒ 补铸并落盘后再答;
85
+ * 读的过程中会话被删 / 换了主 ⇒ `undefined`(与 {@link getHead} 同一条「读间被删」判据)。 */
86
+ sessionFaceOf(sessionId: string): Promise<SessionFace | undefined>;
67
87
  /** Cheap pre-LLM probe (local twin). The map write needs no session_meta row, so there is
68
88
  * no "none" (register-lag) dimension here — two states only. */
69
89
  probeTitle(sessionId: string): Promise<"none" | "untitled" | "titled">;
@@ -222,6 +242,8 @@ export declare class LocalSessionStore implements SessionStore {
222
242
  * importing principal (re-stamped into the service-side owner map; core's local repo has no owner column, §9).
223
243
  * `owner=null` (single-user local) is passed as `undefined` to the core seam (its owner param is `string | undefined`). */
224
244
  importEntries(sessionId: string, owner: string | null, entries: SessionTreeEntry[]): Promise<void>;
245
+ /** importEntries / replaceEntries 的共用写半场:导日志 + 盖属主 + 落化身(新建铸的那只,或换装沿用的那只)。 */
246
+ private importEntriesAs;
225
247
  /** 2c session-sync (§7/§8) — IDEMPOTENT REPLACE. The local twin's replace is DELETE-then-IMPORT: core's
226
248
  * InMemorySessionRepo / FileSessionRepo expose no single replace primitive, but the local backend is single-writer
227
249
  * with NO compaction floor / cross-instance race (a process-local in-memory map / a single-process file repo), so
@@ -3,9 +3,11 @@ import { join, resolve } from "node:path";
3
3
  import { SessionError, validateEntriesForImport } from "@sema-agent/core";
4
4
  import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
5
5
  import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
6
- import { decodeJsonColumn, readStringRecordColumn } from "./sql-json-column.js";
6
+ import { decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
7
7
  import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
8
+ import { carriedIncarnation, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
8
9
  import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
10
+ import { openSessionForItself } from "../session-records-skipped-notice.js";
9
11
  import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, } from "../session-sync-kernel.js";
10
12
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
11
13
  const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
@@ -28,19 +30,21 @@ function userText(entry) {
28
30
  }
29
31
  return null;
30
32
  }
31
- const SIDECAR_DEGRADES = { lastActivity: "recency", title: "auto-title", "instruction-source": "instruction-source baseline" };
33
+ const SIDECAR_DEGRADES = { lastActivity: "recency", title: "auto-title", "instruction-source": "instruction-source baseline", incarnation: "session-incarnation (the write that needed it failed loudly)" };
32
34
  const SIDECAR_MAP_STATE_BY_PATH = new Map();
33
35
  class SidecarStringMap {
34
36
  path;
35
37
  label;
36
38
  onUnreadable;
37
39
  onWriteFailed;
40
+ strict;
38
41
  own = { entries: new Map(), writeChain: Promise.resolve() };
39
- constructor(path, label, onUnreadable, onWriteFailed) {
42
+ constructor(path, label, onUnreadable, onWriteFailed, strict = false) {
40
43
  this.path = path;
41
44
  this.label = label;
42
45
  this.onUnreadable = onUnreadable;
43
46
  this.onWriteFailed = onWriteFailed;
47
+ this.strict = strict;
44
48
  if (path && !SIDECAR_MAP_STATE_BY_PATH.has(path))
45
49
  SIDECAR_MAP_STATE_BY_PATH.set(path, { entries: new Map(), writeChain: Promise.resolve() });
46
50
  }
@@ -62,8 +66,16 @@ class SidecarStringMap {
62
66
  }
63
67
  }
64
68
  catch (e) {
65
- if (e?.code !== "ENOENT")
69
+ if (e?.code === "ENOENT")
70
+ return;
71
+ if (!this.strict) {
66
72
  this.onUnreadable(path, e);
73
+ return;
74
+ }
75
+ const kind = e instanceof SqlJsonColumnError ? e.problem : (e?.code ?? "unreadable");
76
+ throw new Error(`${this.label} is unreadable (${kind}) — refusing to read or mint session incarnations until it is repaired: a replacement ` +
77
+ `token for an existing session would make it a different occupant (its memory opt-out would stop binding it), and a write ` +
78
+ `would overwrite the unreadable bytes. The file is left in place at ${path}.`);
67
79
  }
68
80
  })();
69
81
  }
@@ -112,12 +124,48 @@ export class LocalSessionStore {
112
124
  sidecarPath;
113
125
  titles;
114
126
  instructionSources;
127
+ incarnations;
115
128
  constructor(repo, dataDir) {
116
129
  this.repo = repo;
117
130
  this.sidecarPath = dataDir ? join(dataDir, "session-last-activity.json") : undefined;
118
- const sidecar = (file, which) => new SidecarStringMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which));
131
+ const sidecar = (file, which, strict = false) => new SidecarStringMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which), strict);
119
132
  this.titles = sidecar("session-titles.json", "title");
120
133
  this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source");
134
+ this.incarnations = sidecar("session-incarnations.json", "incarnation", true);
135
+ }
136
+ async writeIncarnation(sessionId, next) {
137
+ await this.incarnations.ensureLoaded();
138
+ const committed = await this.incarnations.commit((entries) => {
139
+ const before = entries.get(sessionId);
140
+ const value = next(before);
141
+ if (value === undefined || value === before)
142
+ return undefined;
143
+ entries.set(sessionId, value);
144
+ return () => {
145
+ if (before === undefined)
146
+ entries.delete(sessionId);
147
+ else
148
+ entries.set(sessionId, before);
149
+ };
150
+ });
151
+ if (!committed)
152
+ throw new SessionError("storage", `LocalSessionStore: session-incarnation sidecar write failed for session ${sessionId} — nothing was recorded`);
153
+ }
154
+ mintIncarnation(sessionId) {
155
+ return this.writeIncarnation(sessionId, () => mintSessionIncarnation());
156
+ }
157
+ ensureIncarnation(sessionId) {
158
+ return this.writeIncarnation(sessionId, (before) => (before === undefined ? mintSessionIncarnation() : undefined));
159
+ }
160
+ async sessionFaceOf(sessionId) {
161
+ const owner = await this.ownerOf(sessionId);
162
+ if (owner === undefined)
163
+ return undefined;
164
+ await this.ensureIncarnation(sessionId);
165
+ const incarnation = this.incarnations.state.entries.get(sessionId);
166
+ if (!this.owners.has(sessionId) || (this.owners.get(sessionId) ?? null) !== owner)
167
+ return undefined;
168
+ return sessionFaceFromRow(sessionId, owner, incarnation);
121
169
  }
122
170
  async probeTitle(sessionId) {
123
171
  await this.titles.ensureLoaded();
@@ -223,9 +271,11 @@ export class LocalSessionStore {
223
271
  }
224
272
  async acquire(sessionId, opts) {
225
273
  if (!sessionId) {
274
+ await this.incarnations.ensureLoaded();
226
275
  const session = await this.repo.create({});
227
276
  const id = (await session.getMetadata()).id;
228
277
  this.owners.set(id, null);
278
+ await this.mintIncarnation(id);
229
279
  return { session: session, sessionId: id };
230
280
  }
231
281
  if (opts?.requireExisting)
@@ -239,7 +289,7 @@ export class LocalSessionStore {
239
289
  }
240
290
  async load(sessionId, requireExisting = false) {
241
291
  try {
242
- const session = await this.repo.open({ id: sessionId, createdAt: "" });
292
+ const session = await openSessionForItself(sessionId, () => this.repo.open({ id: sessionId, createdAt: "" }));
243
293
  if (!this.owners.has(sessionId))
244
294
  this.owners.set(sessionId, null);
245
295
  return { session: session, sessionId };
@@ -249,7 +299,9 @@ export class LocalSessionStore {
249
299
  throw e;
250
300
  if (requireExisting)
251
301
  throw e;
302
+ await this.incarnations.ensureLoaded();
252
303
  const session = await this.repo.create({ id: sessionId });
304
+ await this.mintIncarnation(sessionId);
253
305
  if (!this.owners.has(sessionId))
254
306
  this.owners.set(sessionId, null);
255
307
  return { session: session, sessionId };
@@ -258,15 +310,17 @@ export class LocalSessionStore {
258
310
  async register(sessionId, owner) {
259
311
  if (this.owners.has(sessionId))
260
312
  return;
313
+ await this.incarnations.ensureLoaded();
261
314
  this.owners.set(sessionId, owner);
262
315
  try {
263
316
  await this.repo.open({ id: sessionId, createdAt: "" });
317
+ await this.ensureIncarnation(sessionId);
264
318
  }
265
319
  catch (e) {
266
- if (isNotFound(e))
267
- await this.repo.create({ id: sessionId });
268
- else
320
+ if (!isNotFound(e))
269
321
  throw e;
322
+ await this.repo.create({ id: sessionId });
323
+ await this.mintIncarnation(sessionId);
270
324
  }
271
325
  }
272
326
  async ownerOf(sessionId) {
@@ -369,10 +423,12 @@ export class LocalSessionStore {
369
423
  }
370
424
  }
371
425
  async fork(sourceId, owner) {
426
+ await this.incarnations.ensureLoaded();
372
427
  try {
373
428
  const session = await this.repo.fork({ id: sourceId, createdAt: "" }, {});
374
429
  const id = (await session.getMetadata()).id;
375
430
  this.owners.set(id, owner);
431
+ await this.mintIncarnation(id);
376
432
  return id;
377
433
  }
378
434
  catch (e) {
@@ -410,10 +466,15 @@ export class LocalSessionStore {
410
466
  return gen();
411
467
  }
412
468
  async importEntries(sessionId, owner, entries) {
469
+ await this.importEntriesAs(sessionId, owner, entries, mintSessionIncarnation());
470
+ }
471
+ async importEntriesAs(sessionId, owner, entries, incarnation) {
413
472
  if (!this.repo.importEntries)
414
473
  throw new SessionError("storage", "session import is not supported by this local backend");
474
+ await this.incarnations.ensureLoaded();
415
475
  await this.repo.importEntries(sessionId, owner ?? undefined, entries);
416
476
  this.owners.set(sessionId, owner);
477
+ await this.writeIncarnation(sessionId, () => incarnation);
417
478
  }
418
479
  async replaceEntries(sessionId, owner, entries) {
419
480
  if (!this.repo.importEntries)
@@ -421,8 +482,10 @@ export class LocalSessionStore {
421
482
  if (!this.repo.delete)
422
483
  throw new SessionError("storage", "session replace is not supported by this local backend (no delete seam)");
423
484
  validateEntriesForImport(entries);
485
+ await this.incarnations.ensureLoaded();
486
+ const prior = this.owners.has(sessionId) ? { owner: this.owners.get(sessionId) ?? null, incarnation: this.incarnations.state.entries.get(sessionId) } : undefined;
424
487
  await this.deleteSession(sessionId, owner);
425
- await this.importEntries(sessionId, owner, entries);
488
+ await this.importEntriesAs(sessionId, owner, entries, carriedIncarnation(prior, owner));
426
489
  }
427
490
  beginImportStaging(realSessionId, token) {
428
491
  const h = new LocalStagingHandle(this, realSessionId, stagingIdFor(realSessionId, token));
@@ -449,7 +512,7 @@ export class LocalSessionStore {
449
512
  this.owners.delete(sessionId);
450
513
  this.lastActivity.delete(sessionId);
451
514
  this.lastTaskRun.delete(sessionId);
452
- for (const map of [this.titles, this.instructionSources]) {
515
+ for (const map of [this.titles, this.instructionSources, this.incarnations]) {
453
516
  await map.ensureLoaded();
454
517
  if (map.state.entries.delete(sessionId))
455
518
  await map.persist();