@sema-agent/server 7.103.2 → 7.104.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 (175) hide show
  1. package/MIGRATION.md +346 -2
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +271 -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 +4 -0
  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 +13 -28
  58. package/dist/config.js +53 -70
  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/http/admission.d.ts +11 -4
  66. package/dist/http/admission.js +5 -1
  67. package/dist/http/dispatch.js +3 -3
  68. package/dist/http/principal-gate.d.ts +22 -0
  69. package/dist/http/principal-gate.js +20 -0
  70. package/dist/http/resume-legs.js +9 -5
  71. package/dist/http/routes/agents-roster.d.ts +7 -0
  72. package/dist/http/routes/agents-roster.js +6 -2
  73. package/dist/http/routes/approvals-assistant.d.ts +48 -19
  74. package/dist/http/routes/approvals-assistant.js +117 -50
  75. package/dist/http/routes/capabilities.js +3 -2
  76. package/dist/http/routes/config-catalog.js +1 -0
  77. package/dist/http/routes/memory-consolidation.d.ts +3 -2
  78. package/dist/http/routes/memory-policy.js +3 -3
  79. package/dist/http/routes/rules.js +2 -2
  80. package/dist/http/routes/runs.js +27 -2
  81. package/dist/http/routes/session-sync.js +8 -14
  82. package/dist/http/routes/sessions-list.js +3 -2
  83. package/dist/http/routes/sessions.js +44 -23
  84. package/dist/http/routes/shared-memory.js +2 -2
  85. package/dist/http/routes/tasks.js +22 -19
  86. package/dist/http/routes/trace-usage.js +4 -4
  87. package/dist/http/send.d.ts +12 -0
  88. package/dist/http/send.js +12 -1
  89. package/dist/http/server.d.ts +21 -4
  90. package/dist/http/server.js +7 -4
  91. package/dist/http/sync-submit-body.d.ts +43 -0
  92. package/dist/http/sync-submit-body.js +4 -0
  93. package/dist/http/wire-types.d.ts +23 -5
  94. package/dist/index.d.ts +1 -0
  95. package/dist/index.js +1 -0
  96. package/dist/leader/merge.d.ts +8 -3
  97. package/dist/memory-incident-sink.js +2 -1
  98. package/dist/model-select.d.ts +9 -0
  99. package/dist/model-select.js +5 -1
  100. package/dist/observability/corrupt-read-seat.d.ts +151 -16
  101. package/dist/observability/corrupt-read-seat.js +158 -15
  102. package/dist/observability/fail-open.d.ts +12 -0
  103. package/dist/observability/fail-open.js +12 -0
  104. package/dist/observability/run-terminal.d.ts +15 -1
  105. package/dist/observability/run-terminal.js +4 -0
  106. package/dist/parked-decide.d.ts +8 -3
  107. package/dist/parked-decide.js +56 -20
  108. package/dist/parked-revive-surface.d.ts +14 -7
  109. package/dist/parked-revive-surface.js +5 -1
  110. package/dist/plugins/approval-ask-store-file.d.ts +4 -1
  111. package/dist/plugins/approval-ask-store-file.js +9 -12
  112. package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
  113. package/dist/plugins/approval-ask-store-sql.js +4 -1
  114. package/dist/plugins/approval-exemption-store.d.ts +21 -1
  115. package/dist/plugins/approval-exemption-store.js +36 -7
  116. package/dist/plugins/approval-nonce-store.d.ts +16 -2
  117. package/dist/plugins/approval-nonce-store.js +17 -7
  118. package/dist/plugins/caching-session-store.d.ts +5 -0
  119. package/dist/plugins/caching-session-store.js +3 -0
  120. package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
  121. package/dist/plugins/file-resume-anchor-store.js +19 -5
  122. package/dist/plugins/file-run-store.d.ts +21 -8
  123. package/dist/plugins/file-run-store.js +60 -38
  124. package/dist/plugins/local-session-store.d.ts +22 -0
  125. package/dist/plugins/local-session-store.js +74 -11
  126. package/dist/plugins/memory-run-store.d.ts +7 -5
  127. package/dist/plugins/memory-run-store.js +11 -4
  128. package/dist/plugins/one-time-migrations.d.ts +38 -1
  129. package/dist/plugins/one-time-migrations.js +143 -15
  130. package/dist/plugins/permission-rule-store-file.d.ts +9 -2
  131. package/dist/plugins/permission-rule-store-file.js +37 -51
  132. package/dist/plugins/pg-session-storage.d.ts +3 -0
  133. package/dist/plugins/pg-session-storage.js +19 -10
  134. package/dist/plugins/run-store-sql.d.ts +12 -4
  135. package/dist/plugins/run-store-sql.js +15 -13
  136. package/dist/plugins/send-file-ledger.d.ts +5 -1
  137. package/dist/plugins/send-file-ledger.js +11 -6
  138. package/dist/plugins/session-event-payload.d.ts +14 -4
  139. package/dist/plugins/session-event-payload.js +5 -2
  140. package/dist/plugins/store-backend.d.ts +7 -3
  141. package/dist/plugins/store-backend.js +10 -6
  142. package/dist/plugins/store-contracts.d.ts +7 -2
  143. package/dist/plugins/tidb-pool.js +8 -2
  144. package/dist/plugins/tidb-session-store.d.ts +6 -1
  145. package/dist/plugins/tidb-session-store.js +17 -10
  146. package/dist/project-identity.d.ts +4 -6
  147. package/dist/request-key-closure.js +1 -1
  148. package/dist/request-value-closure.d.ts +72 -3
  149. package/dist/request-value-closure.js +12 -4
  150. package/dist/roster-bound-http-status.d.ts +6 -0
  151. package/dist/roster-bound-http-status.js +6 -0
  152. package/dist/run-local.js +22 -27
  153. package/dist/runs.d.ts +22 -2
  154. package/dist/runs.js +29 -7
  155. package/dist/sealed-key.d.ts +31 -4
  156. package/dist/sealed-key.js +9 -0
  157. package/dist/security.d.ts +5 -1
  158. package/dist/session-children-retire.d.ts +92 -0
  159. package/dist/session-children-retire.js +85 -0
  160. package/dist/session-error-wire.d.ts +58 -0
  161. package/dist/session-error-wire.js +38 -0
  162. package/dist/session-incarnation.d.ts +61 -0
  163. package/dist/session-incarnation.js +43 -0
  164. package/dist/session-records-skipped-notice.d.ts +20 -0
  165. package/dist/session-records-skipped-notice.js +27 -0
  166. package/dist/tool-approval.d.ts +65 -0
  167. package/dist/tool-approval.js +36 -9
  168. package/dist/trace/core-keyset-guard.d.ts +8 -7
  169. package/dist/trace/engine-notice-wire.d.ts +3 -1
  170. package/dist/trace/engine-notice-wire.js +2 -0
  171. package/dist/trace/project.d.ts +1 -1
  172. package/dist/trace/project.js +9 -0
  173. package/dist/trace/wire-projection-faces.d.ts +10 -3
  174. package/dist/trace/wire-projection-faces.js +8 -2
  175. package/package.json +2 -2
@@ -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;
@@ -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();
@@ -55,10 +55,12 @@ export declare class MemoryRunStore {
55
55
  /** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
56
56
  * [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
57
57
  setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
58
- /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard). */
59
- setSuspended(taskId: string): Promise<void>;
60
- /** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review. */
61
- setNeedsReview(taskId: string): Promise<void>;
58
+ /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
59
+ * S-173:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。 */
60
+ setSuspended(taskId: string, result: PersistedTaskResult | null): Promise<void>;
61
+ /** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review.
62
+ * S-173:同 {@link setSuspended} 落封口脱敏后的 `result`。 */
63
+ setNeedsReview(taskId: string, result: PersistedTaskResult | null): Promise<void>;
62
64
  /** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags.
63
65
  * [ref](codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改。 */
64
66
  markResuming(taskId: string, instanceId: string): Promise<boolean>;
@@ -124,7 +126,7 @@ export declare class MemoryRunStore {
124
126
  /** Fail stale `running` rows (crashed-run backstop) + release task_active for any now-terminal row. */
125
127
  reapStale(olderThanMs: number): Promise<number>;
126
128
  /** [ref] shared tail of both suspended-run reapers: flip one parked row → failed('approval.expired'), the
127
- * in-memory twin of the SQL UPDATE (`error`/`error_code` set directly — no result blob to derive from). */
129
+ * in-memory twin of the SQL UPDATE (`error`/`error_code` set directly — no result blob to derive from; S-173: the park-time `result` is cleared, same as the SQL `result = NULL`). */
128
130
  private failExpiredPark;
129
131
  /** Release the claim for TERMINAL rows only — suspended/needs_review keep it (the `DELETE ta … WHERE tr.status
130
132
  * NOT IN ('running','suspended','needs_review')` twin, shared by reapStale + the [ref] suspended reapers). */
@@ -1,4 +1,4 @@
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 { notifyRunTerminal } from "../observability/run-terminal.js";
@@ -105,7 +105,7 @@ export class MemoryRunStore {
105
105
  .map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
106
106
  }
107
107
  async setTerminal(taskId, status, result, error) {
108
- result = redactTerminalResult(result);
108
+ result = sealRunRowResult(result);
109
109
  error = error === null ? null : (redactErrorMessage(error) ?? null);
110
110
  const r = this.runs.get(taskId);
111
111
  if (!r) {
@@ -140,17 +140,21 @@ export class MemoryRunStore {
140
140
  if (pending)
141
141
  notifyRunTerminal(pending);
142
142
  }
143
- async setSuspended(taskId) {
143
+ async setSuspended(taskId, result) {
144
+ const sealed = sealRunRowResult(result);
144
145
  const r = this.runs.get(taskId);
145
146
  if (r && (r.status === "running" || r.status === "suspended")) {
146
147
  r.status = "suspended";
148
+ r.result = sealed;
147
149
  r.updatedAt = new Date();
148
150
  }
149
151
  }
150
- async setNeedsReview(taskId) {
152
+ async setNeedsReview(taskId, result) {
153
+ const sealed = sealRunRowResult(result);
151
154
  const r = this.runs.get(taskId);
152
155
  if (r && (r.status === "running" || r.status === "needs_review")) {
153
156
  r.status = "needs_review";
157
+ r.result = sealed;
154
158
  r.updatedAt = new Date();
155
159
  }
156
160
  }
@@ -159,6 +163,7 @@ export class MemoryRunStore {
159
163
  if (!r || (r.status !== "suspended" && r.status !== "needs_review"))
160
164
  return false;
161
165
  r.status = "running";
166
+ r.result = null;
162
167
  r.instanceId = instanceId;
163
168
  r.cancelRequested = false;
164
169
  r.preemptRequested = false;
@@ -292,6 +297,7 @@ export class MemoryRunStore {
292
297
  for (const r of this.runs.values()) {
293
298
  if (r.status === "running" && r.updatedAt.getTime() < cutoff) {
294
299
  r.status = "failed";
300
+ r.result = null;
295
301
  r.error = "run stalled (instance lost?)";
296
302
  r.updatedAt = new Date();
297
303
  reaped += 1;
@@ -302,6 +308,7 @@ export class MemoryRunStore {
302
308
  }
303
309
  failExpiredPark(r) {
304
310
  r.status = "failed";
311
+ r.result = null;
305
312
  r.error = "approval expired before decision";
306
313
  r.errorCode = "approval.expired";
307
314
  r.updatedAt = new Date();
@@ -16,8 +16,45 @@ export interface OneTimeMigrationReport {
16
16
  /** M4:`device_audit.detail` 里 JSON 文本 `null` 被改成列 NULL 的行数(表不在 ⇒ 恒 0)。
17
17
  * 非零 = 这次迁移真改写了存量字节,必须让运维看见(与 M1 的 `retiredSnapshotRows` 同一条纪律)。 */
18
18
  readonly auditDetailJsonNullRows: number;
19
+ /** M5:`session_meta.incarnation` 列 —— `added` = 本次 `ADD COLUMN`;`present` = 早已在(新库 / 第二跑);
20
+ * `no-table` = 这只库上没有 `session_meta`(中央 DDL 之后不可达,只为不编一个读数);`lazy-on-read` = local 文件车道
21
+ * (没有这张表,存量会话首读补铸)。 */
22
+ readonly sessionIncarnationColumn: "added" | "present" | "no-table" | "lazy-on-read";
23
+ /** M5:本次给空化身铸了令牌的行数。非零 = 这次迁移真写了存量行,必须让运维看见(与 M4 同一条纪律)。 */
24
+ readonly sessionIncarnationsBackfilled: number;
19
25
  }
20
- /** 三条迁移都不适用的载体(local 车道没有 SQL 表)。 */
26
+ /**
27
+ * S-590 —— 一条一次性迁移的窗(登记表的一行;纯数据)。字段注写着每个值的取证方式,改一行之前照着重核。
28
+ * 规则(窗长 30 天、窗关后的第一个 minor 删码、门怎么判)在本文件头注「窗」一节,只写那一次。
29
+ */
30
+ export interface OneTimeMigrationWindow {
31
+ /** 本文件里的条目名(头注「四条迁移」的 M1…M4)。 */
32
+ readonly id: string;
33
+ /** 引入这条迁移的版本 = 第一只含其代码的发布 tag(取证:`git log -S '<该条的常量名>' -- src/plugins/one-time-migrations.ts`
34
+ * 找引入提交 → `git tag --contains <提交> --sort=creatordate` 取最早)。 */
35
+ readonly introducedIn: string;
36
+ /** 该版本在 npm 上的发布日,UTC 日历日 YYYY-MM-DD(取证:`npm view @sema-agent/server time`)。`null` = **未发布形**:引入它的
37
+ * 那一版还没发布,由抬 `package.json` 到那一版的发布准备提交回填(规则见文件头「窗」一节;键必须显式写,缺席 ⇒ 到期门红)。 */
38
+ readonly releasedOn: string | null;
39
+ /** = `releasedOn` + 30 天(UTC 日历日;到期门逐条核这条等式)。今天晚于它 ⇒ 到期门红,删码排进其后第一个 minor。
40
+ * 与 `releasedOn` 同为 `null`(未发布形,不计到期)或同为日期。 */
41
+ readonly removableAfter: string | null;
42
+ /** 删码时删什么(到期门的红句逐字引用)。 */
43
+ readonly removal: string;
44
+ /** 删码之后,存量库若还带着迁移前的形,由哪一条**既有**响亮臂接住;没有就如实写没有(不为它新造兼容读)。 */
45
+ readonly afterRemoval: string;
46
+ /** 本条在 {@link OneTimeMigrationReport} 里的读数键 —— 报告的每个键恰属一条(到期门核:新迁移不登记 ⇒ 红)。 */
47
+ readonly reportKeys: readonly (keyof OneTimeMigrationReport)[];
48
+ }
49
+ /**
50
+ * 🔴 S-590 —— 一次性迁移登记表,本文件各条的窗的**唯一属主**。别处(接口 JSDoc、调用点、DDL 注、部署文档)只写「窗口见
51
+ * `plugins/one-time-migrations.ts` 登记表」;起动日志行 `one_time_migrations_applied` 的 `removableAfter` 读数由本表派生。
52
+ *
53
+ * M1–M3 同一次删:M3 的「本版在这只库上跑过了」标记就是 M1 / M2 的退役 schema 在不在(头注「幂等」一节),先删 M1 / M2
54
+ * 而留着 M3 会让 M3 永远判「跑过了」。三条同版引入、同一天到期,本来就同一次删。
55
+ */
56
+ export declare const ONE_TIME_MIGRATIONS: readonly OneTimeMigrationWindow[];
57
+ /** local 车道上 SQL 独有的四条(M1 / M2 / M4 / M5)的读数:那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
21
58
  export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "askOriginRowsMigrated">;
22
59
  /** 启动期的日志口(logger 的子集)。两级各有其用,见 {@link logOneTimeMigrations}。 */
23
60
  export interface MigrationLogSink {