@sema-agent/server 7.103.2 → 7.105.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/MIGRATION.md +378 -2
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +320 -16
  5. package/deploy/sema-up/README.md +57 -7
  6. package/deploy/sema-up/cluster-up.sh +18 -4
  7. package/deploy/sema-up/kube-up.sh +16 -0
  8. package/deploy/sema-up/lib/dataroot.sh +142 -0
  9. package/deploy/sema-up/lib/flagshape.sh +69 -0
  10. package/deploy/sema-up/lib/lanes.py +14 -4
  11. package/deploy/sema-up/lib/shell-access.sh +41 -0
  12. package/deploy/sema-up/preflight.sh +14 -1
  13. package/deploy/sema-up/sema-up.sh +134 -37
  14. package/deploy/sema-up/smoke.sh +5 -0
  15. package/dist/auto-mode-face.js +3 -4
  16. package/dist/boot/budget-tracing.js +1 -1
  17. package/dist/boot/config-center.d.ts +6 -2
  18. package/dist/boot/config-center.js +5 -59
  19. package/dist/boot/execution-env.d.ts +11 -0
  20. package/dist/boot/execution-env.js +14 -0
  21. package/dist/boot/execution-lane.d.ts +25 -0
  22. package/dist/boot/execution-lane.js +95 -0
  23. package/dist/boot/memory-consolidation.d.ts +185 -9
  24. package/dist/boot/memory-consolidation.js +192 -32
  25. package/dist/boot/resolve-spec.d.ts +2 -0
  26. package/dist/boot/resolve-spec.js +28 -24
  27. package/dist/boot/runner-deps.d.ts +6 -2
  28. package/dist/boot/runner-deps.js +6 -1
  29. package/dist/boot/session-faces.d.ts +5 -0
  30. package/dist/boot/session-faces.js +18 -2
  31. package/dist/boot/session-incarnation-seat.d.ts +41 -0
  32. package/dist/boot/session-incarnation-seat.js +69 -0
  33. package/dist/boot/stage-01-config.js +2 -2
  34. package/dist/boot/stage-02-registry-pull.js +3 -1
  35. package/dist/boot/stage-04-stores.d.ts +2 -0
  36. package/dist/boot/stage-04-stores.js +3 -1
  37. package/dist/boot/stage-05-execution-env.d.ts +4 -0
  38. package/dist/boot/stage-05-execution-env.js +2 -2
  39. package/dist/boot/stage-06-runners.d.ts +4 -1
  40. package/dist/boot/stage-06-runners.js +7 -14
  41. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  42. package/dist/boot/stage-08-reapers.d.ts +4 -1
  43. package/dist/boot/stage-09-leader.d.ts +6 -1
  44. package/dist/boot/stage-09-leader.js +2 -1
  45. package/dist/boot/stage-10-http-server.d.ts +6 -1
  46. package/dist/boot/stage-10-http-server.js +4 -1
  47. package/dist/brain.d.ts +1 -1
  48. package/dist/config-catalog.d.ts +16 -1
  49. package/dist/config-catalog.js +9 -9
  50. package/dist/config-center/restart-signal.d.ts +4 -4
  51. package/dist/config-invariants.d.ts +7 -1
  52. package/dist/config-invariants.js +37 -6
  53. package/dist/config-provider.d.ts +42 -10
  54. package/dist/config-provider.js +18 -16
  55. package/dist/config-types.d.ts +37 -16
  56. package/dist/config-types.js +1 -0
  57. package/dist/config.d.ts +28 -30
  58. package/dist/config.js +95 -77
  59. package/dist/env-facts.d.ts +13 -3
  60. package/dist/env-facts.js +15 -3
  61. package/dist/execution-lane-caps.d.ts +15 -0
  62. package/dist/execution-lane-caps.js +8 -0
  63. package/dist/file-history-capture.d.ts +42 -35
  64. package/dist/file-history-capture.js +15 -10
  65. package/dist/file-history-disclosure.d.ts +7 -31
  66. package/dist/file-history-disclosure.js +12 -0
  67. package/dist/fleet/fleet-bus.d.ts +5 -1
  68. package/dist/http/admission.d.ts +12 -5
  69. package/dist/http/admission.js +5 -1
  70. package/dist/http/dispatch.d.ts +18 -0
  71. package/dist/http/dispatch.js +19 -6
  72. package/dist/http/principal-gate.d.ts +22 -0
  73. package/dist/http/principal-gate.js +20 -0
  74. package/dist/http/resume-legs.d.ts +1 -1
  75. package/dist/http/resume-legs.js +9 -5
  76. package/dist/http/route-table.d.ts +15 -4
  77. package/dist/http/route-table.js +6 -5
  78. package/dist/http/routes/agents-roster.d.ts +7 -0
  79. package/dist/http/routes/agents-roster.js +6 -2
  80. package/dist/http/routes/approvals-assistant.d.ts +48 -19
  81. package/dist/http/routes/approvals-assistant.js +117 -50
  82. package/dist/http/routes/capabilities.js +3 -2
  83. package/dist/http/routes/config-catalog.js +1 -0
  84. package/dist/http/routes/fleet.d.ts +3 -2
  85. package/dist/http/routes/images.js +0 -1
  86. package/dist/http/routes/memory-consolidation.d.ts +3 -2
  87. package/dist/http/routes/memory-policy.js +3 -3
  88. package/dist/http/routes/rules.js +2 -2
  89. package/dist/http/routes/runs.js +29 -10
  90. package/dist/http/routes/session-sync.js +14 -15
  91. package/dist/http/routes/sessions-list.js +3 -2
  92. package/dist/http/routes/sessions.js +49 -25
  93. package/dist/http/routes/shared-memory.js +2 -2
  94. package/dist/http/routes/side-query.js +8 -1
  95. package/dist/http/routes/tasks.js +22 -19
  96. package/dist/http/routes/trace-usage.d.ts +2 -0
  97. package/dist/http/routes/trace-usage.js +8 -13
  98. package/dist/http/send.d.ts +12 -0
  99. package/dist/http/send.js +12 -1
  100. package/dist/http/server.d.ts +30 -8
  101. package/dist/http/server.js +11 -5
  102. package/dist/http/sse-lifecycle.d.ts +1 -1
  103. package/dist/http/sse-log.d.ts +12 -9
  104. package/dist/http/sse-log.js +1 -8
  105. package/dist/http/sync-submit-body.d.ts +43 -0
  106. package/dist/http/sync-submit-body.js +4 -0
  107. package/dist/http/wire-types.d.ts +23 -5
  108. package/dist/index.d.ts +1 -0
  109. package/dist/index.js +1 -0
  110. package/dist/leader/merge.d.ts +8 -3
  111. package/dist/memory-incident-sink.js +2 -1
  112. package/dist/model-select.d.ts +9 -0
  113. package/dist/model-select.js +5 -1
  114. package/dist/observability/corrupt-read-seat.d.ts +151 -16
  115. package/dist/observability/corrupt-read-seat.js +158 -15
  116. package/dist/observability/fail-open.d.ts +17 -1
  117. package/dist/observability/fail-open.js +17 -1
  118. package/dist/observability/metrics.js +1 -1
  119. package/dist/observability/run-terminal.d.ts +60 -10
  120. package/dist/observability/run-terminal.js +119 -9
  121. package/dist/parked-decide.d.ts +8 -3
  122. package/dist/parked-decide.js +56 -20
  123. package/dist/parked-revive-surface.d.ts +14 -7
  124. package/dist/parked-revive-surface.js +5 -1
  125. package/dist/plugins/approval-ask-store-file.d.ts +4 -1
  126. package/dist/plugins/approval-ask-store-file.js +9 -12
  127. package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
  128. package/dist/plugins/approval-ask-store-sql.js +4 -1
  129. package/dist/plugins/approval-exemption-store.d.ts +21 -1
  130. package/dist/plugins/approval-exemption-store.js +36 -7
  131. package/dist/plugins/approval-nonce-store.d.ts +16 -2
  132. package/dist/plugins/approval-nonce-store.js +17 -7
  133. package/dist/plugins/caching-session-store.d.ts +5 -0
  134. package/dist/plugins/caching-session-store.js +3 -0
  135. package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
  136. package/dist/plugins/file-resume-anchor-store.js +19 -5
  137. package/dist/plugins/file-run-store.d.ts +21 -8
  138. package/dist/plugins/file-run-store.js +60 -38
  139. package/dist/plugins/image-bake-store-sql.d.ts +0 -2
  140. package/dist/plugins/image-bake-store-sql.js +0 -5
  141. package/dist/plugins/local-session-store.d.ts +22 -0
  142. package/dist/plugins/local-session-store.js +74 -11
  143. package/dist/plugins/memory-run-store.d.ts +7 -5
  144. package/dist/plugins/memory-run-store.js +11 -4
  145. package/dist/plugins/one-time-migrations.d.ts +38 -1
  146. package/dist/plugins/one-time-migrations.js +143 -15
  147. package/dist/plugins/permission-rule-store-file.d.ts +9 -2
  148. package/dist/plugins/permission-rule-store-file.js +37 -51
  149. package/dist/plugins/pg-session-storage.d.ts +3 -0
  150. package/dist/plugins/pg-session-storage.js +19 -10
  151. package/dist/plugins/retention-store-sql.js +1 -0
  152. package/dist/plugins/run-store-sql.d.ts +15 -6
  153. package/dist/plugins/run-store-sql.js +15 -13
  154. package/dist/plugins/schema-indexes.js +1 -0
  155. package/dist/plugins/send-file-ledger.d.ts +5 -1
  156. package/dist/plugins/send-file-ledger.js +11 -6
  157. package/dist/plugins/session-event-payload.d.ts +14 -4
  158. package/dist/plugins/session-event-payload.js +5 -2
  159. package/dist/plugins/session-policy-store-sql.js +18 -5
  160. package/dist/plugins/store-backend.d.ts +13 -7
  161. package/dist/plugins/store-backend.js +10 -6
  162. package/dist/plugins/store-contracts.d.ts +7 -2
  163. package/dist/plugins/tidb-pool.js +12 -5
  164. package/dist/plugins/tidb-session-store.d.ts +6 -1
  165. package/dist/plugins/tidb-session-store.js +17 -10
  166. package/dist/project-identity.d.ts +4 -6
  167. package/dist/read-face-posture.d.ts +21 -0
  168. package/dist/read-face-posture.js +9 -0
  169. package/dist/request-key-closure.js +1 -1
  170. package/dist/request-value-closure.d.ts +72 -3
  171. package/dist/request-value-closure.js +12 -4
  172. package/dist/roster-bound-http-status.d.ts +6 -0
  173. package/dist/roster-bound-http-status.js +6 -0
  174. package/dist/rules-consent.js +0 -11
  175. package/dist/run-local.js +22 -27
  176. package/dist/runs.d.ts +22 -2
  177. package/dist/runs.js +29 -7
  178. package/dist/sealed-key.d.ts +31 -4
  179. package/dist/sealed-key.js +9 -0
  180. package/dist/security.d.ts +5 -1
  181. package/dist/session-children-retire.d.ts +92 -0
  182. package/dist/session-children-retire.js +85 -0
  183. package/dist/session-error-wire.d.ts +58 -0
  184. package/dist/session-error-wire.js +38 -0
  185. package/dist/session-incarnation.d.ts +61 -0
  186. package/dist/session-incarnation.js +43 -0
  187. package/dist/session-policy-wire.d.ts +98 -12
  188. package/dist/session-policy-wire.js +71 -8
  189. package/dist/session-records-skipped-notice.d.ts +20 -0
  190. package/dist/session-records-skipped-notice.js +27 -0
  191. package/dist/tool-approval.d.ts +65 -0
  192. package/dist/tool-approval.js +36 -9
  193. package/dist/trace/core-keyset-guard.d.ts +8 -7
  194. package/dist/trace/engine-notice-wire.d.ts +3 -1
  195. package/dist/trace/engine-notice-wire.js +2 -0
  196. package/dist/trace/project.d.ts +11 -6
  197. package/dist/trace/project.js +12 -3
  198. package/dist/trace/redact.d.ts +7 -0
  199. package/dist/trace/redact.js +153 -61
  200. package/dist/trace/wire-projection-faces.d.ts +10 -3
  201. package/dist/trace/wire-projection-faces.js +8 -2
  202. package/package.json +2 -2
  203. package/dist/plugins/memory-session-policy-store.d.ts +0 -46
  204. package/dist/plugins/memory-session-policy-store.js +0 -52
@@ -13,6 +13,7 @@ import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, Ques
13
13
  import type { ParkedReviveSurfaceFactory } from "./parked-revive-surface.js";
14
14
  import type { RebuiltInheritedGate } from "./boot/parked-revive-gate.js";
15
15
  import { type WorkflowRunParkKey, type WorkflowAgentSessionIndex } from "./orchestration/workflow-agent-session-index.js";
16
+ import { type SessionFaceReader } from "./session-incarnation.js";
16
17
  export interface ParkedAgentMatch {
17
18
  handle: string;
18
19
  row: BackgroundAgentRecord;
@@ -118,9 +119,13 @@ export interface ParkedDecideDeps {
118
119
  * checkpointStore 同实例,[ref] 设计稿 §4),以及与它同出一张列表的继承座 `mountsCallerTool`(一层行才有;属主与全部判据
119
120
  * 在 `parked-revive-surface.ts`)。修前是 boot 期的一只裸 Agent 工具、从不交座 ⇒ 7.32.0 起停在继承工具上的 allow 恒 422。 */
120
121
  reviveSurface: ParkedReviveSurfaceFactory;
121
- /** #1110 codex r1 [high]:会话属主面(`sessionStorage.ownerOf`)—— 赎回腿在 claim 前读**根会话现在**的属主交给赎回面(属主换人 / 会话
122
- * 已不在 ⇒ 不交座)。缺席 = 本部署无属主面 ⇒ 读数缺席 ⇒ 不交座(fail-closed)。读抛 ⇒ 原样上抛(claim 之前,没有东西要回滚)。 */
123
- sessionOwnerOf?: (sessionId: string) => Promise<string | null | undefined>;
122
+ /**
123
+ * S-790([ref] §4)—— 会话面读口(`sessionStorage.sessionFaceOf`:属主 + 化身一次读)。**必填**:本腿对根会话读两次 ——
124
+ * claim 之前一次、claim 成功之后一次 —— 会话不在或两次不是同一位住客 ⇒ 回滚 claim、答既有的 `409 decide.not_parked`;赎回面只在
125
+ * 属主相等且两次是同一位住客时交继承座。读抛 ⇒ claim 前原样上抛(没有东西要回滚),claim 后先回滚再上抛。
126
+ * (#1110 那只可选的 `sessionOwnerOf` 退役:可选 ⇒ 缺席时静默不复核,正是 S-790 那个窗的形。)
127
+ */
128
+ sessionFaceOf: SessionFaceReader;
124
129
  /** revive args 的 `subagent_type` 成员性判定集([ref] Q1 修正:非 roster 成员必须省略,否则
125
130
  * unknown subagent_type 拒;字面比对 general-purpose 在 roster 撞名时会错省)。缺省空集=恒省略。 */
126
131
  knownAgentTypes?: ReadonlySet<string>;
@@ -2,6 +2,7 @@ import { readRunParks } from "./orchestration/workflow-agent-session-index.js";
2
2
  import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
3
3
  import { backgroundScopesForCheckpointScope, decodeCheckpointScope } from "./security.js";
4
4
  import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
5
+ import { sameSessionOccupant } from "./session-incarnation.js";
5
6
  export async function findParkOrigin(sources, cp) {
6
7
  if (sources.agentStore !== undefined) {
7
8
  let match;
@@ -135,6 +136,16 @@ export function planWorkflowParkedDecide(origin, req) {
135
136
  `agent; completed steps replay from the journal. Do not re-issue the original call.`,
136
137
  };
137
138
  }
139
+ function sessionGone(taskId) {
140
+ return {
141
+ status: 409,
142
+ body: {
143
+ error: "the session this agent belongs to was deleted (or deleted and started again under the same id) while its approval waited, so the approval no longer applies — nothing was resumed",
144
+ errorCode: "decide.not_parked",
145
+ taskId,
146
+ },
147
+ };
148
+ }
138
149
  export async function decideParkedAgent(deps, req, match) {
139
150
  const row = match.row;
140
151
  const rememberRoot = req.grantRemember !== undefined ? (row.rootSessionId ?? row.parentSessionId) : undefined;
@@ -148,9 +159,14 @@ export async function decideParkedAgent(deps, req, match) {
148
159
  if (row.name === undefined) {
149
160
  return { status: 409, body: { error: "parked row carries no agent name — the revive path requires a named agent (row damaged?)", errorCode: "decide.row_unrevivable", taskId: match.handle } };
150
161
  }
162
+ const root = row.rootSessionId;
163
+ if (root === undefined) {
164
+ return { status: 409, body: { error: "this agent's row carries no root session anchor — the revive path cannot tell which session this approval belongs to (row damaged?)", errorCode: "decide.row_unrevivable", taskId: match.handle } };
165
+ }
166
+ const faceBefore = await deps.sessionFaceOf(root);
167
+ if (faceBefore === undefined)
168
+ return sessionGone(match.handle);
151
169
  const inheritedGate = deps.rebuildInheritedGate ? await deps.rebuildInheritedGate(row, { principal: decodeCheckpointScope(req.scope) }) : undefined;
152
- const rootOwner = row.rootSessionId !== undefined && deps.sessionOwnerOf ? await deps.sessionOwnerOf(row.rootSessionId) : undefined;
153
- const surface = deps.reviveSurface(row, { principal: decodeCheckpointScope(req.scope), ...(rootOwner !== undefined ? { rootOwner } : {}) });
154
170
  const stores = { agentStore: deps.agentStore, checkpointStore: deps.checkpointStore };
155
171
  const claim = await deps.registry.claimParkedAgent(stores, match.handle, { scope: row.scope, owner: row.owner });
156
172
  if (!claim.ok) {
@@ -167,6 +183,44 @@ export async function decideParkedAgent(deps, req, match) {
167
183
  }
168
184
  }
169
185
  const ticket = claim.ticket;
186
+ const quietWarn = (event, fields) => {
187
+ try {
188
+ deps.warn?.(event, fields);
189
+ }
190
+ catch {
191
+ }
192
+ };
193
+ const rollback = async (why) => {
194
+ try {
195
+ const disp = await deps.registry.rollbackParkedClaim(stores, ticket);
196
+ quietWarn("parked_decide_rolled_back", { taskId: match.handle, why, disposition: disp });
197
+ return disp;
198
+ }
199
+ catch (e) {
200
+ quietWarn("parked_decide_rollback_failed", { taskId: match.handle, why, error: e instanceof Error ? e.message : String(e) });
201
+ return "retry";
202
+ }
203
+ };
204
+ let faceAfter;
205
+ try {
206
+ faceAfter = await deps.sessionFaceOf(root);
207
+ }
208
+ catch (e) {
209
+ await rollback("session_face_unreadable");
210
+ throw e;
211
+ }
212
+ if (!sameSessionOccupant(faceBefore, faceAfter)) {
213
+ await rollback("session_occupant_changed");
214
+ return sessionGone(match.handle);
215
+ }
216
+ let surface;
217
+ try {
218
+ surface = deps.reviveSurface(row, { principal: decodeCheckpointScope(req.scope), root: { before: faceBefore, after: faceAfter } });
219
+ }
220
+ catch (e) {
221
+ await rollback("revive_surface_threw");
222
+ throw e;
223
+ }
170
224
  try {
171
225
  await req.onDecideCommitted?.();
172
226
  }
@@ -193,24 +247,6 @@ export async function decideParkedAgent(deps, req, match) {
193
247
  },
194
248
  },
195
249
  };
196
- const quietWarn = (event, fields) => {
197
- try {
198
- deps.warn?.(event, fields);
199
- }
200
- catch {
201
- }
202
- };
203
- const rollback = async (why) => {
204
- try {
205
- const disp = await deps.registry.rollbackParkedClaim(stores, ticket);
206
- quietWarn("parked_decide_rolled_back", { taskId: match.handle, why, disposition: disp });
207
- return disp;
208
- }
209
- catch (e) {
210
- quietWarn("parked_decide_rollback_failed", { taskId: match.handle, why, error: e instanceof Error ? e.message : String(e) });
211
- return "retry";
212
- }
213
- };
214
250
  defaultSubagentTailBus.forgetHandle(match.handle);
215
251
  let receipt;
216
252
  try {
@@ -11,10 +11,13 @@
11
11
  * 给那个会话的 run 组 `spec.tools` 的同一只工厂、同一个会话分区(任务清单店 `taskListStoreFor(root)` 与领队那一张是同一张;
12
12
  * 多租户的 WebFetch 过滤同样生效 ⇒ 子代同样拿不到)。座 = `(t) => tools.includes(t)`(身份判:同名副本不算)。
13
13
  * · **禁**用 boot 期单例填座:那只 bundle 没有会话,它的任务清单是一张私有表 —— 填了座,每个会话 / 租户的复活队友都会共用它。
14
- * · **根会话现在还属于这张卡的租户**才填座(codex r1 [high]):任务清单店只按会话 id 分区,而会话 id 由调用方自选、删后可被别人重领 ——
15
- * 根会话删了(或换了主人)之后赎回一张旧卡,按 id 重解出来的是**新主人**的清单。属主读数由赎回腿在 claim 前读(`identity.rootOwner`,
16
- * 读自会话属主面;缺席 = 会话已不在 / 读不出),与卡的权威 principal 逐字相等才填;否则不交座(fail-closed:子代不继承,继承工具上的
17
- * allow 回 422、审批待决,deny 照结)。根删除时把子孙的 parked 卡一并围栏 / 过期 = 另车(会话化身轴,[ref] 同族)。
14
+ * · **根会话现在还是这张卡的那一位住客**才填座(codex r1 [high] + S-790 加严):任务清单店只按会话 id 分区,而会话 id 由调用方自选、
15
+ * 删后可被别人(或同一个人)重领 —— 根会话删了(或删了重建)之后赎回一张旧卡,按 id 重解出来的是**下一位住客**的清单。判据两条都要:
16
+ * ① 根会话的属主与卡的权威 principal 逐字相等;② 赎回腿 claim 前 / 后两次读到的是**同一位住客**(化身逐字相等,
17
+ * `session-incarnation.ts` 的 `sameSessionOccupant`)—— 只比属主比不出「同一个人删了重建」。任一条不成立 / 读数缺席 ⇒ 不交座
18
+ * (fail-closed:子代不继承,继承工具上的 allow 回 422、审批待决,deny 照结)。赎回腿自己在两次读不一致时整条拒(`409 decide.not_parked`),
19
+ * 这里的第 ② 条是同一条事实在交座这一处的判据(规则只写在这里,腿只负责读两次)。根删除时把子孙的 parked 卡一并作废 = S-790 件 B
20
+ * (`boot/session-faces.ts` 的删除协调器)。
18
21
  * · 只对**一层**行填座(`rootSessionId` 在场且 = `parentSessionId`:派它的 run 就是根会话的 run,列表正是那个 run 的挂载)。更深的行
19
22
  * (孙代:派它的是一个子代,它的挂载是继承裁剪后的子集,行上读不出来)不填 —— 用 boot 期那只裸 Agent、无座 = core 的 fail-closed 臂;
20
23
  * 行上带继承组成是 core #1112(7.32.x)的半场,到货再改读行。
@@ -23,6 +26,7 @@
23
26
  */
24
27
  import { type BackgroundAgentRecord, type ToolSpec } from "@sema-agent/core";
25
28
  import type { Scenario } from "./capabilities/scenarios.js";
29
+ import { type SessionFace } from "./session-incarnation.js";
26
30
  /** 一次赎回用的面:驱动的 Agent 工具 + (一层行时)与它同出一张列表的继承座。 */
27
31
  export interface ParkedReviveSurface {
28
32
  readonly reviveTool: ToolSpec;
@@ -30,12 +34,15 @@ export interface ParkedReviveSurface {
30
34
  readonly mountsCallerTool?: (tool: ToolSpec) => boolean;
31
35
  }
32
36
  /**
33
- * 按行解析赎回面的入参身份:`principal` = 权威 principal(取 checkpoint 的 scope 列,不取行上有损的 `row.scope`);`rootOwner` = 行的根会话
34
- * **现在**的属主(赎回腿在 claim 前读会话属主面;`null` = 匿名会话,缺席 = 会话已不在 / 读不出 / 本部署无属主面)。
37
+ * 按行解析赎回面的入参身份:`principal` = 权威 principal(取 checkpoint 的 scope 列,不取行上有损的 `row.scope`);`root` = 赎回腿对行的
38
+ * 根会话读到的两次会话面(claim 之前 / 之后;属主 `null` = 匿名会话)。缺席 = 没有读数 ⇒ 不交座。
35
39
  */
36
40
  export interface ParkedReviveSurfaceIdentity {
37
41
  readonly principal: string | undefined;
38
- readonly rootOwner?: string | null;
42
+ readonly root?: {
43
+ readonly before: SessionFace;
44
+ readonly after: SessionFace;
45
+ };
39
46
  }
40
47
  /** 按行解析赎回面。 */
41
48
  export type ParkedReviveSurfaceFactory = (row: BackgroundAgentRecord, identity: ParkedReviveSurfaceIdentity) => ParkedReviveSurface;
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
2
+ import { sameSessionOccupant } from "./session-incarnation.js";
2
3
  const agentOf = (tools) => tools.find((t) => t.name === DEFAULT_SUBAGENT_TOOL_NAME);
3
4
  export function createParkedReviveSurface(defaultScenario) {
4
5
  const bootTool = agentOf(defaultScenario({}).tools);
@@ -8,7 +9,10 @@ export function createParkedReviveSurface(defaultScenario) {
8
9
  const root = row.rootSessionId;
9
10
  if (root === undefined || row.parentSessionId !== root)
10
11
  return { reviveTool: bootTool };
11
- if (identity.rootOwner === undefined || (identity.rootOwner ?? undefined) !== identity.principal)
12
+ const face = identity.root;
13
+ if (face === undefined || !sameSessionOccupant(face.before, face.after))
14
+ return { reviveTool: bootTool };
15
+ if ((face.before.owner ?? undefined) !== identity.principal || (face.after.owner ?? undefined) !== identity.principal)
12
16
  return { reviveTool: bootTool };
13
17
  const tools = defaultScenario({}, identity.principal, { sessionId: root }).tools;
14
18
  const reviveTool = agentOf(tools);
@@ -1,3 +1,4 @@
1
+ import type { Logger } from "../observability/logger.js";
1
2
  import type { AskState } from "../approval-ask-machine.js";
2
3
  import type { AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, DecideResult, EnsureAskResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
3
4
  /** 数据根下的目录名。**按店命名**,与兄弟 File 店同规(`approval-nonces` / `approval-ask-audit` /
@@ -120,7 +121,9 @@ export declare class FileApprovalAskStore implements ApprovalAskStore {
120
121
  *
121
122
  * @param root local 数据根(= `LocalBackend` 的 `FileStorageBackend.root`,零新 env)。
122
123
  */
123
- constructor(root: string);
124
+ constructor(root: string, opts?: {
125
+ warn?: Logger["warn"];
126
+ });
124
127
  /**
125
128
  * 内核的钟。窗口外读 ⇒ **抛**(不静默回落 `Date.now()`):窗口外的读数按定义不在账本上,重放会得出
126
129
  * 另一个时间戳 —— 那是一条**静默**的重放分岔,而本店的全部价值就是重放等价。今天内核的五个读钟点
@@ -1,7 +1,7 @@
1
1
  import { closeSync, copyFileSync, openSync, readSync, statSync, existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { AppendLog, atomicWriteFile, ensureDir, readJsonlRecords } from "@sema-agent/core";
4
- import { createSkippedRecordSink } from "../observability/corrupt-read-seat.js";
3
+ import { AppendLog, atomicWriteFile, ensureDir } from "@sema-agent/core";
4
+ import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
5
5
  import { recordFailOpen } from "../observability/fail-open.js";
6
6
  import { StoreBootRefusalError } from "./store-contracts.js";
7
7
  import { InMemoryApprovalAskStore, encodeDecideUpdatedInput, encodeStoreJsonPayload } from "./approval-ask-store-memory.js";
@@ -26,6 +26,10 @@ const JOURNAL_OP_WORDS = new Set(JOURNAL_OPS);
26
26
  export const JOURNAL_OP_TABLE_GATE = true;
27
27
  const JOURNAL_V1_CLAIM_INTENTS = ["expire", "cancel"];
28
28
  const JOURNAL_V1_CLAIM_INTENT_WORDS = new Set(JOURNAL_V1_CLAIM_INTENTS);
29
+ const ASK_JOURNAL_SHAPE = {
30
+ what: "approval-ask journal record",
31
+ read: (record) => record,
32
+ };
29
33
  function endsAtRecordBoundary(path) {
30
34
  const size = statSync(path).size;
31
35
  if (size === 0)
@@ -50,7 +54,7 @@ export class FileApprovalAskStore {
50
54
  replayRejectedCount = 0;
51
55
  corruptLineCount = 0;
52
56
  corruptQuarantinePath;
53
- constructor(root) {
57
+ constructor(root, opts) {
54
58
  const dir = join(root, APPROVAL_ASK_DIR);
55
59
  ensureDir(dir);
56
60
  this.tmpDir = join(dir, ".tmp");
@@ -58,10 +62,8 @@ export class FileApprovalAskStore {
58
62
  this.path = join(dir, APPROVAL_ASK_JOURNAL_FILE);
59
63
  this.core = new InMemoryApprovalAskStore({ now: () => this.clockRead() });
60
64
  const tornTail = existsSync(this.path) && !endsAtRecordBoundary(this.path);
61
- const records = readJsonlRecords(this.path, createSkippedRecordSink((info) => {
62
- this.corruptLineCount += 1;
63
- recordFailOpen("server.approval-ask-journal.corrupt-record-skipped", `path=${info.path} reason=${info.reason}`);
64
- }));
65
+ const { records, skipped } = replayJsonlSkipping(this.path, "approval-ask-journal", ASK_JOURNAL_SHAPE, opts?.warn);
66
+ this.corruptLineCount = skipped;
65
67
  for (const rec of records)
66
68
  this.replayRecord(rec);
67
69
  this.records = records.length;
@@ -127,11 +129,6 @@ export class FileApprovalAskStore {
127
129
  this.records = lines.length;
128
130
  }
129
131
  replayRecord(raw) {
130
- if (raw === null || typeof raw !== "object") {
131
- this.corruptLineCount += 1;
132
- recordFailOpen("server.approval-ask-journal.corrupt-record-skipped", `path=${this.path} reason=record is not an object`);
133
- return;
134
- }
135
132
  const probe = raw;
136
133
  const refuse = (why) => {
137
134
  throw new StoreBootRefusalError(`FileApprovalAskStore: approval ask journal ${this.path} carries a record this build cannot replay — ${why} ` +
@@ -270,6 +270,10 @@ export declare function assertIntegralEpochMs(field: string, value: number): voi
270
270
  export declare function assertIdempotencyKeyShape(key: string): void;
271
271
  /** `settled_by` 列宽 = 这一格的唯一尺(两方言 DDL 与下面的形门读同一个常量,不手抄第二份)。 */
272
272
  export declare const MAX_SETTLED_BY_CHARS = 16;
273
+ /** `settled_by` 这一列装得下这个词吗(1..{@link MAX_SETTLED_BY_CHARS})—— 形门 ① 的**谓词本身**,店门口
274
+ * ({@link assertDecideSettledBy})与耐久 ask 回决口的 CAS 前体门(S-843:确定的体形错误拒成 400,不落进提交歧义臂)
275
+ * 读同一只,尺不抄第二份。 */
276
+ export declare function settledByFitsColumn(v: string): boolean;
273
277
  /**
274
278
  * S-558 的**入店卫生门**(三 twin 同判,与 {@link assertIdempotencyKeyShape} / {@link assertIntegralEpochMs} 同族)。
275
279
  * 判的是**形**,不是**词** —— 词表属主恒是 core(`resolveAsk` 对自报词表外的值是响亮 fail-closed deny),
@@ -14,11 +14,14 @@ export function assertIdempotencyKeyShape(key) {
14
14
  }
15
15
  }
16
16
  export const MAX_SETTLED_BY_CHARS = 16;
17
+ export function settledByFitsColumn(v) {
18
+ return v.length > 0 && v.length <= MAX_SETTLED_BY_CHARS;
19
+ }
17
20
  export function assertDecideSettledBy(decision) {
18
21
  const v = decision.settledBy;
19
22
  if (v === undefined)
20
23
  return;
21
- if (v.length === 0 || v.length > MAX_SETTLED_BY_CHARS) {
24
+ if (!settledByFitsColumn(v)) {
22
25
  throw new Error(`invalid settledBy: the settlement source is one word from the engine's closed set and lands in a VARCHAR(${MAX_SETTLED_BY_CHARS}) column — must be 1-${MAX_SETTLED_BY_CHARS} chars (got ${JSON.stringify(v.slice(0, 32))}${v.length > 32 ? "…" : ""}, length ${v.length})`);
23
26
  }
24
27
  if (decision.decision !== "deny") {
@@ -1,5 +1,6 @@
1
1
  import type { Pool as MysqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
+ import type { Logger } from "../observability/logger.js";
3
4
  export interface ApprovalExemptionRow {
4
5
  toolName: string;
5
6
  grantedBy: string | null;
@@ -43,11 +44,30 @@ export declare class MemoryApprovalExemptionStore implements ApprovalExemptionSt
43
44
  revoke(sessionId: string, toolName: string): Promise<boolean>;
44
45
  deleteBySession(sessionId: string): Promise<number>;
45
46
  }
47
+ /**
48
+ * 🔴 S-582 —— **fail-closed per session file.** A record in the MIDDLE of a session's log that cannot be read (bit rot,
49
+ * a hand edit, a line `42`, an object missing `toolName`) used to be skipped silently. When that line was a REVOKE
50
+ * tombstone, the grant before it survived the replay: a "don't ask again" the operator had revoked came back and the
51
+ * tool ran WITHOUT asking — the fail-open direction on the approval axis. We cannot tell which fact the line held, so
52
+ * the replay owner (`observability/corrupt-read-seat.ts`, seat `approval-exemption`) discloses it and closes that
53
+ * file's gate: `has` / `list` / `grant` / `revoke` for THAT session reject with `JsonlStoreCorruptError`, and every
54
+ * consumer already treats a rejected `has` as "not exempt" (ask anyway). Other sessions are untouched (one file = one
55
+ * session's facts). `deleteBySession` is NOT gated: right-to-delete must not be blocked by a bad byte, and deleting
56
+ * the file is exactly the remedy — it clears the gate with the file.
57
+ */
46
58
  export declare class FileApprovalExemptionStore implements ApprovalExemptionStore {
47
59
  private readonly dir;
48
60
  private readonly bySession;
49
61
  private readonly logs;
50
- constructor(root: string);
62
+ /** S-582: sanitized session key → that session log's gate (only files that replayed a bad record are listed). */
63
+ private readonly corruptGates;
64
+ private readonly warn;
65
+ /** @param opts.warn corrupt-read disclosure seat (`LocalBackend` wires `discloseWarn`; absent ⇒ the owner falls back to `console.warn`, never to silence). */
66
+ constructor(root: string, opts?: {
67
+ warn?: Logger["warn"];
68
+ });
69
+ /** S-582: first line of every judging / appending method (see the class note). */
70
+ private assertUsable;
51
71
  private pathFor;
52
72
  /** Replay every session log (torn-tail-safe; last-wins with revoke tombstones). Index keyed by the SANITIZED id. */
53
73
  private hydrate;
@@ -1,6 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync, readdirSync, rmSync } from "node:fs";
3
- import { AppendLog, ensureDir, readJsonlRecords, sanitizePathComponent } from "@sema-agent/core";
3
+ import { AppendLog, ensureDir, sanitizePathComponent } from "@sema-agent/core";
4
+ import { replayJsonlGuarded } from "../observability/corrupt-read-seat.js";
4
5
  export class TiDBApprovalExemptionStore {
5
6
  pool;
6
7
  constructor(pool) {
@@ -98,15 +99,36 @@ export class MemoryApprovalExemptionStore {
98
99
  return n;
99
100
  }
100
101
  }
102
+ const EXEMPTION_RECORD_SHAPE = {
103
+ what: "approval-exemption record without a string toolName (or with a mistyped revoked / grantedBy / ts)",
104
+ read(record) {
105
+ const { toolName, revoked, grantedBy, ts } = record;
106
+ if (typeof toolName !== "string")
107
+ return undefined;
108
+ if (!(revoked === undefined || typeof revoked === "boolean"))
109
+ return undefined;
110
+ if (!(grantedBy === undefined || grantedBy === null || typeof grantedBy === "string"))
111
+ return undefined;
112
+ if (!(ts === undefined || typeof ts === "string"))
113
+ return undefined;
114
+ return { toolName, grantedBy: grantedBy ?? null, ts: ts ?? "", ...(revoked !== undefined ? { revoked } : {}) };
115
+ },
116
+ };
101
117
  export class FileApprovalExemptionStore {
102
118
  dir;
103
119
  bySession = new Map();
104
120
  logs = new Map();
105
- constructor(root) {
121
+ corruptGates = new Map();
122
+ warn;
123
+ constructor(root, opts) {
106
124
  this.dir = join(root, "approval-exemptions");
125
+ this.warn = opts?.warn;
107
126
  ensureDir(this.dir);
108
127
  this.hydrate();
109
128
  }
129
+ assertUsable(key) {
130
+ this.corruptGates.get(key)?.assertUsable();
131
+ }
110
132
  pathFor(sessionId) {
111
133
  return join(this.dir, `${sanitizePathComponent(sessionId)}.jsonl`);
112
134
  }
@@ -122,13 +144,13 @@ export class FileApprovalExemptionStore {
122
144
  if (!name.endsWith(".jsonl"))
123
145
  continue;
124
146
  const key = name.slice(0, -".jsonl".length);
125
- const records = readJsonlRecords(join(this.dir, name));
147
+ const { records, gate } = replayJsonlGuarded(join(this.dir, name), "approval-exemption", EXEMPTION_RECORD_SHAPE, this.warn);
148
+ if (gate.tripped)
149
+ this.corruptGates.set(key, gate);
126
150
  if (!records.length)
127
151
  continue;
128
152
  const m = this.bySession.get(key) ?? new Map();
129
153
  for (const r of records) {
130
- if (!r || typeof r.toolName !== "string")
131
- continue;
132
154
  if (r.revoked) {
133
155
  m.delete(r.toolName);
134
156
  }
@@ -154,6 +176,7 @@ export class FileApprovalExemptionStore {
154
176
  }
155
177
  async grant(sessionId, toolName, grantedBy) {
156
178
  const key = sanitizePathComponent(sessionId);
179
+ this.assertUsable(key);
157
180
  this.logFor(sessionId).append({ toolName, grantedBy: grantedBy ?? null, ts: new Date().toISOString() }, true);
158
181
  let m = this.bySession.get(key);
159
182
  if (!m) {
@@ -164,10 +187,14 @@ export class FileApprovalExemptionStore {
164
187
  m.set(toolName, { grantedBy, createdAt: prior?.createdAt ?? Date.now() });
165
188
  }
166
189
  async has(sessionId, toolName) {
167
- return this.bySession.get(sanitizePathComponent(sessionId))?.has(toolName) ?? false;
190
+ const key = sanitizePathComponent(sessionId);
191
+ this.assertUsable(key);
192
+ return this.bySession.get(key)?.has(toolName) ?? false;
168
193
  }
169
194
  async list(sessionId) {
170
- const m = this.bySession.get(sanitizePathComponent(sessionId));
195
+ const key = sanitizePathComponent(sessionId);
196
+ this.assertUsable(key);
197
+ const m = this.bySession.get(key);
171
198
  if (!m)
172
199
  return [];
173
200
  return [...m.entries()]
@@ -176,6 +203,7 @@ export class FileApprovalExemptionStore {
176
203
  }
177
204
  async revoke(sessionId, toolName) {
178
205
  const key = sanitizePathComponent(sessionId);
206
+ this.assertUsable(key);
179
207
  const m = this.bySession.get(key);
180
208
  if (!m?.has(toolName))
181
209
  return false;
@@ -200,6 +228,7 @@ export class FileApprovalExemptionStore {
200
228
  throw new Error(`approval-exemption purge failed for ${sessionId}: ${e instanceof Error ? e.message : String(e)}`);
201
229
  }
202
230
  this.bySession.delete(key);
231
+ this.corruptGates.delete(key);
203
232
  return removed;
204
233
  }
205
234
  dispose() {
@@ -1,6 +1,7 @@
1
1
  import type { Pool as MysqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
3
  import { type SqlDriver } from "./sql-driver.js";
4
+ import type { Logger } from "../observability/logger.js";
4
5
  /** 表名(单数形;`test/schema-naming-invariants.test.ts` 的闭集词表钉着)。 */
5
6
  export declare const APPROVAL_NONCE_TABLE = "approval_nonce";
6
7
  /**
@@ -71,7 +72,15 @@ export declare class MemoryApprovalNonceStore implements ApprovalNonceStore {
71
72
  *
72
73
  * 崩溃语义:`append` 走 fsync。丢一条记录 = 那个 nonce 在它剩下的 TTL 里可被重放一次(与「整台副本重启」
73
74
  * 的既有暴露同级,且 SQL 车道不受此影响);**绝不**能出现的是反向 —— 记录还在却被读成不在,那要靠
74
- * `readJsonlRecords` 的断尾安全(半行丢弃)兜住。
75
+ * core JSONL 读法的断尾安全(半行丢弃)兜住。
76
+ *
77
+ * 🔴 **S-582:中间一行读不出 ⇒ 整本拒用(fail-closed)**。撕尾是崩溃的正常形(core 丢弃且不报);而**中间**
78
+ * 那一行读不出(位腐 / 手改 / 一行 `42` / 缺键的对象)时,我们不知道它是哪一只 nonce —— 修前静默跳过它,等于
79
+ * 让那只已消费的证明再被受理一次(S-280 要关的重放洞换了个入口)。现在由属主披露(`approval_nonce_corrupt_read`)
80
+ * 并合上 {@link gate}:`consume` / `reapExpired` 第一行即带码拒(`JsonlStoreCorruptError`,外层答 500);`nowMs` 不读账、
81
+ * 不经闸。清扫同样要拒:压实会把幸存者原样写回、把坏行连同证据一起抹掉,下一次重启读到的就是一本「干净」的账 ——
82
+ * 闸自己打开,而那只 nonce 仍然没被记住。出路在拒绝句里(移开文件的代价与安全时机)。
83
+ * 代价如实:这台 local 机器上直连门的 plan_review 在有人处置之前全部 500(`/decide` 不经本店,不受影响)。
75
84
  */
76
85
  export declare class FileApprovalNonceStore implements ApprovalNonceStore {
77
86
  private readonly path;
@@ -80,7 +89,12 @@ export declare class FileApprovalNonceStore implements ApprovalNonceStore {
80
89
  * 从复合键里**解析**出来是一条没必要的反向铸形 —— 记录留着就不必解析。 */
81
90
  private readonly rows;
82
91
  private log;
83
- constructor(root: string);
92
+ /** S-582:本账的损坏闸(见类注)。 */
93
+ private readonly gate;
94
+ /** @param opts.warn 腐读披露座(`LocalBackend` 接 `discloseWarn`;缺席 ⇒ 属主退回 `console.warn`,不退回沉默)。 */
95
+ constructor(root: string, opts?: {
96
+ warn?: Logger["warn"];
97
+ });
84
98
  private logFor;
85
99
  /** local 车道:店 = 这台机器上的这个数据根,所以店钟就是本进程的墙钟(理由同 Memory 腿)。 */
86
100
  nowMs(): Promise<number>;
@@ -1,7 +1,8 @@
1
1
  import { join } from "node:path";
2
- import { atomicWriteFile, ensureDir, readJsonlRecords, AppendLog } from "@sema-agent/core";
2
+ import { atomicWriteFile, ensureDir, AppendLog } from "@sema-agent/core";
3
3
  import { mysqlDriver, pgDriver, sqlDbNowMs, SQL_DB_NOW_MS } from "./sql-driver.js";
4
4
  import { isDupKeyError } from "./sql-errors.js";
5
+ import { replayJsonlGuarded } from "../observability/corrupt-read-seat.js";
5
6
  export const APPROVAL_NONCE_TABLE = "approval_nonce";
6
7
  export const MAX_APPROVAL_NONCE_CHARS = 190;
7
8
  export class SqlApprovalNonceStore {
@@ -66,22 +67,29 @@ export class MemoryApprovalNonceStore {
66
67
  return n;
67
68
  }
68
69
  }
70
+ const NONCE_RECORD_SHAPE = {
71
+ what: "approval-nonce record without a string s / string n / numeric e",
72
+ read(record) {
73
+ const { s, n, e } = record;
74
+ return typeof s === "string" && typeof n === "string" && typeof e === "number" ? { s, n, e } : undefined;
75
+ },
76
+ };
69
77
  export class FileApprovalNonceStore {
70
78
  path;
71
79
  tmpDir;
72
80
  rows = new Map();
73
81
  log;
74
- constructor(root) {
82
+ gate;
83
+ constructor(root, opts) {
75
84
  const dir = join(root, "approval-nonces");
76
85
  ensureDir(dir);
77
86
  this.tmpDir = join(dir, ".tmp");
78
87
  ensureDir(this.tmpDir);
79
88
  this.path = join(dir, "consumed.jsonl");
80
- for (const r of readJsonlRecords(this.path)) {
81
- if (!r || typeof r.s !== "string" || typeof r.n !== "string" || typeof r.e !== "number")
82
- continue;
83
- this.rows.set(keyOf(r.s, r.n), { s: r.s, n: r.n, e: r.e });
84
- }
89
+ const { records, gate } = replayJsonlGuarded(this.path, "approval-nonce", NONCE_RECORD_SHAPE, opts?.warn);
90
+ this.gate = gate;
91
+ for (const r of records)
92
+ this.rows.set(keyOf(r.s, r.n), r);
85
93
  }
86
94
  logFor() {
87
95
  return (this.log ??= new AppendLog(this.path));
@@ -90,6 +98,7 @@ export class FileApprovalNonceStore {
90
98
  return Date.now();
91
99
  }
92
100
  async consume(sessionId, nonce, expiresAtMs) {
101
+ this.gate.assertUsable();
93
102
  const k = keyOf(sessionId, nonce);
94
103
  if (this.rows.has(k))
95
104
  return false;
@@ -99,6 +108,7 @@ export class FileApprovalNonceStore {
99
108
  return true;
100
109
  }
101
110
  async reapExpired() {
111
+ this.gate.assertUsable();
102
112
  const nowMs = await this.nowMs();
103
113
  const survivors = [];
104
114
  let removed = 0;
@@ -3,6 +3,7 @@ import type { SessionListItem } from "../security.js";
3
3
  import type { StagingHandle } from "../session-sync-kernel.js";
4
4
  import { type PlacementAcquireOpts } from "./session-placement.js";
5
5
  import type { InstructionSourceStore } from "../instruction-source-notice.js";
6
+ import type { SessionFaceReader } from "../session-incarnation.js";
6
7
  export interface CachingOptions {
7
8
  /** Idle seconds a woken session is kept warm. 0 disables caching (pure passthrough). */
8
9
  ttlSec: number;
@@ -37,6 +38,10 @@ export declare class CachingSessionStore implements SessionStore {
37
38
  private readonly timer?;
38
39
  /** Delegated tenant-ownership ops, present only if the inner store exposes them (the TiDB store does). */
39
40
  readonly ownerOf?: (sessionId: string) => Promise<string | null | undefined>;
41
+ /** S-790:属主 + 化身一次读,纯转发(不经暖缓存 —— 化身判的就是「此刻坐在座位上的是谁」,缓存的旧答案正是要消灭的那一形)。
42
+ * 🔴 必须转发(与 retention / placements / instruction-source 同一条教训):生产默认把真店包进本类,不转发 = parked 决定腿
43
+ * 与记忆座在默认部署上读不到化身。 */
44
+ readonly sessionFaceOf?: SessionFaceReader;
40
45
  readonly register?: (sessionId: string, owner: string | null) => Promise<void>;
41
46
  /** Delegated SessionRepo seam (§0.5) — list/fork/delete operate on the DURABLE tables directly (they bypass the
42
47
  * warm cache by design; a fork/delete must read/write durable state, not a possibly-stale cached view). */
@@ -6,6 +6,7 @@ export class CachingSessionStore {
6
6
  ttlMs;
7
7
  timer;
8
8
  ownerOf;
9
+ sessionFaceOf;
9
10
  register;
10
11
  listSessions;
11
12
  fork;
@@ -45,6 +46,8 @@ export class CachingSessionStore {
45
46
  const ownerAware = inner;
46
47
  if (ownerAware.ownerOf)
47
48
  this.ownerOf = ownerAware.ownerOf.bind(inner);
49
+ if (ownerAware.sessionFaceOf)
50
+ this.sessionFaceOf = ownerAware.sessionFaceOf.bind(inner);
48
51
  if (ownerAware.register)
49
52
  this.register = ownerAware.register.bind(inner);
50
53
  if (ownerAware.listSessions)
@@ -1,13 +1,18 @@
1
+ import type { Logger } from "../observability/logger.js";
1
2
  export declare class FileResumeAnchorStore {
2
3
  private readonly dir;
3
4
  /** sessionId → (eventId → {entryId, owner}) — the same in-memory shape MemoryResumeAnchorStore holds, hydrated on ctor. */
4
5
  private readonly bySession;
5
6
  /** sessionId → the open append log (one O_APPEND fd per session, lazily opened on first put, held for lifetime). */
6
7
  private readonly logs;
7
- constructor(root: string);
8
+ private readonly warn;
9
+ /** @param opts.warn corrupt-read disclosure seat (S-582; absent ⇒ the replay owner falls back to `console.warn`). */
10
+ constructor(root: string, opts?: {
11
+ warn?: Logger["warn"];
12
+ });
8
13
  /** `<dir>/<sanitizePathComponent(sessionId)>.jsonl` — the per-session log path (sessionId is uuidv7, already safe). */
9
14
  private pathFor;
10
- /** Replay every session log into the in-memory index on boot (torn-tail-safe via readJsonlRecords; last-wins upsert
15
+ /** Replay every session log into the in-memory index on boot (torn-tail-safe via the replay owner; last-wins upsert
11
16
  * so a re-stamped eventId reflects the latest appended record). The in-memory index is keyed by the SANITIZED
12
17
  * sessionId throughout (put/resolve/list/delete all sanitize), and each on-disk file's base name IS
13
18
  * `sanitizePathComponent(sessionId)` — so the index key is just the file base, with no de-sanitize needed. */