@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
@@ -1,10 +1,10 @@
1
1
  import { parkedApprovalCard } from "../approval-content-kind.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { CHECKPOINT_FACET_SQL, CHECKPOINT_SWEEP_ORDER_SQL, CheckpointIndexRefusedError, judgeCheckpointIndexCells } from "./checkpoint-index-columns.js";
4
- import { CheckpointError, isGateKind, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, winnerFromOutcome, summarizeCheckpoint, checkpointRevOf, MAX_SUPPORTED_CHECKPOINT_VERSION, ExecutionOutcomeConflictError, executionRecordDisposition, executionVerdict, } from "@sema-agent/core";
4
+ import { CheckpointError, isGateKind, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, winnerFromOutcome, summarizeCheckpoint, checkpointRevOf, judgeCheckpointRow, CONTRACT_KIT_ENGINE_VERSION, ExecutionOutcomeConflictError, executionRecordDisposition, executionVerdict, } from "@sema-agent/core";
5
5
  import { redactDeep, redactSecrets } from "../trace/redact.js";
6
- import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, countKindlessBatchOffers, parkRowCarriesSkillCardFacts, parkRowFacts, readRuleOfferList, skillCardFacts } from "../approval-card.js";
7
- import { assertJsonColumnWritable, createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readJsonArrayColumn, readJsonObjectColumn, readNonNullJsonValueColumn, } from "./sql-json-column.js";
6
+ import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, countKindlessBatchOffers, parkRowFacts, readRuleOfferList, skillCardFacts } from "../approval-card.js";
7
+ import { assertJsonColumnWritable, createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, jsonColumnTextSha256, readJsonArrayColumn, readJsonColumnTextSha256, readJsonObjectColumn, readNonNullJsonValueColumn, } from "./sql-json-column.js";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { durableRowIdentity, withoutCheckpointToken } from "../checkpoint-identity.js";
10
10
  import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.js";
@@ -21,6 +21,35 @@ export const TERMINAL_GRACE_MS = 3_600_000;
21
21
  export function tokenFingerprint(token) {
22
22
  return "sha256:" + createHash("sha256").update(token).digest("hex").slice(0, 12);
23
23
  }
24
+ const STORED_ROW_MEMO_CAP = 8192;
25
+ class StoredRowProjectionMemo {
26
+ rows = new Map();
27
+ keyOf(sha256) {
28
+ return `${sha256}@${CONTRACT_KIT_ENGINE_VERSION}`;
29
+ }
30
+ get(sha256) {
31
+ const key = this.keyOf(sha256);
32
+ const hit = this.rows.get(key);
33
+ if (hit !== undefined) {
34
+ this.rows.delete(key);
35
+ this.rows.set(key, hit);
36
+ }
37
+ return hit;
38
+ }
39
+ set(sha256, projection) {
40
+ const key = this.keyOf(sha256);
41
+ this.rows.delete(key);
42
+ this.rows.set(key, projection);
43
+ while (this.rows.size > STORED_ROW_MEMO_CAP) {
44
+ const oldest = this.rows.keys().next().value;
45
+ if (oldest === undefined)
46
+ break;
47
+ this.rows.delete(oldest);
48
+ }
49
+ }
50
+ }
51
+ const STORED_ROW_READ_BATCH = 100;
52
+ const CHECKPOINT_SHA256_AS = "checkpoint_sha256";
24
53
  const GATE_KIND_IS_LIVE_RUN_PARK = {
25
54
  human: true,
26
55
  irreversible_ask: true,
@@ -236,7 +265,7 @@ export function approvalDecisionOfWinner(winner, boundCallId) {
236
265
  }
237
266
  }
238
267
  }
239
- export function pendingActionToolCallId(body) {
268
+ function pendingActionToolCallId(body) {
240
269
  if (typeof body !== "object" || body === null)
241
270
  return { readable: false };
242
271
  if ("sourceTaskId" in body)
@@ -265,17 +294,22 @@ export function pendingActionToolCallId(body) {
265
294
  return { readable: false };
266
295
  return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceSessionId };
267
296
  }
268
- function pendingActionOfColumn(cell, where) {
269
- let body;
297
+ export function candidateIdentityOf(cp) {
298
+ if (judgeCheckpointRow(cp).kind === "lacking")
299
+ return { readable: false };
300
+ return pendingActionToolCallId(cp);
301
+ }
302
+ function candidateIdentityOfColumn(cell, where) {
303
+ let cp;
270
304
  try {
271
- body = decodeJsonColumn(cell, readJsonObjectColumn, `checkpoint.checkpoint ${where}`);
305
+ cp = decodeCheckpointColumn(cell, where);
272
306
  }
273
307
  catch (e) {
274
308
  if (!isJsonColumnBodyProblem(e))
275
309
  throw e;
276
310
  return { readable: false };
277
311
  }
278
- return pendingActionToolCallId(body);
312
+ return candidateIdentityOf(cp);
279
313
  }
280
314
  function readActorAssertion(v) {
281
315
  if (typeof v !== "object" || v === null)
@@ -346,6 +380,7 @@ export class SqlCheckpointStore {
346
380
  fidelity = "json";
347
381
  execution = { outcome: true };
348
382
  claims;
383
+ storedRowMemo = new StoredRowProjectionMemo();
349
384
  constructor(db, logger, opts = {}) {
350
385
  this.db = db;
351
386
  this.logger = logger;
@@ -433,16 +468,12 @@ export class SqlCheckpointStore {
433
468
  }
434
469
  }
435
470
  async get(token) {
436
- const { rows } = await this.db.query(this.q(`SELECT version, status, ${jsonColumnText("mysql", "checkpoint")}, ${jsonColumnText("mysql", "outcome")}, reopen_reason, rev, ` +
437
- `${jsonColumnText("mysql", "pending_steer_queue")}, ${jsonColumnText("mysql", "execution_outcome")}, execution_at_ms FROM checkpoint WHERE token = ?`, `SELECT version, status, ${jsonColumnText("pg", "checkpoint")}, ${jsonColumnText("pg", "outcome")}, reopen_reason, rev, ` +
471
+ const { rows } = await this.db.query(this.q(`SELECT status, ${jsonColumnText("mysql", "checkpoint")}, ${jsonColumnText("mysql", "outcome")}, reopen_reason, rev, ` +
472
+ `${jsonColumnText("mysql", "pending_steer_queue")}, ${jsonColumnText("mysql", "execution_outcome")}, execution_at_ms FROM checkpoint WHERE token = ?`, `SELECT status, ${jsonColumnText("pg", "checkpoint")}, ${jsonColumnText("pg", "outcome")}, reopen_reason, rev, ` +
438
473
  `${jsonColumnText("pg", "pending_steer_queue")}, ${jsonColumnText("pg", "execution_outcome")}, execution_at_ms FROM checkpoint WHERE token = $1`), [token]);
439
474
  const r = rows[0];
440
475
  if (!r)
441
476
  return null;
442
- const version = Number(r.version);
443
- if (version > MAX_SUPPORTED_CHECKPOINT_VERSION) {
444
- throw new Error(`checkpoint format version ${version} is newer than this build supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION}) — upgrade the service to resume it`);
445
- }
446
477
  const cp = refusing("a checkpoint nobody can resume", () => decodeCheckpointColumn(r.checkpoint, `for token ${tokenFingerprint(token)}`));
447
478
  cp.status = r.status;
448
479
  const outcomeRaw = refusing("a decision record nobody can read", () => decodeOutcomeColumn(r.outcome, `for token ${tokenFingerprint(token)}`));
@@ -468,7 +499,6 @@ export class SqlCheckpointStore {
468
499
  this.logger?.info?.("checkpoint_get", {
469
500
  tokenFp: tokenFingerprint(token),
470
501
  status: cp.status,
471
- version,
472
502
  hasWorkspaceHandle: !!cp.state?.workspaceHandle,
473
503
  snapshotId: cp.state?.workspaceHandle?.snapshotId,
474
504
  });
@@ -558,7 +588,8 @@ export class SqlCheckpointStore {
558
588
  const d = this.db.dialect;
559
589
  const base = `SELECT c.session_id, c.token, c.scope, c.tool_name, c.tool_call_id, ${jsonColumnText(d, "tool_input")}, c.bound_input_hash, ` +
560
590
  `${jsonColumnText(d, "risk_descriptor")}, ${jsonColumnText(d, "rule_suggestions")}, c.has_bidi_controls, c.created_at_ms, c.deadline, c.gate_kind, c.approval_card, ta.task_id, ` +
561
- `${jsonColumnText(d, "checkpoint", PARK_ROW_PENDING_ACTION)}, ${jsonColumnText(d, "checkpoint", PARK_ROW_CHECKPOINT_ID)} ` +
591
+ `${jsonColumnText(d, "checkpoint", PARK_ROW_PENDING_ACTION)}, ${jsonColumnText(d, "checkpoint", PARK_ROW_CHECKPOINT_ID)}, ` +
592
+ `${jsonColumnTextSha256(d, "checkpoint", CHECKPOINT_SHA256_AS)} ` +
562
593
  "FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
563
594
  const { rows } = scope
564
595
  ? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at_ms ASC`, [scope])
@@ -592,43 +623,77 @@ export class SqlCheckpointStore {
592
623
  ...(approvalCard !== undefined ? { approvalCard } : {}),
593
624
  ...parkRowFacts(pendingAction),
594
625
  };
595
- return [{ row, token: String(r.token), readSkillCard: parkRowCarriesSkillCardFacts(pendingAction) }];
626
+ return [{ row, token: String(r.token), sha256: readJsonColumnTextSha256(r[CHECKPOINT_SHA256_AS], "checkpoint.checkpoint_sha256 (operator queue row)") }];
627
+ });
628
+ const projections = await this.storedRowProjections(entries);
629
+ const out = entries.map((e) => {
630
+ const p = projections.get(e.token);
631
+ return p === undefined ? e.row : { ...e.row, ...p.skillCardFacts, rowVerdict: p.rowVerdict };
596
632
  });
597
- const out = [];
598
- for (const e of entries)
599
- out.push(e.readSkillCard ? { ...e.row, ...(await this.readParkedSkillCardFacts(e.token)) } : e.row);
600
633
  return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
601
634
  }
602
- async readParkedSkillCardFacts(token) {
603
- const { rows } = await this.db.query(this.q(`SELECT ${jsonColumnText("mysql", "checkpoint")} FROM checkpoint WHERE token = ? AND status = 'pending'`, `SELECT ${jsonColumnText("pg", "checkpoint")} FROM checkpoint WHERE token = $1 AND status = 'pending'`), [token]);
604
- const r = rows[0];
605
- if (r === undefined)
606
- return {};
635
+ async storedRowProjections(rows) {
636
+ const out = new Map();
637
+ const misses = [];
638
+ const expected = new Map();
639
+ for (const { token, sha256 } of rows) {
640
+ expected.set(token, sha256);
641
+ const hit = this.storedRowMemo.get(sha256);
642
+ if (hit === undefined)
643
+ misses.push(token);
644
+ else
645
+ out.set(token, hit);
646
+ }
647
+ for (let i = 0; i < misses.length; i += STORED_ROW_READ_BATCH) {
648
+ const batch = misses.slice(i, i + STORED_ROW_READ_BATCH);
649
+ const { rows: read } = await this.db.query(this.q(`SELECT token, ${jsonColumnText("mysql", "checkpoint")}, ${jsonColumnTextSha256("mysql", "checkpoint", CHECKPOINT_SHA256_AS)} FROM checkpoint ` +
650
+ `WHERE status = 'pending' AND token IN (${batch.map(() => "?").join(", ")})`, `SELECT token, ${jsonColumnText("pg", "checkpoint")}, ${jsonColumnTextSha256("pg", "checkpoint", CHECKPOINT_SHA256_AS)} FROM checkpoint ` +
651
+ `WHERE status = 'pending' AND token IN (${batch.map((_, j) => `$${j + 1}`).join(", ")})`), batch);
652
+ for (const r of read) {
653
+ const token = String(r.token);
654
+ const sha256 = readJsonColumnTextSha256(r[CHECKPOINT_SHA256_AS], "checkpoint.checkpoint_sha256 (whole-row read)");
655
+ const projection = this.projectStoredRow(r, sha256);
656
+ if (projection !== undefined && sha256 === expected.get(token))
657
+ out.set(token, projection);
658
+ }
659
+ }
660
+ return out;
661
+ }
662
+ projectStoredRow(r, sha256) {
607
663
  let cp;
608
664
  try {
609
- cp = decodeCheckpointColumn(r.checkpoint, "(operator queue row, skill-card facts)");
665
+ cp = decodeCheckpointColumn(r.checkpoint, "(operator queue row, whole-row read)");
610
666
  }
611
667
  catch (e) {
612
668
  if (!isJsonColumnBodyProblem(e))
613
669
  throw e;
614
670
  recordFailOpen("server.approvals.park-row-blob-unreadable");
615
- return {};
671
+ return undefined;
616
672
  }
673
+ let summary;
617
674
  try {
618
- return skillCardFacts(summarizeCheckpoint(cp));
675
+ summary = summarizeCheckpoint(cp);
619
676
  }
620
677
  catch {
621
678
  recordFailOpen("server.approvals.park-row-blob-unreadable");
622
- return {};
679
+ return { rowVerdict: judgeCheckpointRow(cp), skillCardFacts: {} };
623
680
  }
681
+ const projection = { rowVerdict: summary.rowVerdict, skillCardFacts: skillCardFacts(summary) };
682
+ this.storedRowMemo.set(sha256, projection);
683
+ return projection;
624
684
  }
625
685
  async listByScope(scope) {
626
- const { rows } = await this.db.query(this.q(`SELECT ${jsonColumnText("mysql", "checkpoint")}, status FROM checkpoint WHERE status = 'pending' AND scope = ? ORDER BY created_at_ms ASC LIMIT 500`, `SELECT ${jsonColumnText("pg", "checkpoint")}, status FROM checkpoint WHERE status = 'pending' AND scope = $1 ORDER BY created_at_ms ASC LIMIT 500`), [scope]);
686
+ const { rows } = await this.db.query(this.q(`SELECT ${jsonColumnText("mysql", "checkpoint")}, ${jsonColumnTextSha256("mysql", "checkpoint", CHECKPOINT_SHA256_AS)}, status FROM checkpoint WHERE status = 'pending' AND scope = ? ORDER BY created_at_ms ASC LIMIT 500`, `SELECT ${jsonColumnText("pg", "checkpoint")}, ${jsonColumnTextSha256("pg", "checkpoint", CHECKPOINT_SHA256_AS)}, status FROM checkpoint WHERE status = 'pending' AND scope = $1 ORDER BY created_at_ms ASC LIMIT 500`), [scope]);
627
687
  const out = [];
628
688
  for (const r of rows) {
689
+ const sha256 = readJsonColumnTextSha256(r[CHECKPOINT_SHA256_AS], `checkpoint.checkpoint_sha256 in the ${scope} scheduler view`);
629
690
  const cp = refusing("a checkpoint nobody can resume", () => decodeCheckpointColumn(r.checkpoint, `in the ${scope} scheduler view`));
630
691
  cp.status = r.status;
631
- out.push(summarizeCheckpoint(cp, (sentence) => recordFailOpen("server.checkpoint.inbox-risk-axes-damaged", withoutCheckpointToken(sentence, cp.token))));
692
+ const memo = this.storedRowMemo.get(sha256);
693
+ const summary = summarizeCheckpoint(cp, (sentence) => recordFailOpen("server.checkpoint.inbox-risk-axes-damaged", withoutCheckpointToken(sentence, cp.token)), memo?.rowVerdict);
694
+ if (memo === undefined)
695
+ this.storedRowMemo.set(sha256, { rowVerdict: summary.rowVerdict, skillCardFacts: skillCardFacts(summary) });
696
+ out.push(summary);
632
697
  }
633
698
  return out;
634
699
  }
@@ -642,8 +707,8 @@ export class SqlCheckpointStore {
642
707
  return rows[0] ? String(rows[0].token) : null;
643
708
  }
644
709
  async findCheckpointCandidatesForAsk(scope, sessionId, toolCallId, sinceMs) {
645
- const { rows } = await this.db.query(this.q(`SELECT token, status, created_at_ms, tool_call_id, bound_input_hash, version, ${jsonColumnText("mysql", "checkpoint")} FROM checkpoint ` +
646
- "WHERE scope=? AND session_id=? AND created_at_ms>=? AND (tool_call_id=? OR tool_call_id IS NULL) ORDER BY created_at_ms ASC", `SELECT token, status, created_at_ms, tool_call_id, bound_input_hash, version, ${jsonColumnText("pg", "checkpoint")} FROM checkpoint ` +
710
+ const { rows } = await this.db.query(this.q(`SELECT token, status, created_at_ms, tool_call_id, bound_input_hash, ${jsonColumnText("mysql", "checkpoint")} FROM checkpoint ` +
711
+ "WHERE scope=? AND session_id=? AND created_at_ms>=? AND (tool_call_id=? OR tool_call_id IS NULL) ORDER BY created_at_ms ASC", `SELECT token, status, created_at_ms, tool_call_id, bound_input_hash, ${jsonColumnText("pg", "checkpoint")} FROM checkpoint ` +
647
712
  "WHERE scope=$1 AND session_id=$2 AND created_at_ms>=$3 AND (tool_call_id=$4 OR tool_call_id IS NULL) ORDER BY created_at_ms ASC"), [scope, sessionId, sinceMs, toolCallId]);
648
713
  const out = [];
649
714
  for (const r of rows) {
@@ -654,11 +719,7 @@ export class SqlCheckpointStore {
654
719
  boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
655
720
  sourceSessionId: null,
656
721
  };
657
- if (Number(r.version) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
658
- out.push({ ...base, boundCallId: null, unparseable: true });
659
- continue;
660
- }
661
- const derived = pendingActionOfColumn(r.checkpoint, `for token ${tokenFingerprint(String(r.token))}`);
722
+ const derived = candidateIdentityOfColumn(r.checkpoint, `for token ${tokenFingerprint(String(r.token))}`);
662
723
  if (!derived.readable) {
663
724
  out.push({ ...base, boundCallId: null, unparseable: true });
664
725
  continue;
@@ -32,7 +32,7 @@
32
32
  * 路由表 LRU 上限 {@link ROUTE_MAX}:满额逐最老(refresh-on-use);被逐后下次 `acquire` 按上述规则重derive
33
33
  * (requireExisting 的 host-first 重试仍落对店),中途 touch/noteTaskRun 缺路由=落 transient(no-op 级)。
34
34
  */
35
- import { type SessionStore, type AcquiredSession, type SessionStoreSummary, type SessionPlacement, type PlacedSessionRow } from "@sema-agent/core";
35
+ import { type SessionStore, type AcquiredSession, type SessionHead, type SessionStoreSummary, type SessionPlacement, type PlacedSessionRow } from "@sema-agent/core";
36
36
  import type { ExecutionPlacementSeat } from "../execution-placement.js";
37
37
  import type { ForkToFn } from "./session-fork.js";
38
38
  /** acquire 选项形(core 5.48 SessionStore 契约):[ref] placement=受信 spawner 的分区建会话声明。 */
@@ -63,6 +63,13 @@ export declare class ForkRoutingSessionStore implements SessionStore {
63
63
  readonly executionPlacement?: ExecutionPlacementSeat;
64
64
  /** fork 续写的店契约透传(host 有才有):目标在 host 店原子新建,新 id 与源一并记 host 路由(与 {@link fork} 的 host 腿同)。 */
65
65
  readonly forkTo?: ForkToFn;
66
+ /**
67
+ * core `SessionStore.headOf`(按 id 读一行:属主 / 住客 / 叶子)—— **按 acquire 的路由转发**({@link pick}:placed 名册 → 路由表 → 未知落 transient,
68
+ * 与 acquire 对同一只 id 落的店同一只),所以答的就是 acquire 交出的那只会话所在的行(core 7.62.0 [ref] 盒义务 7:声明 placements 的店答 `headOf`,
69
+ * 住客与 acquire 交出的同源;引擎在子 Runner 上据它读「没人派生的 run」自己那一行定委派树属主)。两只店都答 `headOf` 才声明:有一边答不了 ⇒ 不声明
70
+ * (半只读口会把那一边的行答成「不在」)。
71
+ */
72
+ readonly headOf?: (sessionId: string) => Promise<SessionHead | undefined>;
66
73
  constructor(host: SessionStore, transient: SessionStore);
67
74
  private remember;
68
75
  private pick;
@@ -11,6 +11,7 @@ export class ForkRoutingSessionStore {
11
11
  listPlaced;
12
12
  executionPlacement;
13
13
  forkTo;
14
+ headOf;
14
15
  constructor(host, transient) {
15
16
  this.host = host;
16
17
  this.transient = transient;
@@ -23,6 +24,11 @@ export class ForkRoutingSessionStore {
23
24
  if (host.fork === undefined && transient.fork === undefined) {
24
25
  this.fork = undefined;
25
26
  }
27
+ const hostHeadOf = host.headOf;
28
+ const transientHeadOf = transient.headOf;
29
+ if (typeof hostHeadOf === "function" && typeof transientHeadOf === "function") {
30
+ this.headOf = (id) => (this.pick(id) === host ? hostHeadOf.call(host, id) : transientHeadOf.call(transient, id));
31
+ }
26
32
  const hostFaces = host;
27
33
  if (hostFaces.executionPlacement !== undefined)
28
34
  this.executionPlacement = hostFaces.executionPlacement;
@@ -147,11 +147,9 @@ export declare class LocalCheckpointStore {
147
147
  * · **没有列,所以没有列/blob 一致性门**:SQL 孪生的两道矛盾门(`tool_call_id` 与 `bound_input_hash`
148
148
  * 的列 vs blob)在本形结构上不存在 —— 本形只有 blob 这一个真源,谈不上两份互相矛盾。判别力因此**不弱**:
149
149
  * 那两道门防的是「外部改库 / schema 偏斜让列与 blob 分家」,而本形没有可分家的第二份。
150
- * · **没有行级版本闸**:本类 `peekScopeByToken` 顶注已亲核过这条事实 —— 本形的 `get()` 没有行级版本闸,
151
- * 唯一的读不动形是账本重放级(整店同时读不动)。所以 SQL 孪生那道 `version > MAX_SUPPORTED` 的
152
- * `unparseable` 臂在这里没有对应物;本口的 `unparseable` 只由**窄读器**判(blob 形不合)。
153
- * · **窄读器是同一只** `pendingActionToolCallId`(与 SQL 孪生共用,零第二份手抄):身份三元组的第一维
154
- * `sourceSessionId`、`boundInputHash`、以及「读不出 vs 确定不匹配」的三态,全部由它一家判。
150
+ * · **身份判词是同一只** `candidateIdentityOf`(与 SQL 孪生共用,零第二份手抄):先问 core 判官(答 `lacking` ⇒ `unparseable`,
151
+ * 7.144.0 起两只孪生同一臂 —— 修前只有 SQL 孪生按版本列比,本形没有对应物),再由窄读器判身份三元组的第一维 `sourceSessionId`、
152
+ * `boundInputHash`、以及「读不出 vs 确定不匹配」的三态。
155
153
  */
156
154
  findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
157
155
  /**
@@ -159,9 +157,9 @@ export declare class LocalCheckpointStore {
159
157
  * 语义与存在理由的唯一属主 = SQL 侧 `peekScopeByToken` 顶注。
160
158
  *
161
159
  * ⚠️ 形差如实记:SQL 孪生那口是真「轻读」(只 SELECT scope 一列),本形没有列可选 —— core 的 File 店
162
- * 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()` 没有行级版本闸(唯一的
163
- * 读不动形是账本重放级 = 整店同时读不动,`findPendingTokenBySession` 先抛),所以「行体读得出而 scope
164
- * 读不出」这一支在本形结构上不存在。哪天本形补了行级闸,这里要跟着改成不碰行体的读。
160
+ * 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()` 没有行级拒(两只孪生 7.144.0 起都不按版本拒 ——
161
+ * 本引擎续不续由 core 判官在续跑时答;本形连 SQL 孪生剩下的坏体一臂也没有,唯一的读不动形是账本重放级 = 整店同时读不动,
162
+ * `findPendingTokenBySession` 先抛),所以「行体读得出而 scope 读不出」这一支在本形结构上不存在。哪天本形补了行级拒,这里要跟着改成不碰行体的读。
165
163
  */
166
164
  peekScopeByToken(token: CheckpointToken): Promise<string | null | undefined>;
167
165
  /** `undefined` = no pending row; else the owner scope (local scopes are always strings — never null).
@@ -6,7 +6,7 @@ import { createHash } from "node:crypto";
6
6
  import { existsSync, readFileSync, readdirSync, unlinkSync, mkdirSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { FileCheckpointStore, atomicWriteFile, executionVerdict, sanitizePathComponent, } from "@sema-agent/core";
9
- import { approvalDecisionOfWinner, approvalPayloadFingerprint, boundedRuleOffers, boundedToolInput, pendingActionToolCallId, pendingRowPrecedes, queueRowGateKind, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
9
+ import { approvalDecisionOfWinner, approvalPayloadFingerprint, boundedRuleOffers, boundedToolInput, candidateIdentityOf, pendingRowPrecedes, queueRowGateKind, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
10
10
  import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
11
11
  import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, readStringArrayColumn } from "./sql-json-column.js";
12
12
  import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
@@ -194,6 +194,7 @@ export class LocalCheckpointStore {
194
194
  ...((card) => (card !== undefined ? { approvalCard: card } : {}))(parkedApprovalCard(cp.gate)),
195
195
  ...parkRowFacts(cp.pendingAction),
196
196
  ...skillCardFacts(summary),
197
+ rowVerdict: summary.rowVerdict,
197
198
  });
198
199
  }
199
200
  return rows.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
@@ -276,7 +277,7 @@ export class LocalCheckpointStore {
276
277
  if (cp.scope !== scope || cp.sessionId !== sessionId || cp.createdAt < sinceMs)
277
278
  continue;
278
279
  const base = { token: t, status: cp.status, createdAtMs: cp.createdAt };
279
- const derived = pendingActionToolCallId(cp);
280
+ const derived = candidateIdentityOf(cp);
280
281
  if (!derived.readable) {
281
282
  out.push({ ...base, sourceSessionId: null, boundCallId: null, boundInputHash: null, unparseable: true });
282
283
  continue;
@@ -1,5 +1,4 @@
1
1
  import { FileSessionRepo, type AcquiredSession, type PlacedSessionRow, type SessionHead, type SessionPlacementRecord, type SessionStore, type SessionRepo, type SessionMetadata, type SessionTreeEntry, type StoreDurability } from "@sema-agent/core";
2
- import { type OwnerlessClaim } from "../delegated-session-owner.js";
3
2
  import { type PlacementAcquireOpts } from "./session-placement.js";
4
3
  import { z } from "zod";
5
4
  import { StoreBootRefusalError, type SessionRunClaimSource, type SessionRunRollupSource, type SessionSummary } from "./store-contracts.js";
@@ -30,11 +29,39 @@ export declare const SESSION_META_CELL_OBJECT: z.ZodObject<{
30
29
  createdAt: z.ZodOptional<z.ZodString>;
31
30
  forkedFrom: z.ZodOptional<z.ZodString>;
32
31
  }, z.core.$strict>;
32
+ /** 严格边档清册里一只边档的对账读数(拒句的逐文件一段;{@link LocalDataRootSchemaMismatchError.findings})。 */
33
+ export interface LocalSidecarMismatch {
34
+ /** 文件的绝对路径。 */
35
+ readonly path: string;
36
+ /** 角色(`<文件名> (the local <角色> sidecar)`)。 */
37
+ readonly label: string;
38
+ /** 读不出的种别:`not-json` / `malformed`(形),或 I/O 的 errno(`EACCES` …;没有 errno ⇒ `unreadable`)。 */
39
+ readonly problem: string;
40
+ /** 格里本构建不认得的键(非空 ⇒ 这只边档是更新的引擎写下的:数据根比本构建新)。 */
41
+ readonly unknownFields: readonly string[];
42
+ }
43
+ /**
44
+ * **local 数据根的「本构建读不出」拒**(起服对账一族的 local 腿;机读码与 SQL 列册对账同一个 `config.schema_mismatch`,拒句同形)。
45
+ * 一只严格边档用它那一只严格读者读不出 ⇒ 这一只拒,文件原地不动、一个字节不改。两处触发、一只类:
46
+ * · `boot` —— 起服对账({@link LocalSessionStore.assertStrictSidecarsReadable},唯一调用点 `LocalBackend.ensureSchema()`)把清册里每一只读不出的合成一只:拒启;
47
+ * · `serving` —— 起服之后这只边档第一次被碰时才读不出(嵌入方跳过了 `ensureSchema()`;或起服后别的写者把它写成本构建读不出的形,在替换之前那一读里看见):
48
+ * 读不出的是**形**(`not-json` / `malformed`,确定的判词)⇒ 这只边档在本进程里从此每一读每一写都交这一只(重启才重判);碰它的请求失败(把它交给 HTTP 全局兜底的路由:
49
+ * 500 `internal.error` + 一行 error 级 `request_error` 带本码;自己接住故障的路由按各自的 500 臂答)。I/O 故障(非确定)不粘。
50
+ * 属 {@link StoreBootRefusalError} 族:起服降级臂不吞它(缺省推导的 local 不会被吃成内存后端)。拒句零原文:只点名文件、种别与不认得的键名,不带任何值。
51
+ */
52
+ export declare class LocalDataRootSchemaMismatchError extends StoreBootRefusalError {
53
+ readonly findings: readonly LocalSidecarMismatch[];
54
+ readonly when: "boot" | "serving";
55
+ /** 机读码(与 SQL 列册对账 / 退役 schema 的拒启同一个)。 */
56
+ readonly code = "config.schema_mismatch";
57
+ constructor(findings: readonly LocalSidecarMismatch[], when: "boot" | "serving");
58
+ }
33
59
  /**
34
60
  * local 车道会话存档的**唯一 repo 出口**:本仓里每一只读 `<root>/sessions/*.jsonl` 的 `FileSessionRepo` 都从这里取(`LocalSessionStore` 的底座;会话视图窗不再自开 repo,
35
61
  * 它的元数据与日志都由店交出 —— `local-session-window.ts`),所以坏记录的披露座只有这一份接线。座 = F 类 `server.session-repo.corrupt-record-skipped`(会话存档里读不出的记录丢的是可用性:
36
- * 历史少一截 / 列表少一行,不开任何门;计数单位 = 一条被跳过的记录,逐命中探针行,warn 每进程一次),外加两条作用域腿:会话打开作用域(`noteReplayInSessionOpenScope`:
37
- * 只在 `openSessionForItself` 的作用域里才记)与一次性迁移 M33 的整档读作用域({@link noteReplayInArchiveRewriteRead});作用域外 —— 列表 / 导出 / 视图 —— 两条腿都是空操作,那些读只进运维面。
62
+ * 历史少一截 / 列表少一行,不开任何门;计数单位 = 一条被跳过的记录,逐命中探针行,warn 每进程一次),外加一条作用域腿:一次性迁移 M33 的整档读作用域({@link noteReplayInArchiveRewriteRead});
63
+ * 作用域外那条腿是空操作。用户面的丢失账不经这只座:core 的 `FileSessionRepo` 把回放的计数记在它交出的存储上(`getReplayLoss`),引擎在 run 里读它说
64
+ * `session.lineage_rerooted`(core 7.62.0 [ref];本仓 7.104.0–7.143.0 在这里另铸 `session.records_skipped`,7.144.0 退役 —— 一个事实一个来源)。
38
65
  */
39
66
  export declare function createLocalSessionRepo(root: string): FileSessionRepo;
40
67
  /** 快照目录名(数据根下;本店自有的目录与格式)。常设拒启臂 {@link assertNoArchiveSnapshots} 读它,所以它活得比 M33 的码久。 */
@@ -101,11 +128,6 @@ export declare class LocalSessionStore implements SessionStore {
101
128
  olderThanMs?: number;
102
129
  scope?: string;
103
130
  }) => Promise<PlacedSessionRow[]>;
104
- /**
105
- * 委派继承律的认领口(`delegated-session-owner.ts`;停驻挂点对宿主无主子代行的认领):这只会话的那一位住客(判断时读到的化身)仍无主、且是 placed 行 ⇒ 写 `owner`,
106
- * 返回是否写成。与 `placementOf` 同在或同缺(`delegatedOwnerHostOf`)。随一次性迁移 M23 的窗同删(登记表 M23 行)。
107
- */
108
- readonly claimOwnerless?: OwnerlessClaim;
109
131
  /** placement 的唯一读法(repo 的 `placementOf`;repo 不记 placement ⇒ 恒答缺席 —— 那种 repo 里没有 placed 行)。 */
110
132
  private readonly recordedPlacementOf;
111
133
  /** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one (mirrors TiDBSessionStore). */
@@ -170,7 +192,12 @@ export declare class LocalSessionStore implements SessionStore {
170
192
  * 盘上有会话而边档里没有它那一格 ⇒ 第一次读补铸一格只有化身的(属主没有事实可写)并落盘。严格读(读不出 ⇒ 整只拒,见 `SidecarMap.strict`)。
171
193
  */
172
194
  private readonly meta;
173
- /** 认领行(`session_owner_claimed`,一次性迁移 M10 的认领臂,窗口见 `plugins/one-time-migrations.ts` 登记表)与委派孤行 warn(`delegated_session_owner_orphaned`)的出口。 */
195
+ /**
196
+ * [ref] —— 本店的**严格边档清册**(今天一只:会话元数据)。唯一来源 = 构造里 `sidecar(…, strict)` 那一参:工厂造出严格读的边档时同一手登进来,
197
+ * 没有第二张手写名单。读者只有起服对账 {@link assertStrictSidecarsReadable}。
198
+ */
199
+ private readonly strictSidecars;
200
+ /** 认领行(`session_owner_claimed`,一次性迁移 M10 的认领臂,窗口见 `plugins/one-time-migrations.ts` 登记表)的出口(本店今天只有这一行经它;委派孤行 warn 的铸点已不在 `src/`)。 */
174
201
  private readonly log;
175
202
  /**
176
203
  * 按会话的写锁(表见 {@link SESSION_WRITE_LOCKS_BY_ROOT})—— 会话行两位的**每一笔**写(用户的行格写原语:判 → 边档草稿 → 落盘 → 发布;自动标题器的
@@ -204,12 +231,18 @@ export declare class LocalSessionStore implements SessionStore {
204
231
  * @param dataDir the SAME data dir the file-backed repo persists under (LocalBackend passes `fileBackend.root`).
205
232
  * When given, lastActivity is persisted to `<dataDir>/session-last-activity.json` so it survives a
206
233
  * restart (the file-backed deployment); omit it for the in-memory deployment (nothing survives).
207
- * @param opts.log the `session_owner_claimed` / `delegated_session_owner_orphaned` lines' sink (LocalBackend passes a logger at the deployment's
208
- * LOG_LEVEL; absent ⇒ an info-level JSON-line logger — a claim or an orphaned delegated row is never silent).
234
+ * @param opts.log the `session_owner_claimed` line's sink (LocalBackend passes a logger at the deployment's
235
+ * LOG_LEVEL; absent ⇒ an info-level JSON-line logger — a claim is never silent).
209
236
  * @param opts.durability / opts.placements the deployment's declaration ({@link LocalSessionStoreDeclaration}; see the two fields). */
210
237
  constructor(repo: HeadReadingSessionRepo & Partial<PlacementRecordingRepo>, runs: SessionRunRollupSource & SessionRunClaimSource, dataDir?: string, opts?: {
211
238
  log?: Pick<Logger, "info" | "warn">;
212
239
  } & LocalSessionStoreDeclaration);
240
+ /**
241
+ * [ref] —— **起服对账的 local 腿**(SQL 列册对账的对位;唯一调用点 `LocalBackend.ensureSchema()`,排在 M33 快照消化之前 —— 消化会写会话元数据格):
242
+ * 严格边档清册里的每一只读一次盘(`SidecarMap.reconcile`:冷 ⇒ 载入那一读;已载入 ⇒ 重读只判);读不出的合成**一只** {@link LocalDataRootSchemaMismatchError}
243
+ * (`boot` 形,逐文件一段)⇒ 拒启,开始服务之前。每一只只会以那一只拒失败(严格读的拒臂把读不出的每一形都收成它);别的故障原样上抛(响亮,不归并)。内存部署(没有数据根)⇒ 零读。
244
+ */
245
+ assertStrictSidecarsReadable(): Promise<void>;
213
246
  /**
214
247
  * 这一格此刻是不是 `forking`(fork 创建的格 {@link isForkCreatedCell} ∧ 存档不在)。存档在不在只问 repo 的唯一探针 {@link probeArchive} 的「不在」—— **只问有没有**:存档在而用不了(会话头读不出 / 截断)
215
248
  * 是「在」,内容读不读得出归真正读内容的那一读(门之后、体之前读一次叶子,读不出 ⇒ `corrupt_session`)。所以带键的会话与不带键的会话在执行席上同形,坏档照样能被换装修好
@@ -258,10 +291,10 @@ export declare class LocalSessionStore implements SessionStore {
258
291
  private withSessionWriteLock;
259
292
  /**
260
293
  * 盘上这只会话的存档此刻是什么状态(repo 的**唯一探针**;`open` 读整只存档,所以只在边档里没有那一格、或要判 `forking` 时才走到这里)。三种答案:
261
- * 不在(`not_found`)· 在而读得出 · 读不出({@link UnusableArchive}:`corrupt_session` / `invalid_session` = 在而内容用不了;`storage` = 读的时候抛了别的故障,I/O 级,
294
+ * 不在(`not_found`)· 在而读得出 · 读不出({@link UnusableArchive}:`corrupt_session` = 在而内容用不了;`storage` = 读的时候抛了别的故障,I/O 级,
262
295
  * 说不出在不在 —— 词的读法见 {@link archiveFaultWord})。探针**不吞也不改**那只故障:{@link UnusableArchive.fault} 就是抛出来的那一只,每个读者各自决定上抛还是点名 ——
263
296
  * 要对这份存档动手的({@link existsInRepo};{@link metaOf};{@link readExecutionPlacement})一律上抛它;一次性迁移 M32 的回填({@link executionPlacementBackfill})经
264
- * {@link metaRead} / {@link forkingRead} 收下它、点名跳过,不上抛。只问有没有的 {@link forkingRead} 把 `corrupt_session` / `invalid_session` 读作「在」(存档找到了),`storage` 不替它答。
297
+ * {@link metaRead} / {@link forkingRead} 收下它、点名跳过,不上抛。只问有没有的 {@link forkingRead} 把 `corrupt_session` 读作「在」(存档找到了),`storage` 不替它答。
265
298
  */
266
299
  private probeArchive;
267
300
  /**
@@ -270,7 +303,7 @@ export declare class LocalSessionStore implements SessionStore {
270
303
  */
271
304
  private placementProbe;
272
305
  /**
273
- * 改写面与执行席的读法({@link placementProbe} 的一形):placed ⇒ 那份元组;普通 ⇒ `undefined`;行头读不出(`corrupt_session` / `invalid_session`)⇒ `undefined` —— 没有可读的
306
+ * 改写面与执行席的读法({@link placementProbe} 的一形):placed ⇒ 那份元组;普通 ⇒ `undefined`;行头读不出(`corrupt_session`)⇒ `undefined` —— 没有可读的
274
307
  * placement 记录,这一行按普通行答,内容读不读得出归真正读内容的那一读(坏档照样换得掉,与 forking 探针同一条规矩);说不出(`storage`)⇒ 抛那只故障。
275
308
  * core 7.61.0 起 repo 的探针按行头的元组自己读、不整头解码:placed 行的属主格坏了探针照答元组(本臂不再接得住 placed 行)—— 探针拒只剩「行头记录整个读不出」,
276
309
  * 那时没有任何元组可守(`test/local-session-placement.test.ts` 的 [ref] 三格)。
@@ -290,7 +323,7 @@ export declare class LocalSessionStore implements SessionStore {
290
323
  private metaOf;
291
324
  /**
292
325
  * {@link metaOf} 的本体(三态,不抛「用不了」):边档里有那一格 ⇒ 读它;没有 ⇒ 问**唯一探针** {@link probeArchive} 一次 —— 不在 ⇒ 无此会话;在而读得出 ⇒ 补铸({@link cellOfPresent});
293
- * 读不出 ⇒ {@link UnusableArchive}(**不补铸**:不替一份读不出的存档铸化身;`corrupt_session` / `invalid_session` 两词的故障换成具名拒 {@link SessionOwnerUnreadableError} ——
326
+ * 读不出 ⇒ {@link UnusableArchive}(**不补铸**:不替一份读不出的存档铸化身;`corrupt_session` 的故障换成具名拒 {@link SessionOwnerUnreadableError} ——
294
327
  * 格缺 ∧ 存档用不了 = 这只会话没有可读的属主,见 {@link ownerUnreadableArchive})。要动存档的读者({@link metaOf})把它上抛;不动存档、只要「有没有」的读者
295
328
  * ({@link executionPlacementBackfill})原样收下。
296
329
  */
@@ -451,17 +484,16 @@ export declare class LocalSessionStore implements SessionStore {
451
484
  /** 这次 acquire 要落的 placement:只有声明了分区的店才收(未声明 ⇒ `undefined`,会话就是普通会话 —— core `SessionStore.placements` 缺席的读法)。 */
452
485
  private declaredPlacement;
453
486
  private load;
454
- /** 为这只会话打开它自己的存档,锁内一形(调用方持这只会话的写锁,见 {@link load})。[ref]:这是「为这只会话打开它自己的存档」的那一次
455
- * (引擎 prepare 的 acquire)⇒ 包在打开作用域里:这次回放跳过了记录 ⇒ 打开成功后铸一帧 `session.records_skipped` 投给这只会话(唯一铸点
456
- * 与作用域见 `session-records-skipped-notice.ts`);读不出(`corrupt_session`)照旧原样上抛,不铸。存档只读这一次:住客那一格缺(更早版本
487
+ /** 为这只会话打开它自己的存档,锁内一形(调用方持这只会话的写锁,见 {@link load})。回放跳过了记录 ⇒ 交出的存储带着那份丢失账(core 文件店的
488
+ * `getReplayLoss`,经 {@link occupiedBy} 的视图原样转发),引擎在 run 里说 `session.lineage_rerooted`;读不出(`corrupt_session`)原样上抛。存档只读这一次:住客那一格缺(更早版本
457
489
  * 写下的会话)时,这次打开就是补铸的证明({@link handOut})。GUARD(open 腿):打开一只已存在的会话**不写属主** —— 记下的属主(含「登记为
458
490
  * 无主」)一个字节不动,没被记下的也不在这里替它记成无主:打开不是一个属主事实(7.119.0 前这一腿把「不知道」记成进程内存里的 null,重启即失)。 */
459
491
  private openForItselfHeld;
460
492
  /**
461
493
  * 带 placement 的按 id 首建(锁内一形,调用方持这只会话的写锁、已判过存档不在且不是 fork 创建的格)。写序 = **判别位先落**:先建存档(引擎文件 repo 把元组写进行头、
462
- * 首写不可变),再写会话元数据那一格(化身新铸;属主按委派继承律 —— 根会话的属主,根行不在取父,都不在 ⇒ 孤行哨兵 + 一行 warn;读法与次序只住在
463
- * `delegated-session-owner.ts` 的 `createPlacedSessionRow`,三只店同调)。崩在两笔之间 = 存档在而格缺 = 一只 placed 行、属主从没被记下(读作无主;
464
- * 这形只在 fleet 调用方可读,停驻挂点的认领口 {@link claimOwnerless} 按委派律补上)。
494
+ * 首写不可变),再写会话元数据那一格(化身新铸;属主 = `placement.owner` 原样 —— core 7.62.0 [ref] 盒义务 7:委派树的属主是引擎判的,店只记、不推断;
495
+ * 唯一读法 `placedSessionOwnerOf`,在建存档之前判,坏值 ⇒ 响亮拒、一个字节不写;SQL 孪生同调)。崩在两笔之间 = 存档在而格缺 = 一只 placed 行、属主从没被记下
496
+ * (读作无主;本店没有回填臂 —— M23 的扫描与回写是 SQL 语句,只在两只 SQL 店跑 —— 这一行在具名树的停驻挂点被否决、响亮,匿名树 `null === null` 照放;SQL 孪生的同形由 M23 在窗内回填)。
465
497
  * 格已在而存档不在(登记先落了格、存档还没建 —— 一只普通会话的行)⇒ `placement_refused`(SQL 孪生:登记过的行在,带 placement 的建读回缺席元组,同一判)。
466
498
  * 建后读回准入(镜像方向):别的进程(同一数据根上的 run-local 腿)在这一刻抢先建了同 id 的存档 ⇒ repo 返回的是那一只 ⇒ 元组缺席 / 异组 ⇒ 响亮拒。
467
499
  */
@@ -488,11 +520,6 @@ export declare class LocalSessionStore implements SessionStore {
488
520
  * 整段在这只会话的写锁里(与删除 / 换装 / 登记同一把)。
489
521
  */
490
522
  release(sessionId: string): Promise<void>;
491
- /**
492
- * 委派继承律的认领口(锁内 CAS):placed 行、那一格的化身 = `incarnation`、属主无主(记下的 `null` 或从没被记下)⇒ 写 `owner`、返回 true;任何一条不符 ⇒ false、零写
493
- * (SQL 孪生的 `CLAIM_OWNERLESS_PLACED_SQL` 同四个条件)。placement 读不出 ⇒ 上抛(不写)。
494
- */
495
- private claimOwnerlessPlaced;
496
523
  /**
497
524
  * P0.5 variant-2 (E16, clay 2026-06-26) — enumerate the caller's sessions (CC /resume picker) from the SESSION
498
525
  * abstraction (`repo.list()`), keyset-paginated newest-first by last activity. Session IDENTITY comes from the
@@ -621,7 +648,7 @@ export declare class LocalSessionStore implements SessionStore {
621
648
  * ③ 内容,按写口的动词(唯一读法在下面这一只 switch;这里不读那一格、不判属主,只换**内容**;交给引擎仓的属主恒缺席 —— 规则一):
622
649
  * · `create` / `judged`(导入):原地换锁里读到的那一份存档的日志(没有存档 ⇒ 导入新建一只,匿名),以锁里读到的存档头为 `expect.row`(**行代号**:
623
650
  * 读与写之间存档换了一代 ⇒ 引擎仓拒 `conflict`;它判不出同一代里的追加)。匿名导入只换匿名存档的日志 —— 仍带属主戳的存档(更早的版本写下、一次性迁移 M33
624
- * 改写不了的,或跳版升上来的)本店先以具名拒 `StampedSessionArchiveError` 答(与 fork 同一只;引擎导入律同样会拒,本店不重写、不绕,只让拒带对的出路)。
651
+ * 改写不了的,或跳版升上来的)到不了这里:暂存提交在一切「谁赢」的判之前就以具名拒答了它({@link assertArchiveAnonymousHeld},唯一的一读);`create` 落在没有存档的名上,`replace` 先清存档。
625
652
  * 读存档、校验、存档判与写在引擎仓里是一步,抛在哪一处内容都一个字节没动 ⇒ 撤回 ②({@link withdrawImportCell}:行原样 —— 净零写,SQL 孪生的事务回滚同义)再原样上抛;
626
653
  * · `replace`(接口句「先清既有对话日志,再导入」):先清掉内容存档(不带 `expect` —— 带了就得先读它的头,读不出的存档就清不掉),再导入一只新存档。
627
654
  * 清不可撤:清过之后导入再抛(I/O 故障),内容就没了,行留 ② 落下的 mirror(只读方向,下次推送补齐)—— 已发版本的 replace 同形(删后导入)。
@@ -667,10 +694,18 @@ export declare class LocalSessionStore implements SessionStore {
667
694
  readStagedEntries(stagingId: string): Promise<SessionTreeEntry[]>;
668
695
  /** Drop a finished/aborted local staging handle from the registry (called by the handle on commit/abort). */
669
696
  forgetStaging(stagingId: string): void;
697
+ /**
698
+ * 带戳存档的唯一一判(调用方持这只会话的写锁;只读、零写):这份存档的头仍记着属主戳(更早的版本导入 / 换装写下的,一次性迁移 M33 改写不了的,或跳版升上来的)⇒
699
+ * {@link StampedSessionArchiveError}。引擎导入律不让匿名导入替换带戳存档,本店不重写、不绕 —— 只让拒带对的出路(fork 的源另有一读,同一只具名拒,见 {@link forkTo})。
700
+ * 这是「这份存档的日志根本换不了」的判,所以排在一切「谁赢」的判之前({@link commitStagedImport}):换不了的存档上谈不上同步冲突或载荷分叉,
701
+ * 那两条拒的出路(带同意重来)都是死路 —— 唯一的出路在这一句里。
702
+ */
703
+ private assertArchiveAnonymousHeld;
670
704
  /**
671
705
  * {@link LocalStagingHandle.commit} 的整段 —— **一次持锁**(这只会话的写锁,{@link withSessionWriteLock}):属主 CAS(此刻的属主格 = 这次导入的门判过的那一格,
672
- * 否则抛 {@link StagedCommitOwnerChangedError},一个字节不写;在判目的端日志之前判,SQL 孪生同序)→ 提交时重新分类(目的端此刻的日志 vs staged)
673
- * → 拒未经同意的 fork / stale / 载荷分叉 → 本处写者拒 → 换装(导入写序 ①②③,落点 `judged` = 上面这一判;化身与 fork 键按换装律沿用)或 identical 且内容相同时日志不动、
706
+ * 否则抛 {@link StagedCommitOwnerChangedError},一个字节不写;在判目的端日志之前判,SQL 孪生同序)→ 分区判 → 提交时重新分类(目的端此刻的日志 vs staged)→
707
+ * 这一提交要换日志(不是「id 与内容都相同」那一支)⇒ 带戳存档判({@link assertArchiveAnonymousHeld};只有 local 店有带戳存档,SQL 孪生没有这一步)→
708
+ * 拒未经同意的 fork / stale / 载荷分叉(带戳判在它们之前:带戳存档上日志换不了,同步冲突与载荷分叉的出路 —— 带同意重来 —— 无从谈起)→ 本处写者拒 → 换装(导入写序 ①②③,落点 `judged` = 上面这一判;化身与 fork 键按换装律沿用)或 identical 且内容相同时日志不动、
674
709
  * 行照样落(② 那一格:重戳属主 + mirror)→ 两支同记新近(推送即活动)→ **在同一次持锁里**
675
710
  * 读出落定的会话面交回。锁放下之前谁也换不掉这一位住客,所以交回的面就是这次提交落下的那一位(SQL 孪生在提交事务里读同一行,同义);
676
711
  * 提交之后按住客条件写的重放(会话行两位)只认这只面,不再重读。改属主的每一条写腿(登记 / 删除 / 导入 / 换装)都在同一把锁里,所以锁内这一读到锁放下都成立。