@sema-agent/server 7.143.0 → 7.144.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 (94) hide show
  1. package/MIGRATION.md +166 -0
  2. package/USAGE.md +24 -14
  3. package/dist/approval-card.d.ts +31 -14
  4. package/dist/approval-card.js +8 -8
  5. package/dist/boot/execution-env.js +1 -1
  6. package/dist/boot/runner-deps.d.ts +10 -5
  7. package/dist/boot/runner-deps.js +5 -4
  8. package/dist/boot/shutdown.d.ts +1 -1
  9. package/dist/boot/shutdown.js +4 -2
  10. package/dist/boot/stage-01-config.js +2 -0
  11. package/dist/boot/stage-07-capability-layer.js +1 -1
  12. package/dist/boot/stage-10-http-server.js +2 -7
  13. package/dist/capabilities/scenarios.d.ts +14 -18
  14. package/dist/capabilities/scenarios.js +1 -2
  15. package/dist/config-d-migration.d.ts +2 -0
  16. package/dist/delegated-session-owner.d.ts +33 -151
  17. package/dist/delegated-session-owner.js +1 -83
  18. package/dist/entry-mode-clamp-notice.d.ts +1 -1
  19. package/dist/execution-placement.d.ts +7 -4
  20. package/dist/hooks/classic-stations.d.ts +3 -1
  21. package/dist/hooks/classic-stations.js +2 -0
  22. package/dist/http/principal-gate.d.ts +1 -1
  23. package/dist/http/refusal-body.d.ts +7 -3
  24. package/dist/http/refusal-body.js +1 -0
  25. package/dist/http/resume-legs.js +11 -4
  26. package/dist/http/routes/approvals-assistant.js +1 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/lane-home-probe.d.ts +15 -0
  30. package/dist/lane-home-probe.js +4 -0
  31. package/dist/observability/corrupt-read-seat.d.ts +5 -9
  32. package/dist/observability/corrupt-read-seat.js +1 -2
  33. package/dist/observability/fail-open.d.ts +12 -8
  34. package/dist/observability/fail-open.js +12 -8
  35. package/dist/observability/run-terminal.d.ts +1 -1
  36. package/dist/observability/run-terminal.js +2 -2
  37. package/dist/parked-revive-surface.d.ts +6 -7
  38. package/dist/peer-inbound-face.d.ts +29 -34
  39. package/dist/peer-inbound-face.js +28 -63
  40. package/dist/plugins/caching-session-store.d.ts +0 -3
  41. package/dist/plugins/caching-session-store.js +0 -3
  42. package/dist/plugins/checkpoint-store-sql.d.ts +41 -34
  43. package/dist/plugins/checkpoint-store-sql.js +99 -38
  44. package/dist/plugins/fork-routing-session-store.d.ts +8 -1
  45. package/dist/plugins/fork-routing-session-store.js +6 -0
  46. package/dist/plugins/local-checkpoint-store.d.ts +6 -8
  47. package/dist/plugins/local-checkpoint-store.js +3 -2
  48. package/dist/plugins/local-session-store.d.ts +64 -29
  49. package/dist/plugins/local-session-store.js +126 -56
  50. package/dist/plugins/local-session-window.d.ts +4 -2
  51. package/dist/plugins/one-time-migrations.d.ts +12 -12
  52. package/dist/plugins/one-time-migrations.js +12 -12
  53. package/dist/plugins/pg-session-storage.d.ts +1 -9
  54. package/dist/plugins/pg-session-storage.js +2 -19
  55. package/dist/plugins/remote-env-host.d.ts +8 -4
  56. package/dist/plugins/remote-env-host.js +7 -9
  57. package/dist/plugins/remote-shell.js +12 -2
  58. package/dist/plugins/schema-consistency.d.ts +5 -1
  59. package/dist/plugins/session-fork.d.ts +1 -1
  60. package/dist/plugins/session-ownership.d.ts +9 -0
  61. package/dist/plugins/session-ownership.js +6 -0
  62. package/dist/plugins/session-store.d.ts +13 -16
  63. package/dist/plugins/session-store.js +9 -10
  64. package/dist/plugins/sql-json-column.d.ts +24 -2
  65. package/dist/plugins/sql-json-column.js +38 -3
  66. package/dist/plugins/store-backend.js +3 -2
  67. package/dist/plugins/tidb-session-store.d.ts +2 -14
  68. package/dist/plugins/tidb-session-store.js +2 -20
  69. package/dist/run-local.js +2 -1
  70. package/dist/runs.js +4 -0
  71. package/dist/security.d.ts +0 -4
  72. package/dist/session-error-wire.d.ts +24 -25
  73. package/dist/session-error-wire.js +4 -5
  74. package/dist/session-incarnation.d.ts +2 -2
  75. package/dist/session-notify-door.d.ts +2 -2
  76. package/dist/session-notify-door.js +5 -1
  77. package/dist/tool-approval.d.ts +3 -1
  78. package/dist/tool-approval.js +47 -18
  79. package/dist/trace/core-keyset-guard.d.ts +1 -1
  80. package/dist/trace/engine-notice-wire.d.ts +1 -1
  81. package/dist/trace/engine-notice-wire.js +1 -1
  82. package/dist/trace/ledger-sink.js +12 -4
  83. package/dist/trace/model-segment.d.ts +25 -6
  84. package/dist/trace/model-segment.js +47 -6
  85. package/dist/trace/project.d.ts +8 -2
  86. package/dist/trace/project.js +6 -4
  87. package/dist/trace/redact.d.ts +11 -0
  88. package/dist/trace/redact.js +19 -3
  89. package/dist/trace/self-minted-wire.d.ts +88 -9
  90. package/dist/trace/self-minted-wire.js +25 -3
  91. package/package.json +2 -2
  92. package/wire-changes/server@7.144.0.json +291 -0
  93. package/dist/session-records-skipped-notice.d.ts +0 -21
  94. package/dist/session-records-skipped-notice.js +0 -27
@@ -3,17 +3,17 @@ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, rea
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { dirname, join, resolve } from "node:path";
6
- import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
6
+ import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf, placedSessionOwnerOf } from "./session-ownership.js";
7
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
- import { createPlacedSessionRow, sourceFacesVia } from "../delegated-session-owner.js";
10
9
  import { placementCapabilityOf } from "./session-placement.js";
11
10
  import { z } from "zod";
12
11
  import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
13
12
  import { StoreBootRefusalError } from "./store-contracts.js";
14
13
  import { judgeSessionRowWrite, sessionRowTagKey, settleSessionRowWrite, } from "../session-row.js";
15
14
  import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
16
- import { createZodColumnReader, decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
15
+ import { createZodColumnReader, decodeJsonColumn, isJsonColumnBodyProblem, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
16
+ import { SCHEMA_MISMATCH_ERROR_CODE } from "./schema-consistency.js";
17
17
  import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./one-time-migrations.js";
18
18
  import { recordFailOpen } from "../observability/fail-open.js";
19
19
  import { createLogger } from "../observability/logger.js";
@@ -24,7 +24,6 @@ import { SessionForkInProgressError, SessionWriterPresentError, WRITABLE, assert
24
24
  import { classifyForkTarget, forkByFreshId } from "./session-fork.js";
25
25
  import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
26
26
  import { decodeSessionWorkspaceCell, encodeSessionWorkspaceCell } from "./session-workspace-cell.js";
27
- import { noteReplayInSessionOpenScope, openSessionForItself } from "../session-records-skipped-notice.js";
28
27
  import { createCorruptReadFailOpenSeat } from "../observability/corrupt-read-seat.js";
29
28
  import { buildObjectivePreview, isAttachmentFrame } from "../user-authored-text.js";
30
29
  import { classifySyncRelationshipByIds, ownerCellIs, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, } from "../session-sync-kernel.js";
@@ -35,8 +34,8 @@ function archiveFaultWord(code) {
35
34
  case "not_found":
36
35
  return "absent";
37
36
  case "corrupt_session":
38
- case "invalid_session":
39
37
  return code;
38
+ case "invalid_session":
40
39
  case "invalid_entry":
41
40
  case "invalid_fork_target":
42
41
  case "storage":
@@ -58,7 +57,7 @@ function ownerUnreadableArchive(probe) {
58
57
  const { fault } = probe;
59
58
  if (probe.word === "storage" || !(fault instanceof SessionError))
60
59
  return probe;
61
- return { ...probe, fault: new SessionOwnerUnreadableError(probe.word, fault) };
60
+ return { ...probe, fault: new SessionOwnerUnreadableError(fault) };
62
61
  }
63
62
  function importRowRefused(sessionId, landing) {
64
63
  return new SessionError("conflict", landing === "create"
@@ -154,6 +153,32 @@ function ownerClaimWindowOpen() {
154
153
  }
155
154
  const SIDECAR_MAP_STATE_BY_PATH = new Map();
156
155
  const SESSION_WRITE_LOCKS_BY_ROOT = new Map();
156
+ const quotedFieldNames = (names) => `${names.slice(0, 8).map((name) => JSON.stringify(name.slice(0, 64))).join(", ")}${names.length > 8 ? `, … (${names.length} in all)` : ""}`;
157
+ function describeLocalDataRootMismatch(findings, when) {
158
+ const lines = [
159
+ when === "boot"
160
+ ? `${SCHEMA_MISMATCH_ERROR_CODE}: the local data root does not match this build's shape — refusing to start.`
161
+ : `${SCHEMA_MISMATCH_ERROR_CODE}: the local data root does not match this build's shape — refusing to read or write the file(s) named below until this process is restarted.`,
162
+ ];
163
+ for (const f of findings) {
164
+ lines.push(f.unknownFields.length > 0
165
+ ? ` ${f.label} at ${f.path}: UNKNOWN field(s) ${quotedFieldNames(f.unknownFields)} — the data root is NEWER than this build (a newer server engine wrote it).`
166
+ : ` ${f.label} at ${f.path}: unreadable (${f.problem}) — this build cannot read it, and an unreadable file is treated as a mismatch, not as empty.`);
167
+ }
168
+ lines.push("This build never reads such a file as empty and never writes over it: either would silently change what the file records.", "", " Ways out (these three and no others):", " (1) the data root is NEWER than this build (unknown fields): deploy the build that wrote it;", " (2) roll back: restore the data-root backup taken before the upgrade, then start this build — also the way out when the file's bytes are damaged (not-json / malformed);", " (3) the file could not be read at all (an I/O error such as EACCES): repair access to it (permissions, mount, disk) and restart — nothing in the file needs changing.", "The check behind this refusal only reads: the file was left in place and nothing in the data root was changed.");
169
+ return lines.join("\n");
170
+ }
171
+ export class LocalDataRootSchemaMismatchError extends StoreBootRefusalError {
172
+ findings;
173
+ when;
174
+ code = SCHEMA_MISMATCH_ERROR_CODE;
175
+ constructor(findings, when) {
176
+ super(describeLocalDataRootMismatch(findings, when));
177
+ this.findings = findings;
178
+ this.when = when;
179
+ this.name = "LocalDataRootSchemaMismatchError";
180
+ }
181
+ }
157
182
  class SidecarMap {
158
183
  path;
159
184
  label;
@@ -180,32 +205,68 @@ class SidecarMap {
180
205
  if (!path)
181
206
  return Promise.resolve();
182
207
  const st = this.state;
208
+ if (st.refused !== undefined)
209
+ return Promise.reject(st.refused);
183
210
  if (!st.loaded) {
184
211
  st.loaded = (async () => {
185
- try {
186
- const parsed = decodeJsonColumn(await readFile(path, "utf8"), this.read, this.label);
187
- for (const [id, v] of Object.entries(parsed)) {
212
+ const disk = await this.readDisk(path);
213
+ if (disk.state === "absent")
214
+ return;
215
+ if (disk.state === "read") {
216
+ for (const [id, v] of Object.entries(disk.entries)) {
188
217
  if (!st.entries.has(id))
189
218
  st.entries.set(id, v);
190
219
  }
220
+ return;
191
221
  }
192
- catch (e) {
193
- if (e?.code === "ENOENT")
194
- return;
195
- if (!this.strict) {
196
- this.onUnreadable(path, e);
197
- return;
198
- }
199
- const kind = e instanceof SqlJsonColumnError ? e.problem : (e?.code ?? "unreadable");
200
- throw new Error(`${this.label} is unreadable (${kind}) — refusing to read or write session owners and incarnations until it is repaired: ` +
201
- `reading it as empty would turn every session ownerless, and a replacement incarnation token for an existing session would ` +
202
- `make it a different occupant (its memory opt-out would stop binding it); a write would overwrite the unreadable bytes. ` +
203
- `The file is left in place at ${path}.`);
222
+ if (!this.strict) {
223
+ this.onUnreadable(path, disk.fault);
224
+ return;
204
225
  }
226
+ st.loaded = undefined;
227
+ throw this.refuse(path, disk);
205
228
  })();
206
229
  }
207
230
  return st.loaded;
208
231
  }
232
+ async reconcile() {
233
+ const path = this.path;
234
+ if (!path)
235
+ return;
236
+ const st = this.state;
237
+ if (st.refused !== undefined)
238
+ throw st.refused;
239
+ if (!st.loaded)
240
+ return this.ensureLoaded();
241
+ await st.loaded;
242
+ const disk = await this.readDisk(path);
243
+ if (disk.state === "unreadable")
244
+ throw this.refuse(path, disk);
245
+ }
246
+ async readDisk(path) {
247
+ let text;
248
+ try {
249
+ text = await readFile(path, "utf8");
250
+ }
251
+ catch (e) {
252
+ return e?.code === "ENOENT" ? { state: "absent" } : { state: "unreadable", fault: e };
253
+ }
254
+ try {
255
+ return { state: "read", entries: decodeJsonColumn(text, this.read, this.label) };
256
+ }
257
+ catch (e) {
258
+ return { state: "unreadable", fault: e };
259
+ }
260
+ }
261
+ refuse(path, disk) {
262
+ const shape = isJsonColumnBodyProblem(disk.fault);
263
+ const problem = disk.fault instanceof SqlJsonColumnError ? disk.fault.problem : (disk.fault?.code ?? "unreadable");
264
+ const unknownFields = disk.fault instanceof SqlJsonColumnError ? disk.fault.unknownKeys : [];
265
+ const refusal = new LocalDataRootSchemaMismatchError([{ path, label: this.label, problem, unknownFields }], "serving");
266
+ if (shape)
267
+ this.state.refused = refusal;
268
+ return refusal;
269
+ }
209
270
  persist() {
210
271
  return this.commit(() => true);
211
272
  }
@@ -222,11 +283,22 @@ class SidecarMap {
222
283
  const path = this.path;
223
284
  const st = this.state;
224
285
  const link = st.writeChain.then(async () => {
286
+ if (st.refused !== undefined)
287
+ throw st.refused;
225
288
  const base = new Map(st.entries);
226
289
  const draft = new Map(base);
227
290
  if (!change(draft))
228
291
  return true;
229
292
  if (path) {
293
+ if (this.strict) {
294
+ const disk = await this.readDisk(path);
295
+ if (disk.state === "unreadable") {
296
+ if (isJsonColumnBodyProblem(disk.fault))
297
+ throw this.refuse(path, disk);
298
+ this.onWriteFailed(disk.fault);
299
+ return false;
300
+ }
301
+ }
230
302
  try {
231
303
  const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
232
304
  await writeFile(tmp, JSON.stringify(Object.fromEntries(draft)), "utf8");
@@ -247,7 +319,7 @@ class SidecarMap {
247
319
  }
248
320
  return true;
249
321
  });
250
- st.writeChain = link.then(() => undefined);
322
+ st.writeChain = link.then(() => undefined, () => undefined);
251
323
  return link;
252
324
  }
253
325
  }
@@ -261,10 +333,7 @@ function noteReplayInArchiveRewriteRead(info) {
261
333
  }
262
334
  export function createLocalSessionRepo(root) {
263
335
  return new FileSessionRepo(root, {
264
- onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", (info) => {
265
- noteReplayInSessionOpenScope(info);
266
- noteReplayInArchiveRewriteRead(info);
267
- }),
336
+ onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", noteReplayInArchiveRewriteRead),
268
337
  });
269
338
  }
270
339
  function occupiedBy(session, row) {
@@ -357,7 +426,6 @@ export class LocalSessionStore {
357
426
  placements;
358
427
  placementOf;
359
428
  listPlaced;
360
- claimOwnerless;
361
429
  recordedPlacementOf;
362
430
  pending = new Map();
363
431
  rowsDropped = new Set();
@@ -375,6 +443,7 @@ export class LocalSessionStore {
375
443
  instructionSources;
376
444
  workspaces;
377
445
  meta;
446
+ strictSidecars = [];
378
447
  log;
379
448
  sessionWriteLocks;
380
449
  executionPlacement;
@@ -395,7 +464,6 @@ export class LocalSessionStore {
395
464
  this.recordedPlacementOf = (sessionId) => repoPlacementOf.call(repo, sessionId);
396
465
  this.placementOf = this.recordedPlacementOf;
397
466
  this.listPlaced = (kind, o) => repoListPlaced.call(repo, kind, o);
398
- this.claimOwnerless = (sessionId, incarnation, owner) => this.claimOwnerlessPlaced(sessionId, incarnation, owner);
399
467
  }
400
468
  else {
401
469
  this.recordedPlacementOf = async () => undefined;
@@ -415,7 +483,12 @@ export class LocalSessionStore {
415
483
  }
416
484
  this.durability = opts.durability;
417
485
  }
418
- const sidecar = (file, which, read, strict = false) => new SidecarMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, read, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which), strict);
486
+ const sidecar = (file, which, read, strict = false) => {
487
+ const map = new SidecarMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, read, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which), strict);
488
+ if (strict)
489
+ this.strictSidecars.push(map);
490
+ return map;
491
+ };
419
492
  this.titles = sidecar("session-titles.json", "title", readStringRecordColumn);
420
493
  this.pinned = sidecar("session-pinned.json", "pinned", readPinnedSidecar);
421
494
  this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source", readStringRecordColumn);
@@ -436,6 +509,19 @@ export class LocalSessionStore {
436
509
  place: (sessionId, occupant, cell) => this.placeExecution(sessionId, occupant, cell),
437
510
  };
438
511
  }
512
+ async assertStrictSidecarsReadable() {
513
+ const loads = await Promise.allSettled(this.strictSidecars.map((map) => map.reconcile()));
514
+ const findings = [];
515
+ for (const load of loads) {
516
+ if (load.status === "fulfilled")
517
+ continue;
518
+ if (!(load.reason instanceof LocalDataRootSchemaMismatchError))
519
+ throw load.reason;
520
+ findings.push(...load.reason.findings);
521
+ }
522
+ if (findings.length > 0)
523
+ throw new LocalDataRootSchemaMismatchError(findings, "boot");
524
+ }
439
525
  async forkingRead(sessionId, meta) {
440
526
  if (!isForkCreatedCell(meta))
441
527
  return false;
@@ -928,7 +1014,7 @@ export class LocalSessionStore {
928
1014
  }
929
1015
  async openForItselfHeld(sessionId, opts) {
930
1016
  await this.meta.ensureLoaded();
931
- const session = await openSessionForItself(sessionId, () => this.repo.open({ id: sessionId, createdAt: "" }));
1017
+ const session = await this.repo.open({ id: sessionId, createdAt: "" });
932
1018
  admitSessionPlacement(sessionId, (await session.getMetadata()).placement, opts ?? {}, placementCapabilityOf(this));
933
1019
  return this.handOut(sessionId, session);
934
1020
  }
@@ -936,15 +1022,9 @@ export class LocalSessionStore {
936
1022
  if (this.metaCellOf(sessionId) !== undefined) {
937
1023
  throw new SessionError("placement_refused", `session ${sessionId} is already registered as an ordinary session — refusing to create a subagent transcript on its id`);
938
1024
  }
939
- let session;
940
- await createPlacedSessionRow(sessionId, placement, {
941
- readFaces: sourceFacesVia((id) => this.headOf(id)),
942
- insert: async (owner) => {
943
- session = await this.repo.create({ id: sessionId, placement });
944
- await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() } : undefined));
945
- },
946
- readOwner: async () => (await this.headOf(sessionId))?.owner,
947
- }, this.log);
1025
+ const owner = placedSessionOwnerOf(sessionId, placement);
1026
+ const session = await this.repo.create({ id: sessionId, placement });
1027
+ await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() } : undefined));
948
1028
  admitSessionPlacement(sessionId, (await session.getMetadata()).placement, opts, placementCapabilityOf(this));
949
1029
  return this.handOut(sessionId, session);
950
1030
  }
@@ -986,20 +1066,6 @@ export class LocalSessionStore {
986
1066
  await this.repo.delete({ id: sessionId, createdAt: "" });
987
1067
  });
988
1068
  }
989
- async claimOwnerlessPlaced(sessionId, incarnation, owner) {
990
- return this.withSessionWriteLock(sessionId, async () => {
991
- if ((await this.placedRecordOrThrow(sessionId)) === undefined)
992
- return false;
993
- return this.writeMeta(sessionId, (before) => {
994
- if (before === undefined)
995
- return undefined;
996
- const cell = readSessionMetaCell(before);
997
- if (cell.incarnation !== incarnation || (cell.owner ?? null) !== null)
998
- return undefined;
999
- return cellWith(before, { owner });
1000
- });
1001
- });
1002
- }
1003
1069
  async listSessions(opts) {
1004
1070
  await this.loadRowSidecars();
1005
1071
  const rows = [];
@@ -1195,8 +1261,6 @@ export class LocalSessionStore {
1195
1261
  case "judged":
1196
1262
  try {
1197
1263
  const archive = (await this.archiveHeadOf(sessionId)) ?? null;
1198
- if ((archive?.owner ?? null) !== null)
1199
- throw new StampedSessionArchiveError(sessionId);
1200
1264
  await this.repo.importEntries(sessionId, undefined, entries, { expect: { row: archive } });
1201
1265
  }
1202
1266
  catch (err) {
@@ -1274,6 +1338,10 @@ export class LocalSessionStore {
1274
1338
  forgetStaging(stagingId) {
1275
1339
  this.stagings.delete(stagingId);
1276
1340
  }
1341
+ async assertArchiveAnonymousHeld(sessionId) {
1342
+ if (((await this.archiveHeadOf(sessionId))?.owner ?? null) !== null)
1343
+ throw new StampedSessionArchiveError(sessionId);
1344
+ }
1277
1345
  async commitStagedImport(sessionId, owner, staged, opts) {
1278
1346
  return this.withImportLock(sessionId, owner, async (importer) => {
1279
1347
  const current = await this.metaOf(sessionId);
@@ -1284,6 +1352,9 @@ export class LocalSessionStore {
1284
1352
  await this.assertNotPlacedHeld(sessionId);
1285
1353
  const dstEntries = await this.exportEntries(sessionId);
1286
1354
  const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
1355
+ const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
1356
+ if (!contentEqual)
1357
+ await this.assertArchiveAnonymousHeld(sessionId);
1287
1358
  if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
1288
1359
  throw new SyncConflictError(sessionId, rel);
1289
1360
  }
@@ -1293,7 +1364,6 @@ export class LocalSessionStore {
1293
1364
  if (diverged)
1294
1365
  throw new SyncConflictError(sessionId, contentForkRelation(diverged, rel.newEntryIds));
1295
1366
  }
1296
- const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
1297
1367
  if (current?.execution?.role === "writer")
1298
1368
  throw new SessionWriterPresentError(sessionId, current.execution.lane);
1299
1369
  if (rel.relation !== "identical" || !contentEqual) {
@@ -24,12 +24,14 @@
24
24
  *
25
25
  * ## Cost: one linear walk, the engine's
26
26
  * The branch is core's own `getPathToRoot` over the whole log (the walk every engine read of a local session makes, with its stop
27
- * rules: an entry without a parent ends it, a parent the log does not hold is `invalid_session`, an unknown leaf is `not_found`).
27
+ * rules: an entry without a parent ends it, a parent the log does not hold is `corrupt_session` — `invalid_session` up to core 7.61.x —, an unknown
28
+ * leaf is `not_found`). A missing parent whose replay dropped records is the one break a RUN does not refuse since core 7.62.0 ([ref]: it re-roots
29
+ * and says `session.lineage_rerooted`); this view is not a run and takes the walk's refusal as is.
28
30
  * Since core 7.54.0 ([ref]) that walk is linear; before, it built the path with `unshift` (quadratic — seconds on a long session, on
29
31
  * the event loop) and this module walked the `parentId` chain itself. The second walker is retired: the view reads the branch the
30
32
  * engine reads, by the engine's reading. Every later read of the window goes through the same walk, bounded by the floor.
31
33
  * A parent CYCLE (a corrupted log; the engine never writes one) is refused by that same walk: since core 7.54.1 it carries one
32
- * `ParentChainGuard`, and the entry it stands on twice ends it with `invalid_session` naming that entry — `wake` refuses a looped log
34
+ * `ParentChainGuard`, and the entry it stands on twice ends it with `corrupt_session` (`invalid_session` up to core 7.61.x) naming that entry — `wake` refuses a looped log
33
35
  * the way it refuses one whose parent is missing. Core 7.54.0's walk had no guard (a looped log spun it forever on the event loop,
34
36
  * and the view's wake with it). The retired walker's own cycle refusal went with it: the engine's guard is the one refusal now, shared
35
37
  * by `getPathToRoot` and the run's resume ancestor scan (the two walks core 7.54.1 guards; this module has no walker of its own left).
@@ -194,9 +194,10 @@
194
194
  * (拒因点名版本与键);不带 ⇒ 载荷零改动读回,读回方按 5 写回原位,启动日志 `config_lkg_migrated` 一行。规则在 `config-lkg.ts` 顶注 formatVersion 5 一段与
195
195
  * `migrateV4Lkg`。没有读数键(它不写库)。
196
196
  * · **M23 存量 placed 子代行的属主**(7.133.0)—— 7.132.x 及以前两只 SQL 店建委派子代的 placed 行时属主恒 NULL,会话面的属主谓词对 NULL 放行 ⇒ 谁拿到子代的会话 id
197
- * (父客户端在子代帧的 `transcriptId` 上收到)谁就读得到整段转录。7.133.0 起建行那一笔按委派继承律写属主(`src/delegated-session-owner.ts`,律的唯一属主),
197
+ * (父客户端在子代帧的 `transcriptId` 上收到)谁就读得到整段转录。7.133.0–7.143.x 建行那一笔按本仓委派继承律写属主,7.144.0 起记引擎交的 `placement.owner`
198
+ * (core 7.62.0 [ref];`src/delegated-session-owner.ts` 头注),
198
199
  * 属主为 NULL 的 placed 行只对 fleet 调用方放行(无主放行臂的唯一判据 `security.ts` 的 `ownerlessRowOpen`,常设的读面律,不随本条删);存量行 ⇒ **每次起服的幂等回填**,
199
- * 判据与建行逐字同一只(`judgeDelegatedOwner`,属主 = 判断时见证到的):根会话的行在 ⇒ 它的属主(根无主 ⇒ 子代仍无主,不写);根行不在 ⇒ 父会话的行;都不在(孤行)⇒
200
+ * 判据 = 旧律(`judgeDelegatedOwner`,属主 = 判断时见证到的;引擎当年的读数没有落库,读不回):根会话的行在 ⇒ 它的属主(根无主 ⇒ 子代仍无主,不写);根行不在 ⇒ 父会话的行;都不在(孤行)⇒
200
201
  * 哨兵属主 `_orphan:<id>`(保留主体前缀,任何已验身份都不等于它 —— 不留 NULL,宁可没人能读),计数 + 起服一行 warn `placed_session_owners_orphaned` 点名条数。记下的根 / 父是空串 ⇒ 读作没有那一格。
201
202
  * **单趟**:按 `session_id` 键集分页扫 `placement_kind IS NOT NULL ∧ owner IS NULL` 的行,每一页一条语句连读根 / 父两行(属主、住客、是不是 placed 行;连接键套
202
203
  * `NULLIF(…, '')`);回写 = 认领句(`CLAIM_OWNERLESS_PLACED_SQL`:只 CAS 子行自己 —— 扫描读到的化身 + `owner IS NULL` + `placement_kind IS NOT NULL`;扫描与回写之间根 / 父被删、
@@ -204,10 +205,9 @@
204
205
  * 化身为空的行(子代或来源行)⇒ 响亮拒(全仓「读到空化身」同一条律)。读数三格:写成的(`placedSessionOwnersInherited` / `placedSessionOwnersOrphaned`)与
205
206
  * **残余** `placedSessionOwnersRemaining` = 律这一读判不出、因为它读到的来源本身还是一只无主的委派子代行的行(根已删、父是同样待回填的行,且排在它之后或在同一页里)。
206
207
  * 残余非零 ⇒ 读数行 warn;**不拒启、不重扫**:本文件的每一条在窗内每次起服都跑(「窗」一节),残余行只对 fleet 调用方放行(读面律),由之后起服的本条(一次至少再定下一层)
207
- * 或这一行的下一次停驻(停驻挂点按同一条律认领,`delegated-session-owner.ts` 的 `settleDelegatedOwner`)定下。根无主的子代(律就让它无主)不是残余,每次起服照样被扫到、照样不写。
208
- * **够不到的一格(如实登记)**:local 车道没有 placed 行(引擎 File 店不实现 placement,子代住私店),本条不碰 local 的会话边档;local 店也不记委派树、
209
- * 不认领 ⇒ 经晋升腿进 local 宿主店的升级前子代行仍无主,它们再次停驻时停驻挂点照「属主不是律给的那一只 ⇒ 拒」处置(收口 = local 的 placement 协议;随包文字的补偿:
210
- * local 带身份的部署升级前先把停驻待批的审批处理完)。窗 = 发布日 + 30 天(与 M19 / M21 同一条论证:
208
+ * 定下(7.144.0 起停驻挂点只比对、不认领)。根无主的子代(律就让它无主)不是残余,每次起服照样被扫到、照样不写。
209
+ * **够不到的一格(如实登记)**:本条只扫 SQL 两店,不碰 local 的会话边档 ⇒ 经晋升腿进 local 宿主店的升级前子代行(普通行)仍无主;它们再次停驻时停驻挂点只比对
210
+ * 引擎交来的属主(复活腿 = 那一行自己的属主 ⇒ 无主照无主比对;随包文字的补偿:local 带身份的部署升级前先把停驻待批的审批处理完)。窗 = 发布日 + 30 天(与 M19 / M21 同一条论证:
211
211
  * 窗关时缺省部署上升级前的子代转录已被联合收割按 `BG_AGENT_RETENTION_MS` 收走;调长了留存的部署、滚动升级里旧副本在窗内新写的行如实登记,不为它们加长窗)。
212
212
  * · **M24 workflow journal 的属主 scope 搬家**(7.133.0)—— 7.132.x 及以前 `workflow_journal` 的 scope 是**每一行**上的一列:读面按它判,写面(按
213
213
  * `(run_id, ordinal)` 的 upsert)从不判它 ⇒ 别的 scope 的 append 落新序数插进一条 run、落既有序数覆盖属主的结果。7.133.0 起一条 run 的属主 scope 只记在
@@ -310,7 +310,7 @@
310
310
  * · **local 车道**:没有绑定表、没有 `session_meta`(device 车道只在 SQL 后端)⇒ 只有 ④ 与回填前提:读本地 run 店有 run 的会话,执行席读出 absent 的 ⇒ 经同一只席 `place` 成 writer(表唯一项)
311
311
  * (`LocalBackend.runOneTimeMigrations`);local 没有「切换收尾」的形可读 ⇒ 窗内每次起服都判,首跑之后它只碰认领与放置之间被打断的会话(与 `place` 会落下的格同值)。
312
312
  * 有 run、而**存档读不出**的会话 —— 会话元数据格缺(升级链上从没被再读过的老会话)而探针读不出,或 fork 创建的格而探针说不出存档在不在 ——:回填跳过它并点名
313
- * (读数 `executionPlacementSessionsUnreadable` + 一行一条 warn `execution_placement_session_unreadable`,带会话 id 与故障词〔闭集 `corrupt_session` / `invalid_session` / `storage`;
313
+ * (读数 `executionPlacementSessionsUnreadable` + 一行一条 warn `execution_placement_session_unreadable`,带会话 id 与故障词〔闭集 `corrupt_session` / `storage`;
314
314
  * `storage` = 读存档文件时抛了别的故障:权限 / 磁盘 I/O / 超大,再带 `errno`〕),**不拒启**、一个字节没写 —— 回填只补一格放置、不动存档,读不出的存档不归它判
315
315
  * (这一支只收**探针对这只会话的读故障**;会话元数据边档本身读不出、问账日志压实失败等整库级故障照旧拒启);该会话的请求照旧答存档的故障,存档修好(从留存副本恢复,
316
316
  * 或 `storage` 时权限 / 磁盘修好)后的下一次起服照常补放置;存档移走则它不再是本服务的会话(不补放置、不再点名;fork 创建的会话例外 —— 它的格还在,读作 fork 未落定,出路是同键重试那次 fork 或删除该会话)。
@@ -430,7 +430,7 @@ export interface OneTimeMigrationReport {
430
430
  * 行数(SQL 独有;local 恒 0)。非零 ⇒ 另一行 warn 点名条数。 */
431
431
  readonly placedSessionOwnersOrphaned: number;
432
432
  /** M23:这一次起服留着无主的存量 placed 子代行 —— 律这一读判不出,因为它读到的来源本身还是一只无主的委派子代行(根已删、父待回填)。它们只对 fleet 调用方
433
- * 放行(读面律),由之后起服的本条或它的下一次停驻定下。根无主的子代(律就让它无主)不算。行数(SQL 独有;local 恒 0)。非零 ⇒ 读数行 warn(不拒启)。 */
433
+ * 放行(读面律),由之后起服的本条定下。根无主的子代(律就让它无主)不算。行数(SQL 独有;local 恒 0)。非零 ⇒ 读数行 warn(不拒启)。 */
434
434
  readonly placedSessionOwnersRemaining: number;
435
435
  /** M24:workflow journal 的属主 scope —— `moved` = 本次把行上的 scope 列搬进 `workflow_journal_owner`(回填 + 删跨 scope 行 + 删列);`current` = 早已是新形
436
436
  * (新库 / 第二跑:`workflow_journal` 上没有 scope 列);`file-store` = local 车道(core 的 File 店,属主 = 每条 run 的 journal 文件首行)。 */
@@ -528,7 +528,7 @@ export interface ParkedApprovalCardRowTooLong extends ParkedApprovalCardRowUnrea
528
528
  readonly bytes: number;
529
529
  }
530
530
  /**
531
- * M32 local 腿被跳过的一只会话:会话 id 是点名坐标;故障词是闭集 {@link UnusableArchiveWord}(`corrupt_session` = 会话头读不出;`invalid_session` = 读得出、结构走不通;
531
+ * M32 local 腿被跳过的一只会话:会话 id 是点名坐标;故障词是闭集 {@link UnusableArchiveWord}(`corrupt_session` = 会话头读不出,或读得出而日志走不成一棵树;
532
532
  * `storage` = 读的时候抛了别的故障,I/O 级 —— 权限 / 磁盘 / 超大,此时 `errno` 带那只故障的 `code`)。盘上路径与故障句不出店。
533
533
  */
534
534
  export interface ExecutionPlacementSessionUnreadable {
@@ -846,13 +846,13 @@ export declare function repairParkedRows(db: MigrationDb, dialect: SqlDialect, r
846
846
  export declare function backfillParkedApprovalCards(db: MigrationDb, dialect: SqlDialect, receipts: MigrationReceipts): Promise<void>;
847
847
  /**
848
848
  * M23 —— 存量 placed 子代行的属主回填,**每次起服单趟、幂等**(规则、为什么、够不到的一格见头注 M23 段):键集分页扫完 placed ∧ 属主 NULL 的行,每一行按唯一判据判
849
- * (`judgeDelegatedOwner`,建行那一笔与停驻挂点同一只:来源顺序根、父;属主 = 这一读见证到的):
850
- * · 判出属主 ⇒ {@link CLAIM_OWNERLESS_PLACED_SQL}(只 CAS 子行自己,绑扫描读到的化身;与停驻挂点的认领**同一句、同一份绑参**;第二跑零命中);写成的按属主计进
849
+ * (`judgeDelegatedOwner`,旧律:来源顺序根、父;属主 = 这一读见证到的):
850
+ * · 判出属主 ⇒ {@link CLAIM_OWNERLESS_PLACED_SQL}(只 CAS 子行自己,绑扫描读到的化身;第二跑零命中);写成的按属主计进
851
851
  * `placedSessionOwnersOrphaned`(谁都不匹配:孤行哨兵,或继承自孤行祖先的同一只哨兵)或 `placedSessionOwnersInherited`,每一次提交当场记进收据账;零命中(这一行在扫描之后
852
852
  * 被写过 / 删过 / 换了住客)⇒ 不计,下一次起服按那时的律重判;
853
853
  * · 判出无主:读到的来源本身是一只 placed 行(还没定下的委派子代行)⇒ 计进 `placedSessionOwnersRemaining`(残余);来源是普通会话(根无主)⇒ 律就让它无主,不计;
854
854
  * · 扫到的任何一行化身为空 ⇒ 响亮拒(不论判不判得出属主;来源行同一条律)。
855
- * 不重扫、不拒启:残余由下一次起服或那一行的下一次停驻定下,期间只对 fleet 调用方放行(头注 M23 段)。
855
+ * 不重扫、不拒启:残余由下一次起服定下,期间只对 fleet 调用方放行(头注 M23 段)。
856
856
  */
857
857
  export declare function inheritPlacedSessionOwners(db: MigrationDb, dialect: SqlDialect, receipts: MigrationReceipts): Promise<void>;
858
858
  /**
@@ -404,17 +404,17 @@ export const ONE_TIME_MIGRATIONS = [
404
404
  introducedIn: "7.133.0",
405
405
  releasedOn: "2026-10-21",
406
406
  removableAfter: "2026-11-20",
407
- removal: "删 migrateOn 的 M23 步(`inheritPlacedSessionOwners`:placed ∧ 属主 NULL 的行按 session_id 键集分页单趟扫描 + 同一条语句连读根 / 父两行 + 按认领句回写属主)与 `PLACED_OWNER_SCAN_PAGE` / `placedOwnerScanSql` / `scannedPlacement` / `scannedFaces` / `scannedPlacedSources`(`isOrphanSessionOwner` 若再无别的读者一并删);" +
408
- "**同窗同删的另一半**(认领:按律给升级前写下的无主子代行写属主 —— 停驻挂点对宿主无主行做的就是本条对那一行的单行步):`delegated-session-owner.ts` 的 `CLAIM_OWNERLESS_PLACED_SQL` / `claimOwnerlessParams` / `OwnerlessClaim` / " +
409
- "`settleDelegatedOwner` 的认领一步(删后它只剩树比对与属主比对)/ `DelegationTreeStore.claimOwnerless`," +
410
- "两只 SQL 店(`tidb-session-store.ts` / `pg-session-storage.ts`)与 local 店(`local-session-store.ts` 的 `claimOwnerless` 字段与 `claimOwnerlessPlaced`)的 `claimOwnerless`、`CachingSessionStore` 的转发、`OwnerAwareSessionStore.claimOwnerless`、`delegatedOwnerHostOf` 对它的「与 `placementOf` 同在或同缺」;" +
407
+ removal: "删 migrateOn 的 M23 步(`inheritPlacedSessionOwners`:placed ∧ 属主 NULL 的行按 session_id 键集分页单趟扫描 + 同一条语句连读根 / 父两行 + 按认领句回写属主)与 `PLACED_OWNER_SCAN_PAGE` / `placedOwnerScanSql` / `scannedPlacement` / `scannedFaces` / `scannedPlacedSources`;" +
408
+ "**同窗同删的另一半**(旧律的判据与写句 —— 7.144.0 起建行与停驻挂点都不再读它们,M23 是唯一读者):`delegated-session-owner.ts` 头注 M23 一节与 `judgeDelegatedOwner` / `inheritedOwnerOf` / `ownerSourcesOf` / " +
409
+ "`orphanSessionOwner` / `isOrphanSessionOwner` / `SourceOwnerReading` / `InheritedOwner` / `PlacementSources` / `CLAIM_OWNERLESS_PLACED_SQL` / `claimOwnerlessParams`;" +
411
410
  "报告、日志行与 `NO_SQL_MIGRATIONS` 的 `placedSessionOwnersInherited` / `placedSessionOwnersOrphaned` / `placedSessionOwnersRemaining` 与 `placed_session_owners_orphaned` 那一行 warn;" +
412
411
  "`test/one-time-migrations.test.ts` 的 M23 组与 `test/one-time-migrations-db-integration.test.ts` 的 M23 格。" +
413
- "**不随本条删**:属主为 NULL 的 placed 行只对 fleet 调用方放行(`security.ts` 的 `ownerlessRowOpen` / `sessionOwnerCellOf` 与各门读 `placementOf` 的那一次)—— 那是常设的读面律",
412
+ "**不随本条删**:`ORPHAN_SESSION_OWNER_PREFIX` 与它在 `security.ts` 保留主体前缀里的登记(已落库的行可能带哨兵,永久保留);" +
413
+ "属主为 NULL 的 placed 行只对 fleet 调用方放行(`security.ts` 的 `ownerlessRowOpen` / `sessionOwnerCellOf` 与各门读 `placementOf` 的那一次)—— 那是常设的读面律",
414
414
  afterRemoval: "会话面的读面律(常设,不随本条删)接住:跳过了带本条的版本直接升的库里,属主为 NULL 的存量 placed 子代行只对 fleet 调用方放行(出示 service 凭据、不报主体;dev-open)," +
415
415
  "其余调用方答 404(与别人的行同体)—— 出路是升级时先过一次带本条的版本,或运维按 `placement_root_session_id`(缺则 `placement_parent_session_id`)那一行的属主手工回填、" +
416
- "推不出的写一个任何主体都不匹配的值;停驻挂点读到无主的宿主子代行 ⇒ 回到比对的既有臂:无主 ≠ 律给的属主 ⇒ 停驻否决(fail-closed,响亮;出路同上);" +
417
- "建行那一笔的继承律不随本条删(那是常设的写法,不是迁移)",
416
+ "推不出的写一个任何主体都不匹配的值;停驻挂点只比对宿主行属主与引擎交来的属主(复活腿交那一行自己的属主),不写无主行;" +
417
+ "建行那一笔记 `placement.owner` 不随本条删(那是常设的写法 —— core 盒义务 7,不是迁移)",
418
418
  reportKeys: ["placedSessionOwnersInherited", "placedSessionOwnersOrphaned", "placedSessionOwnersRemaining"],
419
419
  },
420
420
  {
@@ -571,7 +571,7 @@ export const ONE_TIME_MIGRATIONS = [
571
571
  "`local_archive_kept_stamped` / `local_archive_unreadable` 两行 warn、读数行 note 里的 M33 句与头注 M33 段;`test/local-archive-owner.test.ts` 里标题含 `P3` / `P4` / `P5` / `P6` / `F1` / `F2` / `F3` 的格与 `legacyRoot` / `writeArchive` / `boot` / `M33_ZERO` / `m33Of` 助手。" +
572
572
  "**不随本条删**:行格的 `createdAt` / `forkedFrom` 两键(常设的数据形)、`LocalArchiveSnapshotsPresentError` / `assertNoArchiveSnapshots` / `LOCAL_ARCHIVE_SNAPSHOT_DIR` 与 `LocalBackend.ensureSchema` 里调它的那一句、" +
573
573
  "`session-error-wire.ts` 的 `StampedSessionArchiveError` / `STAMPED_SESSION_ARCHIVE_SENTENCE` / `sessionErrorWireOf` 与本店两处带戳判(fork 的源、改日志推送的落点)、行上缺建档时刻的 F 类读法(`server.local-session-row.created-at-absent`)",
574
- afterRemoval: "三只常设臂接住:① 数据根下的快照目录非空 ⇒ 拒启(`config.local_archive_snapshot_present`,点名文件与两条出路);② 跳过了带本条的版本直接升的数据根(或本条留着的存档),带属主戳的存档被 fork 与改日志的推送同一只具名拒(409 `conflict` + 固定句,零写 / 行格净零写)," +
574
+ afterRemoval: "三只常设臂接住:① 数据根下的快照目录非空 ⇒ 拒启(`config.local_archive_snapshot_present`,点名文件与两条出路);② 跳过了带本条的版本直接升的数据根(或本条留着的存档),带属主戳的存档被 fork 与改日志的推送同一只具名拒(409 `conflict.session_archive_stamped` + 固定句,零写 / 行格净零写)," +
575
575
  "读 / 续跑 / 拉取照常 —— 出路 = 删掉会话后重推,或升级时先过一次带本条的版本并在窗内起服;③ 行上没有建档时刻的格(同一类数据根)⇒ 列表 / 会话视图 / 交出的元数据读纪元(F 类 tag 计数)",
576
576
  reportKeys: ["localArchiveRowFactsBackfilled", "localArchivesAnonymized", "localArchivesKeptStamped", "localArchivesUnreadable", "localArchiveSnapshotsRecovered"],
577
577
  },
@@ -1687,10 +1687,10 @@ export async function inheritPlacedSessionOwners(db, dialect, receipts) {
1687
1687
  const sessionId = String(r.session_id);
1688
1688
  after = sessionId;
1689
1689
  const incarnation = incarnationFromCell(sessionId, r.incarnation);
1690
- const judged = judgeDelegatedOwner(sessionId, scannedPlacement(r), scannedFaces(r));
1691
- const owner = judged.inherited.owner;
1690
+ const inherited = judgeDelegatedOwner(sessionId, scannedPlacement(r), scannedFaces(r));
1691
+ const owner = inherited.owner;
1692
1692
  if (owner === null) {
1693
- if (judged.inherited.kind === "inherited" && scannedPlacedSources(r).has(judged.inherited.from))
1693
+ if (inherited.kind === "inherited" && scannedPlacedSources(r).has(inherited.from))
1694
1694
  receipts.placedSessionOwnersRemaining += 1;
1695
1695
  continue;
1696
1696
  }
@@ -1858,7 +1858,7 @@ function logReceiptRows(log, backend, r) {
1858
1858
  ...(row.errno !== undefined ? { errno: row.errno } : {}),
1859
1859
  note: "a local session that has runs (and either no session-metadata cell — it was written before the cell existed and has not been read since — or a fork-created cell) has an archive the server cannot read, " +
1860
1860
  "so its execution placement could not be backfilled. The session was skipped and nothing was written or changed: boot is not blocked, and every request that needs the " +
1861
- "session keeps answering the archive's own fault (fault names it: corrupt_session = the session header cannot be read; invalid_session = the stored log's structure is broken; " +
1861
+ "session keeps answering the archive's own fault (fault names it: corrupt_session = the session header cannot be read, or the stored log does not form one tree; " +
1862
1862
  "storage = reading the archive file itself failed — errno names it, e.g. EACCES = no permission, EIO = disk I/O error, ERR_STRING_TOO_LONG = the file is too large to read) until the " +
1863
1863
  "archive file under the data root's sessions directory is readable again — restored from a kept copy, or (storage) its permissions or disk repaired; the next boot then backfills its placement as for any other session. " +
1864
1864
  "(Removing the archive file ends the warning but places nothing: the session then no longer exists on this server — except a fork-created session, whose cell stays and reads as a fork that has not landed: retry the same fork, or delete the session.) The session is named on every boot until then",
@@ -3,7 +3,6 @@ import type { Pool, PoolClient } from "pg";
3
3
  import type { SessionSummary } from "./store-contracts.js";
4
4
  import { type ClientSessionRowWrite, type ReplaySessionRowWrite, type ReplaySessionRowWriteOutcome, type SessionRowPatch, type SessionRowWriteOutcome } from "../session-row.js";
5
5
  import { type PlacementAcquireOpts } from "./session-placement.js";
6
- import { type OrphanOwnerLog } from "../delegated-session-owner.js";
7
6
  import { type StagingHandle } from "../session-sync-kernel.js";
8
7
  import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
9
8
  import type { SessionWorkspace } from "../session-workspace.js";
@@ -71,26 +70,19 @@ export declare class PgSessionStore implements SessionStore {
71
70
  readonly durability: "durable";
72
71
  /** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one. */
73
72
  private readonly pending;
74
- /** 委派子代孤行那一行 warn 的去处(TiDB 孪生同格)。 */
75
- private readonly log;
76
73
  /** 执行席(TiDB 孪生同格;实现与语句的唯一属主 = `execution-placement-sql.ts`,本店只交自己的池)。 */
77
74
  readonly executionPlacement: ExecutionPlacementSeat;
78
75
  /** 本店的池包成的 SqlDriver(TiDB 孪生同格):执行席与会话行格写原语的事务都经它借连接。 */
79
76
  private readonly db;
80
77
  constructor(pool: Pool, opts?: {
81
- readonly log?: OrphanOwnerLog;
82
78
  readonly retention?: RetentionDeclaration;
83
79
  });
84
80
  acquire(sessionId?: string, opts?: PlacementAcquireOpts): Promise<AcquiredSession>;
85
81
  private load;
86
82
  /** Idempotently create a session_meta row with an owner (core `SessionStore.register`). Existing owner is never overwritten. */
87
83
  register(sessionId: string, owner: string | null): Promise<void>;
88
- /** acquire 的建行(唯一一处;[ref] 委派继承律,只住在 `createPlacedSessionRow`,本店交既有读写口 —— TiDB 孪生同名方法同注)。 */
84
+ /** acquire 的建行(唯一一处;placed 会话的属主 = `placement.owner` 原样,core 7.62.0 [ref] 盒义务 7 —— TiDB 孪生同名方法同注)。 */
89
85
  private createOnAcquire;
90
- /** [ref] 委派继承律的认领口(TiDB 孪生 {@link TiDBSessionStore.claimOwnerless} 同义;句 = `CLAIM_OWNERLESS_PLACED_SQL`,规则在 `delegated-session-owner.ts`)。 */
91
- claimOwnerless(sessionId: string, incarnation: string, owner: string): Promise<boolean>;
92
- /** 若干来源行的会话面(属主 + 化身)一次读回(TiDB 孪生同义;活住客的投影 `sessionFaceFromRow`,空化身 ⇒ 响亮拒)。 */
93
- private sourceFaces;
94
86
  /** The one session_meta INSERT. Existing owner is never overwritten.
95
87
  * [ref]:`placement` 与行同一条 INSERT;`DO NOTHING` 臂 ⇒ 已存在行的元组永不被重写(first-write 不可变)。 */
96
88
  private insertMeta;
@@ -10,8 +10,6 @@ import { pgSafeJsonStringify } from "./pg-safe-json.js";
10
10
  import { toIso } from "./sql-row-helpers.js";
11
11
  import { placementCapabilityOf, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SESSION_DURABILITY, SESSION_PLACEMENTS, } from "./session-placement.js";
12
12
  import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
13
- import { CLAIM_OWNERLESS_PLACED_SQL, claimOwnerlessParams, createPlacedSessionRow } from "../delegated-session-owner.js";
14
- import { createLogger } from "../observability/logger.js";
15
13
  import { classifySyncRelationshipByIds, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
16
14
  import { isPgUniqueViolation } from "./sql-errors.js";
17
15
  import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
@@ -19,7 +17,7 @@ import { SESSION_META_WORKSPACE_COLUMN, decodeSessionWorkspaceCell, encodeSessio
19
17
  import { MANAGED_RETENTION, UNMANAGED_RETENTION } from "./retention-store-sql.js";
20
18
  import { SESSION_META_INCARNATION_COLUMN, carriedRowCells, headFromRow, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
21
19
  import { ensureIndexes } from "./ensure-index.js";
22
- import { assertFirstOwnerWritable, forkOwnerOf, isSessionOwnerValue } from "./session-ownership.js";
20
+ import { assertFirstOwnerWritable, forkOwnerOf, isSessionOwnerValue, placedSessionOwnerOf } from "./session-ownership.js";
23
21
  import { selectForkRows } from "@sema-agent/core";
24
22
  import { FORK_LOG_ROW_SHAPE, ForkTargetUnsettledError, classifyForkTarget, forkByFreshId, forkPointOf } from "./session-fork.js";
25
23
  import { pgDriver } from "./sql-driver.js";
@@ -191,12 +189,10 @@ export class PgSessionStore {
191
189
  placements = SESSION_PLACEMENTS;
192
190
  durability = SESSION_DURABILITY;
193
191
  pending = new Map();
194
- log;
195
192
  executionPlacement;
196
193
  db;
197
194
  constructor(pool, opts = {}) {
198
195
  this.pool = pool;
199
- this.log = opts.log ?? createLogger();
200
196
  this.retention = opts.retention ?? UNMANAGED_RETENTION;
201
197
  this.db = pgDriver(pool);
202
198
  this.executionPlacement = createSqlExecutionPlacementSeat(this.db);
@@ -244,20 +240,7 @@ export class PgSessionStore {
244
240
  await this.insertMeta(sessionId, owner, undefined);
245
241
  }
246
242
  async createOnAcquire(sessionId, placement) {
247
- if (placement === undefined)
248
- return this.insertMeta(sessionId, null, undefined);
249
- await createPlacedSessionRow(sessionId, placement, {
250
- readFaces: (ids) => this.sourceFaces(ids),
251
- insert: (owner) => this.insertMeta(sessionId, owner, placement),
252
- readOwner: async () => (await this.headOf(sessionId))?.owner,
253
- }, this.log);
254
- }
255
- async claimOwnerless(sessionId, incarnation, owner) {
256
- return ((await this.pool.query(CLAIM_OWNERLESS_PLACED_SQL.pg, [...claimOwnerlessParams(sessionId, incarnation, owner)])).rowCount ?? 0) > 0;
257
- }
258
- async sourceFaces(ids) {
259
- const res = await this.pool.query(`SELECT session_id, owner, incarnation FROM session_meta WHERE session_id IN (${ids.map((_, i) => `$${i + 1}`).join(", ")})`, [...ids]);
260
- return new Map(res.rows.map((r) => [String(r.session_id), sessionFaceFromRow(String(r.session_id), r.owner ?? null, r.incarnation)]));
243
+ await this.insertMeta(sessionId, placement === undefined ? null : placedSessionOwnerOf(sessionId, placement), placement);
261
244
  }
262
245
  async insertMeta(sessionId, owner, placement) {
263
246
  const now = new Date();
@@ -71,11 +71,8 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
71
71
  cwd: string;
72
72
  /** core `ExecutionEnv.homeDir`([ref]):本机执行用户的 home —— 主张在场 ⇒ 主张(构造时与子进程拿到的 `HOME` 对证过),否则构造时读到的那个 `HOME`,见 {@link LaneHomeSeat}。 */
73
73
  get homeDir(): string | undefined;
74
- /** 家目录的座(主张在场 ⇒ 构造时对证 · 否则构造时实测)。读数 = {@link childHome}:本类自己组合给子进程的那份 env,不 spawn。 */
74
+ /** 家目录的座(主张在场 ⇒ 构造时对证,证不了 ⇒ 构造抛、环境不铸 · 否则构造时实测)。读数 = {@link childHome}:本类自己组合给子进程的那份 env,不 spawn。 */
75
75
  private readonly homeSeat;
76
- /** 家目录:主张与子进程拿到的 `HOME` 对不上 ⇒ 这只环境的拒句;`shellReady` / `fsReady` / `connect` 三道门原样回它,一条命令、一次文件动作都不做。
77
- * 读数是本类自己组合的 env(纯函数,同一份输入同一个答案)⇒ 构造时判一次、记一辈子。 */
78
- private readonly homeRefusal;
79
76
  /**
80
77
  * core 7.49.0 [ref]:shell 子进程**真拿到的** `CDPATH` 条目(`[]` = 没有)—— 删除守卫按它解裸 `cd <dir>`。本车道的命令就是本机子进程,
81
78
  * env 是本类自己组合的(继承姿态 + `cfg.env`),所以说得出:读同一只组合({@link childEnv} 的**读面**,不披露、不钉 shell),切法与 core
@@ -84,6 +81,13 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
84
81
  * {@link hostShell}(win32 上未落定同步抛 —— 修前每条 Bash 的策略链都因此被拒)。
85
82
  */
86
83
  get cdPath(): readonly string[];
84
+ /**
85
+ * core 7.62.0 [ref]([ref]):shell 子进程**收到的**环境变量名(从不带值)—— 引擎判「git 会不会读一只文本没绑的 `GIT_*`」的唯一来源(声明且没有 `GIT_*`
86
+ * ⇒ `git show HEAD:<path>` 的对象路径只在仓库顶判;缺席 ⇒ 每层祖先照判)。与 {@link cdPath} 同一只组合的读面(继承姿态 + `cfg.env`,不 spawn、不记剥除、不碰 shell);
87
+ * 逐命令的 `options.env` 由引擎自己交,它知道(core 残余:逐命令 env 加一个名)。win32 spawn 层钉的 `SHELL` 不在这一读里(不是 `GIT_*`,不改这一判)。
88
+ * 只有本车道声明:远端车道的 shell 跑在对方自己构建的环境里,适配器说不出(缺席)。
89
+ */
90
+ get receivedEnvNames(): readonly string[];
87
91
  /**
88
92
  * core 7.56.0 [ref]:shell 子进程继承的环境里**收不到**的条目(名 + 规则,从不带值)—— 与 {@link cdPath} 同一只组合的读面
89
93
  * (`server-secret-env.ts` 的 `withheldFromHostSubprocess`:继承组合扣下的发现,减去 `cfg.env` 这条显式授予重供的键)。逐命令的 `options.env`