@sema-agent/server 7.143.0 → 7.145.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 (139) hide show
  1. package/MIGRATION.md +295 -0
  2. package/README.md +3 -0
  3. package/README.zh-CN.md +1 -0
  4. package/USAGE.md +34 -16
  5. package/dist/approval-card.d.ts +31 -14
  6. package/dist/approval-card.js +8 -8
  7. package/dist/audit.d.ts +2 -21
  8. package/dist/audit.js +0 -9
  9. package/dist/boot/execution-env.js +1 -1
  10. package/dist/boot/runner-deps.d.ts +10 -5
  11. package/dist/boot/runner-deps.js +5 -4
  12. package/dist/boot/session-faces.d.ts +1 -1
  13. package/dist/boot/session-faces.js +3 -3
  14. package/dist/boot/shutdown.d.ts +1 -1
  15. package/dist/boot/shutdown.js +4 -2
  16. package/dist/boot/stage-01-config.js +2 -0
  17. package/dist/boot/stage-07-capability-layer.js +1 -1
  18. package/dist/boot/stage-09-leader.d.ts +1 -1
  19. package/dist/boot/stage-10-http-server.d.ts +1 -1
  20. package/dist/boot/stage-10-http-server.js +4 -9
  21. package/dist/capabilities/scenarios.d.ts +14 -18
  22. package/dist/capabilities/scenarios.js +1 -2
  23. package/dist/config-d-migration.d.ts +3 -0
  24. package/dist/delegated-session-owner.d.ts +33 -151
  25. package/dist/delegated-session-owner.js +1 -83
  26. package/dist/entry-mode-clamp-notice.d.ts +1 -1
  27. package/dist/execution-placement.d.ts +7 -4
  28. package/dist/fleet/fleet-bus.d.ts +13 -3
  29. package/dist/fleet/fleet-bus.js +17 -3
  30. package/dist/hooks/classic-stations.d.ts +3 -1
  31. package/dist/hooks/classic-stations.js +2 -0
  32. package/dist/http/admission.js +2 -1
  33. package/dist/http/checkpoint-ctx.d.ts +1 -0
  34. package/dist/http/checkpoint-ctx.js +1 -0
  35. package/dist/http/durable-run-leg.js +7 -6
  36. package/dist/http/principal-gate.d.ts +1 -1
  37. package/dist/http/refusal-body.d.ts +7 -3
  38. package/dist/http/refusal-body.js +1 -0
  39. package/dist/http/resume-legs.js +16 -7
  40. package/dist/http/route-ctx.d.ts +15 -0
  41. package/dist/http/routes/approvals-assistant.d.ts +20 -1
  42. package/dist/http/routes/approvals-assistant.js +28 -6
  43. package/dist/http/routes/rules.d.ts +1 -1
  44. package/dist/http/routes/rules.js +5 -2
  45. package/dist/http/routes/session-sync.js +12 -0
  46. package/dist/http/routes/tasks.js +12 -9
  47. package/dist/http/run-meta.d.ts +14 -7
  48. package/dist/http/run-meta.js +5 -2
  49. package/dist/http/server.d.ts +13 -5
  50. package/dist/http/server.js +3 -0
  51. package/dist/http/submit-ctx.d.ts +12 -0
  52. package/dist/http/submit-ctx.js +23 -0
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.js +1 -0
  55. package/dist/lane-home-probe.d.ts +15 -0
  56. package/dist/lane-home-probe.js +4 -0
  57. package/dist/observability/corrupt-read-seat.d.ts +5 -9
  58. package/dist/observability/corrupt-read-seat.js +1 -2
  59. package/dist/observability/fail-open.d.ts +26 -10
  60. package/dist/observability/fail-open.js +26 -10
  61. package/dist/observability/run-terminal.d.ts +1 -1
  62. package/dist/observability/run-terminal.js +2 -2
  63. package/dist/parked-revive-surface.d.ts +6 -7
  64. package/dist/peer-inbound-face.d.ts +29 -34
  65. package/dist/peer-inbound-face.js +28 -63
  66. package/dist/plugins/approval-ask-store-file.d.ts +16 -2
  67. package/dist/plugins/approval-ask-store-file.js +21 -1
  68. package/dist/plugins/approval-ask-store-memory.d.ts +8 -2
  69. package/dist/plugins/approval-ask-store-memory.js +11 -8
  70. package/dist/plugins/caching-session-store.d.ts +3 -3
  71. package/dist/plugins/caching-session-store.js +3 -3
  72. package/dist/plugins/checkpoint-store-sql.d.ts +62 -37
  73. package/dist/plugins/checkpoint-store-sql.js +123 -40
  74. package/dist/plugins/file-run-store.d.ts +13 -1
  75. package/dist/plugins/file-run-store.js +11 -1
  76. package/dist/plugins/fork-routing-session-store.d.ts +8 -1
  77. package/dist/plugins/fork-routing-session-store.js +6 -0
  78. package/dist/plugins/local-checkpoint-store.d.ts +17 -8
  79. package/dist/plugins/local-checkpoint-store.js +26 -7
  80. package/dist/plugins/local-session-store.d.ts +105 -34
  81. package/dist/plugins/local-session-store.js +168 -65
  82. package/dist/plugins/local-session-window.d.ts +4 -2
  83. package/dist/plugins/memory-run-store.d.ts +2 -0
  84. package/dist/plugins/memory-run-store.js +6 -0
  85. package/dist/plugins/one-time-migrations.d.ts +12 -12
  86. package/dist/plugins/one-time-migrations.js +13 -13
  87. package/dist/plugins/pg-session-storage.d.ts +1 -9
  88. package/dist/plugins/pg-session-storage.js +2 -19
  89. package/dist/plugins/remote-env-host.d.ts +8 -4
  90. package/dist/plugins/remote-env-host.js +7 -9
  91. package/dist/plugins/remote-shell.js +12 -2
  92. package/dist/plugins/run-store-sql.d.ts +4 -0
  93. package/dist/plugins/run-store-sql.js +3 -0
  94. package/dist/plugins/schema-consistency.d.ts +5 -1
  95. package/dist/plugins/session-fork.d.ts +1 -1
  96. package/dist/plugins/session-ownership.d.ts +9 -0
  97. package/dist/plugins/session-ownership.js +6 -0
  98. package/dist/plugins/session-store.d.ts +13 -16
  99. package/dist/plugins/session-store.js +9 -10
  100. package/dist/plugins/sql-json-column.d.ts +24 -2
  101. package/dist/plugins/sql-json-column.js +38 -3
  102. package/dist/plugins/store-backend.js +3 -2
  103. package/dist/plugins/tidb-session-store.d.ts +2 -14
  104. package/dist/plugins/tidb-session-store.js +2 -20
  105. package/dist/prompt-screen.d.ts +21 -0
  106. package/dist/prompt-screen.js +8 -0
  107. package/dist/rules-consent.d.ts +63 -51
  108. package/dist/rules-consent.js +6 -10
  109. package/dist/run-acceptance.d.ts +40 -10
  110. package/dist/run-acceptance.js +38 -5
  111. package/dist/run-local.js +2 -1
  112. package/dist/runs.js +5 -1
  113. package/dist/security.d.ts +10 -7
  114. package/dist/session-agent.d.ts +39 -0
  115. package/dist/session-agent.js +16 -0
  116. package/dist/session-error-wire.d.ts +25 -26
  117. package/dist/session-error-wire.js +4 -5
  118. package/dist/session-incarnation.d.ts +2 -2
  119. package/dist/session-notify-door.d.ts +2 -2
  120. package/dist/session-notify-door.js +5 -1
  121. package/dist/tool-approval.d.ts +3 -1
  122. package/dist/tool-approval.js +47 -18
  123. package/dist/trace/core-keyset-guard.d.ts +1 -1
  124. package/dist/trace/engine-notice-wire.d.ts +1 -1
  125. package/dist/trace/engine-notice-wire.js +1 -1
  126. package/dist/trace/ledger-sink.js +12 -4
  127. package/dist/trace/model-segment.d.ts +26 -6
  128. package/dist/trace/model-segment.js +58 -9
  129. package/dist/trace/project.d.ts +8 -2
  130. package/dist/trace/project.js +6 -4
  131. package/dist/trace/redact.d.ts +77 -2
  132. package/dist/trace/redact.js +234 -99
  133. package/dist/trace/self-minted-wire.d.ts +160 -9
  134. package/dist/trace/self-minted-wire.js +52 -3
  135. package/package.json +3 -3
  136. package/wire-changes/server@7.144.0.json +291 -0
  137. package/wire-changes/server@7.145.0.json +146 -0
  138. package/dist/session-records-skipped-notice.d.ts +0 -21
  139. package/dist/session-records-skipped-notice.js +0 -27
@@ -1,6 +1,6 @@
1
1
  import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import { type ExecutionOutcomeRecordWord, type GateOutcome, type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolvedOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity, type ToolApprovalCard } from "@sema-agent/core";
3
+ import { type ExecutionOutcomeRecordWord, type GateOutcome, type Checkpoint, type CheckpointGate, type CheckpointRowVerdict, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolvedOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity, type ToolApprovalCard } from "@sema-agent/core";
4
4
  import { type ParkRowFacts, type RuleOfferProjection, type SkillCardFacts } from "../approval-card.js";
5
5
  import type { KindlessBatchRowSource, KindlessBatchScan } from "../approval-batch-census.js";
6
6
  import { type SqlDialect, type SqlDriver } from "./sql-driver.js";
@@ -174,6 +174,16 @@ export interface PendingCheckpoint extends ParkRowFacts, SkillCardFacts {
174
174
  * `LivePendingRow.approvalCard`。展示/分诊用,永不参与 resume / gate / CAS。(此前此处是按 `tool_name` 列派生的 `contentKind`,随 [ref] 删。)
175
175
  */
176
176
  approvalCard?: ToolApprovalCard;
177
+ /**
178
+ * 这一行**本引擎续不续得了** —— core 判官 `judgeCheckpointRow` 对这一行盘上字节的判决(core 7.62.0 [ref];与助手收件箱行的 `rowVerdict`、
179
+ * 续跑在第一级读到的拒同一个读者、同一答),逐字投、本服务不重算不筛。判官要整行:SQL 孪生按 (库内算的 `checkpoint` 文本 sha256, 引擎构建)
180
+ * 记忆({@link StoredRowProjectionMemo}),未命中才按令牌批读整行;LOCAL 孪生取 core 店摘要上的同一位。
181
+ * 盘上字节读不成一只检查点的行(体是 `{}`、缺 `state`、JSON 字面 `null` / 标量 / 数组 / 非 JSON)同样判:判官对任何值都答,这种行答 `damaged`,
182
+ * 行照列(计 `server.approvals.park-row-blob-unreadable`)—— 一行坏字节只坏它自己那一行,见 {@link SqlCheckpointStore.readStoredRow}。
183
+ * **在场 ⇔ 这一次列表判过这一行**:缺席只在 SQL 孪生上出现 —— 两次读之间行已离开待决,或两次读之间行的字节变了(判过的是另一份,
184
+ * 不拼到这一行上;下一次列表即带)。缺席**不是** `ok`。非 `ok` 的行照列(人要看到它停在哪)。
185
+ */
186
+ rowVerdict?: CheckpointRowVerdict;
177
187
  }
178
188
  /**
179
189
  * [ref]([ref] / DEBTS [ref]):session 键读口(`findPendingTokenBySession` / `peekPendingScope`)的**行绑定**。
@@ -422,24 +432,8 @@ export declare const DECIDED_REPLAY_SCAN_CAP = 8;
422
432
  * (plan_review / dry_run_review)的判词,那些门的决议入口不是本腿,回放它们等于跨门作答。
423
433
  */
424
434
  export declare function approvalDecisionOfWinner(winner: ResolvedOutcome | undefined, boundCallId: string): "approve" | "deny" | null;
425
- /**
426
- * checkpoint blob → `pendingAction.toolCallId`。
427
- *
428
- * 🔴 三态、**不是**两态(真双库实跑抓到的缺陷,2026-08-06):「读不出」与「读出来了、但这条 park 本来就
429
- * 没有 toolCallId」是两件事,压成同一个 `undefined` 会把每一条 `plan_review`/`task_done`/`resource_limit`
430
- * 腿(core 的 `PendingAction` 联合里三个成员结构上就没有 toolCallId)都误报成坏行。前者应标 `unparseable`
431
- * 交给收敛器当「不确定」,后者是**确定的不匹配**——干净地不是候选。
432
- * - `{ readable: false }` —— JSON 坏 / 形状根本不是对象:真的读不出。
433
- * - `{ readable: true, toolCallId: null }` —— 读出来了,这条 park 无工具动作:确定不匹配。
434
- * - `{ readable: true, toolCallId: "…" }` —— 读出来了,拿去比。
435
- *
436
- * 逐层 `typeof` 收窄,不做裸 `as` 断言:blob 是持久层读回来的 `unknown`,是信任边界(宪法 [ref])。
437
- *
438
- * 🔴 [ref]:入参是**已经解过码的体**,不是一格列文本 —— 本函数不再自己 `JSON.parse`(SQL 车道的解码
439
- * 唯一属主是 `sql-json-column.ts`,见 {@link pendingActionOfColumn};local 孪生本来就递一只对象进来)。
440
- * 于是「串就再解一次」这条读法在这里按构造消失,两条车道递进来的东西同形。
441
- */
442
- export declare function pendingActionToolCallId(body: unknown): {
435
+ /** 收敛器读口对一行的身份判词(三态 + 读不出;读法见 {@link pendingActionToolCallId} / {@link candidateIdentityOf})。 */
436
+ export type CandidateIdentity = {
443
437
  readable: true;
444
438
  toolCallId: string | null;
445
439
  boundInputHash: string | null;
@@ -448,6 +442,14 @@ export declare function pendingActionToolCallId(body: unknown): {
448
442
  } | {
449
443
  readable: false;
450
444
  };
445
+ /**
446
+ * 收敛器读口的**身份判词**(两只孪生同一只;`findCheckpointCandidatesForAsk` 的 SQL / LOCAL 两形都只经这里):先问 core 判官 `judgeCheckpointRow`
447
+ * (core 7.62.0 [ref],[ref])—— 判 `lacking`(更新的引擎能续它、本引擎缺读法:格式版本超上限,或要求表点名了本引擎不认的读法词)⇒ `{ readable: false }`:
448
+ * 本副本读不准这一行,交给收敛器当「不确定」,留给能续它的副本去绑(修前 SQL 孪生按 `version` 列比 `MAX_SUPPORTED_CHECKPOINT_VERSION`,
449
+ * 射程只有版本一轴,LOCAL 孪生没有这一臂)。`damaged` **不是**这一臂:本引擎不续这些字节(别的副本续不续,引擎没有说、本服务不替它们断言),身份读得出就照常判 —— 绑上之后续跑由判官给那句固定的拒,
450
+ * 比让 ask 停在 PARKING 诚实。其余三态见 {@link pendingActionToolCallId}。
451
+ */
452
+ export declare function candidateIdentityOf(cp: Checkpoint): CandidateIdentity;
451
453
  /** `pending_steer_queue` 列承载的 CheckpointState 字段(= core `appendPendingSteer` / `readPendingSteerQueue`
452
454
  * 的入参形)。列是**唯一**权威(suspend 时写的 blob 从不带它)。 */
453
455
  type SteerColumnState = Pick<CheckpointState, "pendingSteerQueue">;
@@ -536,6 +538,8 @@ export declare class SqlCheckpointStore implements CheckpointStore, KindlessBatc
536
538
  /** ctx 改写的认领栅栏({@link SqlCheckpointStore.rewriteCtxWhileClaimed}):后端装配绑**同一只池**上的 run 店(`plugins/store-backend.ts` 的 `checkpoint()`)。
537
539
  * 缺席 ⇒ 那一口响亮抛(装配缺陷),其余口不读它。 */
538
540
  private readonly claims;
541
+ /** 停驻行投影记忆([ref] ④;运维队列与调度视图共用,见 {@link StoredRowProjectionMemo})。进程内、按店实例。 */
542
+ private readonly storedRowMemo;
539
543
  constructor(db: SqlDriver, logger?: {
540
544
  info?(msg: string, meta?: unknown): void;
541
545
  } | undefined, opts?: SqlCheckpointStoreOptions);
@@ -711,29 +715,50 @@ export declare class SqlCheckpointStore implements CheckpointStore, KindlessBatc
711
715
  */
712
716
  listPending(scope?: string): Promise<PendingCheckpoint[]>;
713
717
  /**
714
- * 🔴 [ref] —— 运维队列行的**技能卡两位**({@link SkillCardFacts}:清单 / 成因词)。值与收件箱行同源:core `summarizeCheckpoint` 对同一行投的摘要,经 {@link skillCardFacts} 这一只投影。
715
- * (授予绑定那一臂 —— 摘要 / 缺席词 —— 不在这里:[ref] 起由 {@link parkRowFacts} 读子树投出,本读口失败时它照在。)
718
+ * 运维队列 / 调度视图要的**整行事实**({@link StoredRowProjection}:判官的判决 + 技能卡两位),按令牌交回。
716
719
  *
717
- * 为什么不像 {@link parkRowFacts} 那几位一样直接读 `pendingAction` 子树原值:**清单** `boundGrants` 在寄存行上是未经哈希校验的原值,core 只在摘要里「哈希等于摘要」
718
- * 才发布它(「never a list beside a digest that binds another」);校验读口与哈希函数都不在 core 的公开导出面上,本仓既算不出也不该再写一份 ——
719
- * 于是要拿到校验过的清单,只有对这一行跑 core 的 `summarizeCheckpoint`,而它吃的是整只 {@link Checkpoint}(不是子树)。
720
+ * 为什么要整行:判官 `judgeCheckpointRow` 读要求表要整只用(词集 + 摘要重算读整个 `state` 的表键路径,core [ref] ③);技能卡**清单**
721
+ * `boundGrants` 在寄存行上是未经哈希校验的原值,core 只在摘要里「哈希等于摘要」才发布它,校验读口不在 core 的公开导出面上 —— 两者都只能对整只
722
+ * {@link Checkpoint} 跑 core 的函数(摘要 `summarizeCheckpoint` 一次给两样)。
720
723
  *
721
- * 读放大(如实):**只对行上可能带两位之一的行**(摘要是串,或成因词键在场 —— {@link parkRowCarriesSkillCardFacts})多读该行一次整只快照;普通卡(绝大多数行)零额外读,
722
- * 主 SELECT 仍只取 `pendingAction` 子树(NP-3 不退)。这是一个**只增不改的代价**:带两位之一的待决行数 × 读次数(`/stream` 每连接每 3s 一拍)。
723
- * 🚧 清单校验读口成为 core 公开导出的那一版,本读口与门一并退役,两位改走 `parkRowFacts` 读原值经 core 的读口 —— 零额外读。
724
+ * 读放大(如实):记忆命中零整行读;未命中的行按令牌**批**读(`IN (…)`,每批 ≤ {@link STORED_ROW_READ_BATCH}),每份字节每个进程一次 ——
725
+ * 起服后第一次列表把全部待决行读一遍,此后只有字节变了 / 被挤出记忆的行再读(修前:普通卡零整行读,技能卡每次列表都读;`/stream` 每连接每 3 s 一拍)。
726
+ * 判过的按**读回的那份字节**的摘要入记忆;两次读之间字节变了(批读的摘要 ≠ 主读交来的那一份)⇒ 判过的那一份照入记忆,但**不交回**给这一行 ——
727
+ * 交回的事实只许出自主读读到的同一份字节(一行一个字节版本;修前按令牌拼,B 的判决与清单配 A 的行),这一行本次不带判决与清单(与「离开待决」同一种缺席),
728
+ * 下一次列表主读读到新字节即命中记忆。
724
729
  *
725
- * 失败方向与 {@link readParkRowActionCell} 逐字同一条判据(展示/分诊材料、行级容忍 + 响亮、同一个 tag):整只快照在场却不是一只合法的检查点(读回的体坏了,或 core 的摘要读不动它)
726
- * ⇒ 只丢**这一行**的两位(授予绑定那一臂照在);行已不在待决(两次读之间被决/过期)⇒ 两位缺席;`not-text`(读腿漏了投影)原样抛。
730
+ * 失败方向 = 本店停驻行读者的那一条律({@link SqlCheckpointStore.readStoredRow}):摘要读不动的行(字节解不出 / 体是对象但 core 的摘要读不动)
731
+ * 判决照给、清单与成因词缺席,计 `server.approvals.park-row-blob-unreadable`,不进记忆(下一次列表重读、再计)。行已离开待决(两次读之间被决 / 过期)⇒ 不在返回里。
732
+ * `not-text`(读腿漏了投影)与摘要格形不对原样抛(码坏)。授予绑定那一臂(摘要 / 缺席词)不在这里:由 {@link parkRowFacts} 读子树投出。
727
733
  */
728
- private readParkedSkillCardFacts;
734
+ private storedRowProjections;
735
+ /** 一行整行读回 → 它的 {@link StoredRowProjection}(摘要读得动的入记忆;失败方向见 {@link SqlCheckpointStore.readStoredRow})。 */
736
+ private projectStoredRow;
737
+ /**
738
+ * 🔴 **本店停驻行读者的那一条律:一行的字节只坏它自己那一行**(运维队列 / 订阅流的整行读与收件箱 / 任务视图的 `listByScope` 同经这里)。
739
+ *
740
+ * 一行停驻行的整行读 = `checkpoint` 列那一份字节 → core 判官 `judgeCheckpointRow` 的判决 + (读得动时)core 的摘要 `summarizeCheckpoint`。
741
+ * 判决**恒在**:判官对任何值都答(core:pure and total),字节读不成一只检查点(JSON 字面 `null` / 标量 / 数组 / 非 JSON)⇒ 交给它的是这一格**原样的文本**
742
+ * (不是本店编的一行),它答 `damaged`;体是对象而摘要读不动(`{}`、缺 `state` / `pendingAction` / `gate` 的手改行)⇒ 判官对这只对象答 ——
743
+ * 🚧 判官与摘要对「缺 `pendingAction`」不同答(摘要抛、判官答 `ok`:core 判官不查这一位),这一形的行照列而判决是 `ok`;本店不另判(判官的缺口归 core)。
744
+ * 摘要读不动的两种都计 `server.approvals.park-row-blob-unreadable`(每次列表都计)、不进记忆;**不抛** —— 修前收件箱 / 任务视图对这种行整张拒
745
+ * (`{}` / 缺 `state` 落 503「请重试」,坏行不修同路径恒失败),运维队列对字节解不出的行不带判决:同一张坏行两个读者两个方向。
746
+ * 读腿漂(`not-text`:SELECT 没投影这一列)与摘要格形不对照旧原样抛 —— 那是码坏,不是一行数据坏。
747
+ *
748
+ * 判决按 (这份字节, 引擎构建) 记忆({@link StoredRowProjectionMemo}):命中交给摘要不重判;摘要读得动的那一份由调用方 / 这里记下。
749
+ */
750
+ private readStoredRow;
729
751
  /**
730
752
  * [ref] #2 (scheduler seam, core 1.105): a scope's PENDING checkpoints as lightweight
731
753
  * {@link CheckpointSummary} rows in ONE query — the cross-task triage view a supervisor scheduler needs
732
754
  * (gate kind + risk severity + budget spent + deadline per suspended task), no N+1 `get`s. The projection is
733
755
  * core's shared {@link summarizeCheckpoint} run over the persisted blob (the `checkpoint` column = the same full
734
- * {@link Checkpoint} `get()` parses), so this stays byte-identical to core's InMemory/Pg/File impls. Order =
735
- * created_at_ms ASC; callers (the inbox/scheduler) sort by severity. The COLUMN `status` is authoritative (the blob
736
- * is the suspend-time snapshot), so it overrides the blob's status before the summary is derived.
756
+ * {@link Checkpoint} `get()` parses), so a readable row stays byte-identical to core's InMemory/Pg/File impls. Order =
757
+ * created_at_ms ASC; callers (the inbox/scheduler) sort by severity.
758
+ *
759
+ * 一行读不出摘要(字节解不出 / 体是对象而摘要读不动)⇒ 这一行**照列**,判决由判官给(缺 `pendingAction` 那一形判官答 `ok`,见 {@link SqlCheckpointStore.readStoredRow}),其余几位取本店在字节**之外**握着的事实:
760
+ * 令牌 / 会话 / scope / 建行时刻 / 截止 / 门型(`gate_kind` 列,与运维队列同一只判官 {@link queueRowGateKind}:读不出 ⇒ 不列、计数)/
761
+ * `checkpointId` 成员(是串才带;入 wire 前过信任边界由路由判)。失败方向与运维队列同一条律({@link SqlCheckpointStore.readStoredRow})。
737
762
  *
738
763
  * LIMIT bounds the fan-out (review w16yqkkxv): a triage view never needs more than a few — 500 is a generous
739
764
  * ceiling that still protects memory/latency if a scope ever accumulates pathologically many pending gates.
@@ -772,14 +797,14 @@ export declare class SqlCheckpointStore implements CheckpointStore, KindlessBatc
772
797
  * 若只看 pending 就会把「park 成功过」这个既成事实读成「没 park 过」⇒ 又是一条假阴性 DENIED
773
798
  * (对账三约束③)。park 成功是事实,gate 的活性由 `status` 表达,交给收敛器自己判。
774
799
  * - **单行读不出 ⇒ 标记返回,不抛**:`unparseable` 的行由收敛器视同「不匹配」,一条坏行不许打断整段
775
- * 扫描(§8 C-6)。什么算读不出:blob 的 `version` 超出本 build 支持(`get()` 那条前向兼容门在这里
776
- * 不能 throw,否则一条超前行会让整个 session 的对账永久卡死)、或 blob JSON 坏。
800
+ * 扫描(§8 C-6)。什么算读不出:core 判官对这一行答 `lacking`(更新的引擎能续它 —— 本副本不绑,见 {@link candidateIdentityOf};
801
+ * 这里不能 throw,否则一条超前行会让整个 session 的对账永久卡死)、或 blob JSON 坏。
777
802
  *
778
803
  * 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId` 盖下来的权威投影,SQL 谓词
779
804
  * 先按它(或 NULL)收窄;列为 NULL 的行(无工具动作的 park,或列存在之前的旧行)靠解 blob 补判——解得出
780
805
  * 且相等才算候选,解不出就标 `unparseable`。
781
806
  * 🔴 **没有「列命中即零解析」的快路径**(原注写过,已作废,别照它优化):函数体对**每一行**无条件解
782
- * blob,原因有二 ——(a) 前向兼容门与 blob 可读性门必须门在**所有**命中路径之前(codex F3 + 确认轮:
807
+ * blob,原因有二 ——(a) 判官的 `lacking` 臂与 blob 可读性门必须门在**所有**命中路径之前(codex F3 + 确认轮:
783
808
  * 列长得对不代表 blob 读得出,放行一条读不出的行去 `bindBatch` 会把 ask 钉成 PARKED + 一张本进程读不出
784
809
  * 的 resume 坐标,而 PARKED 不可回滚);(b) `[ref]` 之后 blob 顶层的 `sourceSessionId` 是和解三元组的第一
785
810
  * 维,列命中行结构上也必须解 blob 才拿得到它。
@@ -835,7 +860,7 @@ export declare class SqlCheckpointStore implements CheckpointStore, KindlessBatc
835
860
  *
836
861
  * 刻意**不带** `status='pending'` 谓词:判据是「这一行是不是你的」,与它此刻处在哪一态无关(`get()`
837
862
  * 自己也是裸 `WHERE token = ?`);加了反而在「刚被并发消费」的窗里把属主自己的诊断吞掉。
838
- * 只 SELECT `scope` 一列 ⇒ 版本闸/坏 blob 都影响不到它(那是 `get()` 读 `version`/`checkpoint` 的事)。
863
+ * 只 SELECT `scope` 一列 ⇒ 坏 blob 影响不到它(那是 `get()` 读 `checkpoint` 的事;7.144.0 起 `get()` 不再按版本列拒,本引擎续不续由 core 判官在续跑时答)。
839
864
  */
840
865
  peekScopeByToken(token: CheckpointToken): Promise<string | null | undefined>;
841
866
  peekPendingScope(sessionId: string, binding?: PendingRowBinding): Promise<string | null | undefined>;
@@ -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,
@@ -124,6 +153,9 @@ const readRiskDescriptorBody = createObjectColumnReader();
124
153
  function decodeCheckpointColumn(cell, where) {
125
154
  return decodeJsonColumn(cell, readCheckpointBlobBody, `checkpoint.checkpoint ${where}`);
126
155
  }
156
+ function verdictOfUnreadableBody(cell) {
157
+ return judgeCheckpointRow(cell);
158
+ }
127
159
  function decodeOutcomeColumn(cell, where) {
128
160
  return decodeNullableJsonColumn(cell, readResumeOutcomeBody, `checkpoint.outcome ${where}`);
129
161
  }
@@ -236,7 +268,7 @@ export function approvalDecisionOfWinner(winner, boundCallId) {
236
268
  }
237
269
  }
238
270
  }
239
- export function pendingActionToolCallId(body) {
271
+ function pendingActionToolCallId(body) {
240
272
  if (typeof body !== "object" || body === null)
241
273
  return { readable: false };
242
274
  if ("sourceTaskId" in body)
@@ -265,17 +297,22 @@ export function pendingActionToolCallId(body) {
265
297
  return { readable: false };
266
298
  return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceSessionId };
267
299
  }
268
- function pendingActionOfColumn(cell, where) {
269
- let body;
300
+ export function candidateIdentityOf(cp) {
301
+ if (judgeCheckpointRow(cp).kind === "lacking")
302
+ return { readable: false };
303
+ return pendingActionToolCallId(cp);
304
+ }
305
+ function candidateIdentityOfColumn(cell, where) {
306
+ let cp;
270
307
  try {
271
- body = decodeJsonColumn(cell, readJsonObjectColumn, `checkpoint.checkpoint ${where}`);
308
+ cp = decodeCheckpointColumn(cell, where);
272
309
  }
273
310
  catch (e) {
274
311
  if (!isJsonColumnBodyProblem(e))
275
312
  throw e;
276
313
  return { readable: false };
277
314
  }
278
- return pendingActionToolCallId(body);
315
+ return candidateIdentityOf(cp);
279
316
  }
280
317
  function readActorAssertion(v) {
281
318
  if (typeof v !== "object" || v === null)
@@ -346,6 +383,7 @@ export class SqlCheckpointStore {
346
383
  fidelity = "json";
347
384
  execution = { outcome: true };
348
385
  claims;
386
+ storedRowMemo = new StoredRowProjectionMemo();
349
387
  constructor(db, logger, opts = {}) {
350
388
  this.db = db;
351
389
  this.logger = logger;
@@ -433,16 +471,12 @@ export class SqlCheckpointStore {
433
471
  }
434
472
  }
435
473
  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, ` +
474
+ const { rows } = await this.db.query(this.q(`SELECT status, ${jsonColumnText("mysql", "checkpoint")}, ${jsonColumnText("mysql", "outcome")}, reopen_reason, rev, ` +
475
+ `${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
476
  `${jsonColumnText("pg", "pending_steer_queue")}, ${jsonColumnText("pg", "execution_outcome")}, execution_at_ms FROM checkpoint WHERE token = $1`), [token]);
439
477
  const r = rows[0];
440
478
  if (!r)
441
479
  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
480
  const cp = refusing("a checkpoint nobody can resume", () => decodeCheckpointColumn(r.checkpoint, `for token ${tokenFingerprint(token)}`));
447
481
  cp.status = r.status;
448
482
  const outcomeRaw = refusing("a decision record nobody can read", () => decodeOutcomeColumn(r.outcome, `for token ${tokenFingerprint(token)}`));
@@ -468,7 +502,6 @@ export class SqlCheckpointStore {
468
502
  this.logger?.info?.("checkpoint_get", {
469
503
  tokenFp: tokenFingerprint(token),
470
504
  status: cp.status,
471
- version,
472
505
  hasWorkspaceHandle: !!cp.state?.workspaceHandle,
473
506
  snapshotId: cp.state?.workspaceHandle?.snapshotId,
474
507
  });
@@ -558,7 +591,8 @@ export class SqlCheckpointStore {
558
591
  const d = this.db.dialect;
559
592
  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
593
  `${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)} ` +
594
+ `${jsonColumnText(d, "checkpoint", PARK_ROW_PENDING_ACTION)}, ${jsonColumnText(d, "checkpoint", PARK_ROW_CHECKPOINT_ID)}, ` +
595
+ `${jsonColumnTextSha256(d, "checkpoint", CHECKPOINT_SHA256_AS)} ` +
562
596
  "FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
563
597
  const { rows } = scope
564
598
  ? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at_ms ASC`, [scope])
@@ -592,43 +626,96 @@ export class SqlCheckpointStore {
592
626
  ...(approvalCard !== undefined ? { approvalCard } : {}),
593
627
  ...parkRowFacts(pendingAction),
594
628
  };
595
- return [{ row, token: String(r.token), readSkillCard: parkRowCarriesSkillCardFacts(pendingAction) }];
629
+ return [{ row, token: String(r.token), sha256: readJsonColumnTextSha256(r[CHECKPOINT_SHA256_AS], "checkpoint.checkpoint_sha256 (operator queue row)") }];
630
+ });
631
+ const projections = await this.storedRowProjections(entries);
632
+ const out = entries.map((e) => {
633
+ const p = projections.get(e.token);
634
+ return p === undefined ? e.row : { ...e.row, ...p.skillCardFacts, rowVerdict: p.rowVerdict };
596
635
  });
597
- const out = [];
598
- for (const e of entries)
599
- out.push(e.readSkillCard ? { ...e.row, ...(await this.readParkedSkillCardFacts(e.token)) } : e.row);
600
636
  return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
601
637
  }
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 {};
638
+ async storedRowProjections(rows) {
639
+ const out = new Map();
640
+ const misses = [];
641
+ const expected = new Map();
642
+ for (const { token, sha256 } of rows) {
643
+ expected.set(token, sha256);
644
+ const hit = this.storedRowMemo.get(sha256);
645
+ if (hit === undefined)
646
+ misses.push(token);
647
+ else
648
+ out.set(token, hit);
649
+ }
650
+ for (let i = 0; i < misses.length; i += STORED_ROW_READ_BATCH) {
651
+ const batch = misses.slice(i, i + STORED_ROW_READ_BATCH);
652
+ const { rows: read } = await this.db.query(this.q(`SELECT token, ${jsonColumnText("mysql", "checkpoint")}, ${jsonColumnTextSha256("mysql", "checkpoint", CHECKPOINT_SHA256_AS)} FROM checkpoint ` +
653
+ `WHERE status = 'pending' AND token IN (${batch.map(() => "?").join(", ")})`, `SELECT token, ${jsonColumnText("pg", "checkpoint")}, ${jsonColumnTextSha256("pg", "checkpoint", CHECKPOINT_SHA256_AS)} FROM checkpoint ` +
654
+ `WHERE status = 'pending' AND token IN (${batch.map((_, j) => `$${j + 1}`).join(", ")})`), batch);
655
+ for (const r of read) {
656
+ const token = String(r.token);
657
+ const sha256 = readJsonColumnTextSha256(r[CHECKPOINT_SHA256_AS], "checkpoint.checkpoint_sha256 (whole-row read)");
658
+ const projection = this.projectStoredRow(r, sha256);
659
+ if (sha256 === expected.get(token))
660
+ out.set(token, projection);
661
+ }
662
+ }
663
+ return out;
664
+ }
665
+ projectStoredRow(r, sha256) {
666
+ const read = this.readStoredRow(r, "(operator queue row, whole-row read)", sha256);
667
+ return read.summary === undefined ? { rowVerdict: read.rowVerdict, skillCardFacts: {} } : { rowVerdict: read.rowVerdict, skillCardFacts: skillCardFacts(read.summary) };
668
+ }
669
+ readStoredRow(r, where, sha256, discloseFor) {
670
+ const memo = this.storedRowMemo.get(sha256);
607
671
  let cp;
608
672
  try {
609
- cp = decodeCheckpointColumn(r.checkpoint, "(operator queue row, skill-card facts)");
673
+ cp = decodeCheckpointColumn(r.checkpoint, where);
610
674
  }
611
675
  catch (e) {
612
676
  if (!isJsonColumnBodyProblem(e))
613
677
  throw e;
614
678
  recordFailOpen("server.approvals.park-row-blob-unreadable");
615
- return {};
679
+ return { rowVerdict: verdictOfUnreadableBody(r.checkpoint) };
616
680
  }
681
+ let summary;
617
682
  try {
618
- return skillCardFacts(summarizeCheckpoint(cp));
683
+ summary = summarizeCheckpoint(cp, discloseFor?.(cp), memo?.rowVerdict);
619
684
  }
620
685
  catch {
621
686
  recordFailOpen("server.approvals.park-row-blob-unreadable");
622
- return {};
687
+ return { rowVerdict: judgeCheckpointRow(cp) };
623
688
  }
689
+ if (memo === undefined)
690
+ this.storedRowMemo.set(sha256, { rowVerdict: summary.rowVerdict, skillCardFacts: skillCardFacts(summary) });
691
+ return { rowVerdict: summary.rowVerdict, summary };
624
692
  }
625
693
  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]);
694
+ const d = this.db.dialect;
695
+ const { rows } = await this.db.query(`SELECT ${jsonColumnText(d, "checkpoint")}, ${jsonColumnTextSha256(d, "checkpoint", CHECKPOINT_SHA256_AS)}, ${jsonColumnText(d, "checkpoint", PARK_ROW_CHECKPOINT_ID)}, ` +
696
+ `token, session_id, scope, created_at_ms, deadline, gate_kind FROM checkpoint WHERE status = 'pending' AND scope = ${this.q("?", "$1")} ORDER BY created_at_ms ASC LIMIT 500`, [scope]);
627
697
  const out = [];
628
698
  for (const r of rows) {
629
- const cp = refusing("a checkpoint nobody can resume", () => decodeCheckpointColumn(r.checkpoint, `in the ${scope} scheduler view`));
630
- cp.status = r.status;
631
- out.push(summarizeCheckpoint(cp, (sentence) => recordFailOpen("server.checkpoint.inbox-risk-axes-damaged", withoutCheckpointToken(sentence, cp.token))));
699
+ const sha256 = readJsonColumnTextSha256(r[CHECKPOINT_SHA256_AS], `checkpoint.checkpoint_sha256 in the ${scope} scheduler view`);
700
+ const read = this.readStoredRow(r, `in the ${scope} scheduler view`, sha256, (cp) => (sentence) => recordFailOpen("server.checkpoint.inbox-risk-axes-damaged", withoutCheckpointToken(sentence, cp.token)));
701
+ if (read.summary !== undefined) {
702
+ out.push(read.summary);
703
+ continue;
704
+ }
705
+ const gateKind = queueRowGateKind(r.gate_kind);
706
+ if (gateKind === undefined)
707
+ continue;
708
+ const checkpointId = readCheckpointIdCell(r[PARK_ROW_CHECKPOINT_ID.as]);
709
+ out.push({
710
+ token: String(r.token),
711
+ rowVerdict: read.rowVerdict,
712
+ ...(typeof checkpointId === "string" ? { checkpointId } : {}),
713
+ sessionId: String(r.session_id),
714
+ scope: String(r.scope),
715
+ gateKind,
716
+ createdAt: Number(r.created_at_ms),
717
+ ...(r.deadline == null ? {} : { deadline: Number(r.deadline) }),
718
+ });
632
719
  }
633
720
  return out;
634
721
  }
@@ -642,8 +729,8 @@ export class SqlCheckpointStore {
642
729
  return rows[0] ? String(rows[0].token) : null;
643
730
  }
644
731
  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 ` +
732
+ 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 ` +
733
+ "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
734
  "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
735
  const out = [];
649
736
  for (const r of rows) {
@@ -654,11 +741,7 @@ export class SqlCheckpointStore {
654
741
  boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
655
742
  sourceSessionId: null,
656
743
  };
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))}`);
744
+ const derived = candidateIdentityOfColumn(r.checkpoint, `for token ${tokenFingerprint(String(r.token))}`);
662
745
  if (!derived.readable) {
663
746
  out.push({ ...base, boundCallId: null, unparseable: true });
664
747
  continue;
@@ -118,7 +118,13 @@ export declare class FileRunStore implements InProcessClaimFence {
118
118
  */
119
119
  private refuseIfUnreadable;
120
120
  private hydrate;
121
- /** Commit a RunRow to disk (whole-file atomic replace) AND the index — the single write path for every status change. */
121
+ /** run.json 的整文件原子替换 —— {@link persist} 的落盘那一半,不碰索引。 */
122
+ private writeRunFile;
123
+ /** Commit a RunRow to disk (whole-file atomic replace) AND the index — the single write path for every status change.
124
+ * 落盘在前、`runs.set` 在后:落盘抛 ⇒ 索引原样。要「全有或全无」的写口因此不在索引里那只行上原地先改再调 —— 原地改过的行,落盘一抛就是「内存已改、盘上没改」。
125
+ * 两种不原地的写法:对**副本**调本口(索引里换成副本:`releaseIdemKey` / `bindIdemBodyFingerprint` / `takeOverClaim`);或副本只走 {@link writeRunFile},
126
+ * 落成之后把改动**合回索引里的同一只行**(`fillObjectivePreview`)——后一种用在会撞进收割腿 await 窗的写口上:`reapSuspended` 等收割腿在 probe 的 await 之间手里持着索引里的行对象,
127
+ * await 之后的复核读的就是这只对象,索引里的行被换成副本,它们就看不见之后的认领 / 取消了。 */
122
128
  private persist;
123
129
  private eventLogFor;
124
130
  /** Create a run iff the session has no active run (the task_active unique-key claim) AND no submission key of this
@@ -136,6 +142,12 @@ export declare class FileRunStore implements InProcessClaimFence {
136
142
  * Synchronous from the key check to the index write ⇒ two same-key creates in this process (different sessions,
137
143
  * different claim locks) cannot both pass the check: the loser observes the winner's row. */
138
144
  private commitNewRun;
145
+ /** run 行预览的写一次补写(SQL 孪生同义:列仍是 null 才写,建行写过 / 已补过的不动;行不在 ⇒ 无事)。
146
+ * **副本判改 → 落盘 → 合回**:先把改动落在副本上写盘,落盘抛 ⇒ 内存里的行一字未动(预览仍是 null)、盘上也原样,下一次补写照常再试;
147
+ * 落成之后再把预览合回索引里的**同一只**行对象(不换对象:跨 await 持着行的收割腿复核读的是手上这只对象)。
148
+ * 原地先改再落盘,抛的那一拍就把内存和盘劈成两半(内存带预览、盘上 null),且「列已有值」的早退让这一分叉再也补不回来。
149
+ * 本体同步执行到底,中间没有 await ⇒ 副本落盘与合回之间没有别的写者插进来。 */
150
+ fillObjectivePreview(taskId: string, preview: string): Promise<void>;
139
151
  requestCancel(taskId: string, owner: string | null): Promise<boolean>;
140
152
  isCancelRequested(taskId: string, owner: string | null): Promise<boolean>;
141
153
  requestPreempt(taskId: string, owner: string | null): Promise<boolean>;
@@ -352,9 +352,12 @@ export class FileRunStore {
352
352
  }
353
353
  }
354
354
  }
355
- persist(r) {
355
+ writeRunFile(r) {
356
356
  mkdirSync(this.runDir(r.taskId), { recursive: true });
357
357
  atomicWriteFile(this.tmpDir, this.runJsonPath(r.taskId), `${JSON.stringify(this.toDisk(r))}\n`);
358
+ }
359
+ persist(r) {
360
+ this.writeRunFile(r);
358
361
  this.runs.set(r.taskId, r);
359
362
  }
360
363
  eventLogFor(taskId) {
@@ -427,6 +430,13 @@ export class FileRunStore {
427
430
  this.byIdem.set(idemKey, taskId);
428
431
  return { ok: true };
429
432
  }
433
+ async fillObjectivePreview(taskId, preview) {
434
+ const r = this.runs.get(taskId);
435
+ if (!r || r.objectivePreview !== null)
436
+ return;
437
+ this.writeRunFile({ ...r, objectivePreview: preview });
438
+ r.objectivePreview = preview;
439
+ }
430
440
  async requestCancel(taskId, owner) {
431
441
  const r = this.runs.get(taskId);
432
442
  if (!r || !ownerEq(r.owner, owner) || r.status !== "running")
@@ -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;
@@ -91,6 +91,15 @@ export declare class LocalCheckpointStore {
91
91
  private ensureScopesRebuilt;
92
92
  /** All still-pending checkpoints across the registered scopes (summary + full row via get). */
93
93
  private pendings;
94
+ /**
95
+ * 一条会话的待决行 —— **只读这条会话自己的行**(本店停驻行读者的那一条律:一行的字节只坏它自己那一行;决断不许因为别的会话的一行坏行而失败)。
96
+ * 读口 = core 店的 `listBySession`(按会话取行,一行读不出的行答 `unreadable` 而不是整页失败、也不是跳过;它不经 scope 注册表、不投摘要);
97
+ * `unreadable` 的行经 `get(token)` 取原行(core 店的 `get` 原样拷出,不判形)—— 它仍是这条会话的一张待决行,选不选它由下游的门型绑定判
98
+ * ({@link pickPendingRow}:门型读不出 ⇒ 不命中,与 SQL 孪生 `gate_kind IN (…)` 对 NULL 同向),续不续由续跑的第一级(core 判官)答。
99
+ * 修前三个按会话的读口(`findPendingTokenBySession` / `peekPendingScope` / `deleteBySession`)走 {@link pendings} 的整店枚举,而那一路经 core 的
100
+ * `listByScope` 投摘要 —— 同库任何一行摘要读不动(缺 `state`)⇒ 整只枚举抛,别人那张好卡的决断也 500。
101
+ */
102
+ private sessionPendings;
94
103
  /** Operator approval queue — the same NON-secret card as the SQL twins (never the token). */
95
104
  /** 每一只待决 checkpoint 的会话(去重;不按门型过滤 —— 理由与 SQL 孪生 `listPendingSessionIds` 逐字同,见那里)。 */
96
105
  listPendingSessionIds(): Promise<string[]>;
@@ -147,11 +156,9 @@ export declare class LocalCheckpointStore {
147
156
  * · **没有列,所以没有列/blob 一致性门**:SQL 孪生的两道矛盾门(`tool_call_id` 与 `bound_input_hash`
148
157
  * 的列 vs blob)在本形结构上不存在 —— 本形只有 blob 这一个真源,谈不上两份互相矛盾。判别力因此**不弱**:
149
158
  * 那两道门防的是「外部改库 / schema 偏斜让列与 blob 分家」,而本形没有可分家的第二份。
150
- * · **没有行级版本闸**:本类 `peekScopeByToken` 顶注已亲核过这条事实 —— 本形的 `get()` 没有行级版本闸,
151
- * 唯一的读不动形是账本重放级(整店同时读不动)。所以 SQL 孪生那道 `version > MAX_SUPPORTED` 的
152
- * `unparseable` 臂在这里没有对应物;本口的 `unparseable` 只由**窄读器**判(blob 形不合)。
153
- * · **窄读器是同一只** `pendingActionToolCallId`(与 SQL 孪生共用,零第二份手抄):身份三元组的第一维
154
- * `sourceSessionId`、`boundInputHash`、以及「读不出 vs 确定不匹配」的三态,全部由它一家判。
159
+ * · **身份判词是同一只** `candidateIdentityOf`(与 SQL 孪生共用,零第二份手抄):先问 core 判官(答 `lacking` ⇒ `unparseable`,
160
+ * 7.144.0 起两只孪生同一臂 —— 修前只有 SQL 孪生按版本列比,本形没有对应物),再由窄读器判身份三元组的第一维 `sourceSessionId`、
161
+ * `boundInputHash`、以及「读不出 vs 确定不匹配」的三态。
155
162
  */
156
163
  findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
157
164
  /**
@@ -159,9 +166,11 @@ export declare class LocalCheckpointStore {
159
166
  * 语义与存在理由的唯一属主 = SQL 侧 `peekScopeByToken` 顶注。
160
167
  *
161
168
  * ⚠️ 形差如实记:SQL 孪生那口是真「轻读」(只 SELECT scope 一列),本形没有列可选 —— core 的 File 店
162
- * 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()` 没有行级版本闸(唯一的
163
- * 读不动形是账本重放级 = 整店同时读不动,`findPendingTokenBySession` 先抛),所以「行体读得出而 scope
164
- * 读不出」这一支在本形结构上不存在。哪天本形补了行级闸,这里要跟着改成不碰行体的读。
169
+ * 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()` 没有行级拒、也不判形(两只孪生 7.144.0 起都不按版本拒 ——
170
+ * 本引擎续不续由 core 判官在续跑时答),原样拷出一行。读不动的形有两层:整店级 = 账本重放抛(认不出的事件种类等 ⇒ 整只店装不起来);
171
+ * 单行级也存在 —— 按会话的读口({@link sessionPendings})走 core 店的 `listBySession`,摘要读不出的那一行答 `unreadable`(读口不抛,行经 `get(token)` 取原行)。
172
+ * 即便如此,「行体读得出而 scope 读不出」这一支在本形仍结构上不存在:`scope` 是账本重放认一行的门(`token` / `scope` / `status` 皆字符串)里的成员,
173
+ * 门不过的行重放时被跳过并披露、根本不进店;进了店的行,行体其余成员损成什么样,`get(token)?.scope` 都是字符串。哪天本形补了行级拒、或 core 放宽重放的那道门,这里要跟着改成不碰行体的读。
165
174
  */
166
175
  peekScopeByToken(token: CheckpointToken): Promise<string | null | undefined>;
167
176
  /** `undefined` = no pending row; else the owner scope (local scopes are always strings — never null).