@sema-agent/server 7.141.0 → 7.142.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 (171) hide show
  1. package/MIGRATION.md +215 -0
  2. package/USAGE.md +67 -12
  3. package/dist/approval-ask-audit-store.d.ts +54 -38
  4. package/dist/approval-ask-audit-store.js +58 -39
  5. package/dist/approval-card.d.ts +8 -8
  6. package/dist/approval-crash-convergence.d.ts +17 -7
  7. package/dist/approval-crash-convergence.js +94 -22
  8. package/dist/approval-deny-reasons.d.ts +34 -30
  9. package/dist/approval-deny-reasons.js +21 -6
  10. package/dist/approval-policy-settlement.d.ts +1 -1
  11. package/dist/approval-reconciler.d.ts +33 -1
  12. package/dist/approval-reconciler.js +43 -40
  13. package/dist/auth-bridge.d.ts +9 -6
  14. package/dist/auth-bridge.js +9 -3
  15. package/dist/boot/adoption.js +2 -1
  16. package/dist/boot/budget-tracing.d.ts +16 -0
  17. package/dist/boot/budget-tracing.js +10 -6
  18. package/dist/boot/device-lane.d.ts +1 -1
  19. package/dist/boot/execution-env.d.ts +24 -4
  20. package/dist/boot/execution-env.js +26 -23
  21. package/dist/boot/execution-lane.js +2 -1
  22. package/dist/boot/exit-boundary.js +5 -4
  23. package/dist/boot/memory-consolidation.js +2 -1
  24. package/dist/boot/parked-revive-gate.js +2 -1
  25. package/dist/boot/permission-rules-audit.d.ts +0 -28
  26. package/dist/boot/permission-rules-audit.js +2 -1
  27. package/dist/boot/read-deny-anchor-audit.js +2 -1
  28. package/dist/boot/reapers.js +4 -3
  29. package/dist/boot/resolve-spec.js +11 -2
  30. package/dist/boot/retention-lane.js +5 -3
  31. package/dist/boot/runner-deps.js +16 -13
  32. package/dist/boot/runtime-caps.js +3 -2
  33. package/dist/boot/session-faces.js +4 -3
  34. package/dist/boot/shutdown.js +3 -2
  35. package/dist/boot/stage-04-stores.js +3 -2
  36. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  37. package/dist/boot/stage-05-execution-env.js +8 -6
  38. package/dist/boot/stage-06-runners.d.ts +1 -1
  39. package/dist/boot/stage-06-runners.js +2 -1
  40. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  41. package/dist/boot/stage-07-capability-layer.js +2 -1
  42. package/dist/boot/stage-08-reapers.d.ts +1 -1
  43. package/dist/boot/stage-08-reapers.js +3 -2
  44. package/dist/boot/stage-09-leader.d.ts +1 -1
  45. package/dist/boot/stage-10-http-server.d.ts +1 -1
  46. package/dist/boot/stage-10-http-server.js +5 -4
  47. package/dist/boot/stores.js +3 -2
  48. package/dist/boot/web-admin.js +17 -17
  49. package/dist/boot/webfetch-summarize-lane.js +2 -1
  50. package/dist/boot/workflow-orchestration.js +3 -2
  51. package/dist/capabilities/builtin-tools.d.ts +2 -1
  52. package/dist/capabilities/builtin-tools.js +2 -4
  53. package/dist/capabilities/composite-members.d.ts +2 -1
  54. package/dist/capabilities/composite-members.js +2 -1
  55. package/dist/capability-facts.d.ts +24 -0
  56. package/dist/capability-facts.js +23 -0
  57. package/dist/config-catalog.js +1 -0
  58. package/dist/config-types.d.ts +15 -4
  59. package/dist/config.d.ts +6 -0
  60. package/dist/config.js +17 -3
  61. package/dist/execution-lane-caps.d.ts +2 -1
  62. package/dist/fleet/fleet-bus.d.ts +6 -3
  63. package/dist/hooks/hook-runner.d.ts +6 -0
  64. package/dist/hooks/hook-runner.js +30 -1
  65. package/dist/http/dispatch.js +4 -2
  66. package/dist/http/execution-context.d.ts +2 -2
  67. package/dist/http/idempotency.d.ts +1 -1
  68. package/dist/http/principal-gate.d.ts +3 -1
  69. package/dist/http/principal-gate.js +17 -8
  70. package/dist/http/resume-legs.js +5 -9
  71. package/dist/http/route-table.js +2 -0
  72. package/dist/http/routes/attachments.js +18 -0
  73. package/dist/http/routes/capabilities.js +5 -2
  74. package/dist/http/routes/memory-policy.js +26 -6
  75. package/dist/http/routes/runs.js +3 -3
  76. package/dist/http/routes/session-memory-sync.d.ts +63 -0
  77. package/dist/http/routes/session-memory-sync.js +157 -0
  78. package/dist/http/routes/sessions-list.d.ts +2 -1
  79. package/dist/http/routes/tasks.js +7 -5
  80. package/dist/http/run-read-face.d.ts +7 -0
  81. package/dist/http/run-read-face.js +9 -3
  82. package/dist/http/send.d.ts +5 -0
  83. package/dist/http/send.js +9 -2
  84. package/dist/http/server.d.ts +14 -8
  85. package/dist/http/server.js +2 -4
  86. package/dist/http/submit-replay.d.ts +4 -3
  87. package/dist/http/submit-replay.js +2 -2
  88. package/dist/http/sync-submit-body.d.ts +21 -4
  89. package/dist/http/sync-submit-body.js +13 -0
  90. package/dist/identity-source.d.ts +21 -2
  91. package/dist/identity-source.js +6 -0
  92. package/dist/memory-incident-sink.d.ts +19 -6
  93. package/dist/memory-incident-sink.js +8 -4
  94. package/dist/memory-sync-client.d.ts +21 -21
  95. package/dist/memory-sync-client.js +50 -42
  96. package/dist/memory-sync-identity.d.ts +24 -0
  97. package/dist/memory-sync-identity.js +57 -0
  98. package/dist/memory-sync.d.ts +7 -1
  99. package/dist/memory-sync.js +6 -3
  100. package/dist/observability/fail-open.d.ts +27 -10
  101. package/dist/observability/fail-open.js +20 -7
  102. package/dist/observability/run-terminal.d.ts +92 -34
  103. package/dist/observability/run-terminal.js +167 -162
  104. package/dist/plugins/approval-ask-store-file.d.ts +14 -4
  105. package/dist/plugins/approval-ask-store-file.js +48 -7
  106. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  107. package/dist/plugins/approval-ask-store-memory.js +9 -8
  108. package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
  109. package/dist/plugins/approval-ask-store-sql.js +49 -19
  110. package/dist/plugins/file-run-store.d.ts +6 -1
  111. package/dist/plugins/file-run-store.js +11 -1
  112. package/dist/plugins/local-session-store.d.ts +36 -7
  113. package/dist/plugins/local-session-store.js +76 -32
  114. package/dist/plugins/local-task-attachment-store.d.ts +12 -2
  115. package/dist/plugins/local-task-attachment-store.js +67 -43
  116. package/dist/plugins/memory-key-guards.d.ts +3 -3
  117. package/dist/plugins/memory-run-store.d.ts +6 -1
  118. package/dist/plugins/memory-run-store.js +11 -1
  119. package/dist/plugins/one-time-migrations.d.ts +27 -1
  120. package/dist/plugins/one-time-migrations.js +57 -3
  121. package/dist/plugins/remote-env-e2b.d.ts +23 -4
  122. package/dist/plugins/remote-env-e2b.js +111 -28
  123. package/dist/plugins/remote-env-k8s.d.ts +31 -5
  124. package/dist/plugins/remote-env-k8s.js +86 -19
  125. package/dist/plugins/remote-env-local-docker.d.ts +15 -3
  126. package/dist/plugins/remote-env-local-docker.js +40 -12
  127. package/dist/plugins/run-store-sql.d.ts +9 -1
  128. package/dist/plugins/run-store-sql.js +9 -1
  129. package/dist/plugins/store-backend.d.ts +10 -7
  130. package/dist/plugins/store-backend.js +1 -0
  131. package/dist/plugins/store-contracts.d.ts +26 -1
  132. package/dist/plugins/store-contracts.js +3 -0
  133. package/dist/plugins/task-attachment-store.d.ts +112 -14
  134. package/dist/plugins/task-attachment-store.js +221 -28
  135. package/dist/plugins/workspace-fill.d.ts +60 -0
  136. package/dist/plugins/workspace-fill.js +106 -0
  137. package/dist/project-bundle.d.ts +38 -0
  138. package/dist/project-bundle.js +609 -0
  139. package/dist/run-local.js +2 -8
  140. package/dist/runs.js +1 -2
  141. package/dist/sandbox-egress-policy.d.ts +4 -1
  142. package/dist/sandbox-egress-policy.js +3 -1
  143. package/dist/sandbox-git-plane.d.ts +31 -12
  144. package/dist/sandbox-git-plane.js +24 -4
  145. package/dist/security.d.ts +9 -0
  146. package/dist/security.js +8 -0
  147. package/dist/session-error-wire.d.ts +24 -0
  148. package/dist/session-error-wire.js +7 -0
  149. package/dist/task-settings.d.ts +10 -2
  150. package/dist/task-settings.js +15 -2
  151. package/dist/terminal.d.ts +39 -19
  152. package/dist/terminal.js +65 -22
  153. package/dist/tool-approval.d.ts +4 -3
  154. package/dist/tool-approval.js +12 -14
  155. package/dist/trace/core-keyset-guard.d.ts +123 -6
  156. package/dist/trace/engine-notice-wire.d.ts +8 -2
  157. package/dist/trace/engine-notice-wire.js +4 -2
  158. package/dist/trace/exit-seat.d.ts +2 -1
  159. package/dist/trace/ledger-sink.d.ts +8 -4
  160. package/dist/trace/ledger-sink.js +2 -2
  161. package/dist/trace/project.d.ts +18 -1
  162. package/dist/trace/project.js +16 -0
  163. package/dist/trace/self-minted-wire.d.ts +251 -2
  164. package/dist/trace/self-minted-wire.js +82 -0
  165. package/dist/trace/wire-projection-faces.d.ts +1 -1
  166. package/dist/trace/wire-projection-faces.js +1 -1
  167. package/dist/web-admin-client/apply-effective.js +21 -1
  168. package/dist/web-admin-client/effective-keys.js +2 -0
  169. package/dist/web-admin-client/types.d.ts +4 -0
  170. package/package.json +1 -1
  171. package/wire-changes/server@7.142.0.json +639 -0
@@ -42,7 +42,7 @@ import type { PgQueryFn } from "./pg-query.js";
42
42
  import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
43
43
  import { type AskState, type BatchState } from "../approval-ask-machine.js";
44
44
  import type { HostSettledBy } from "../host-decision.js";
45
- import { type AskRowCause, type SettleCause } from "../approval-deny-reasons.js";
45
+ import { type AskRowEndingCause, type SettleCause, type ParkTriggerCause } from "../approval-deny-reasons.js";
46
46
  import { type IndexSpec } from "./ensure-index.js";
47
47
  import { type ExecutionLane } from "../execution-lane-caps.js";
48
48
  export type AskDecision = "approve" | "deny";
@@ -132,7 +132,16 @@ export interface AskRow {
132
132
  */
133
133
  updatedInput?: unknown;
134
134
  decidedAtMs: number | null;
135
- deniedReason: string | null;
135
+ /**
136
+ * `ending_cause` 列原值 —— **终态成因**:这一行为什么没被人答就结束了(到 `VOID` / `DENIED` 的转移带着它写:取消 claim、店内撤兄弟 / bind-once 落选 / 批中止、对账器、起服收敛;没带的转移 ⇒ 保持 `null`)。
137
+ * 只经 {@link askRowEndingCause} 读(词表 + 行不变式在那里);`null` = 没写归因。
138
+ */
139
+ endingCause: string | null;
140
+ /**
141
+ * `park_trigger` 列原值 —— **转 park 的触发成因**:这一行为什么被转进投递面。**唯一写者** = 赢下 `STREAM_PENDING → PARKING` 的那一次 `claimTerminal(expire, { cause })`,
142
+ * 此后不再被写(行走到 `PARKED` / `DENIED` / `VOID` 之后它仍在;{@link AskTransitionPatch} 没有这一键)。只经 {@link askRowParkTrigger} 读;`null` = 没写(不带 cause 的转 park)。
143
+ */
144
+ parkTrigger: string | null;
136
145
  /** PARKED 坐标三件。 */
137
146
  gateToken: string | null;
138
147
  gateBoundCallId: string | null;
@@ -222,7 +231,9 @@ export interface AskTransitionPatch {
222
231
  decisionActor?: unknown | null;
223
232
  decisionNote?: string | null;
224
233
  decidedAtMs?: number | null;
225
- deniedReason?: string | null;
234
+ /** 终态成因(值域与终局 claim 的 `cause` 同一型 {@link SettleCause};读口按终态成因词表读,且只在 `VOID` / `DENIED` 上作数 —— 别的转移上写了也读成缺席)。
235
+ * 补丁**没有**触发成因那一键:它只由 `claimTerminal(expire, { cause })` 写一次。 */
236
+ endingCause?: SettleCause;
226
237
  gateToken?: string | null;
227
238
  gateBoundCallId?: string | null;
228
239
  gateBoundInputHash?: string | null;
@@ -330,9 +341,10 @@ export interface ExpireResult {
330
341
  */
331
342
  export type AskTerminalClaimIntent = "expire" | "cancel" | "refuse";
332
343
  /**
333
- * 终局 claim 的可选实参:`cause` —— **赢下 `cancel`(VOID)/ `expire`(PARKING)时写进这一行 `denied_reason` 列的成因词**(决断口 410 / 晚答 404 / 跨副本的败方读数读同一列,
334
- * 所以这一行自己记着「为什么」,读面不靠任何进程内的东西)。`cancel` 写终态成因({@link SettleCause} 里的 `run_cancelled` / `run_exited`),
335
- * `expire` 写转 park 的触发成因({@link ParkTriggerCause}:`ttl_expired` / `broadcast_lost` / `stream_disconnected` / `undeliverable` / `shutdown_before_park`);缺席 ⇒ 列保持 NULL(读面缺席即缺席)。
344
+ * 终局 claim 的可选实参:`cause` —— **赢下终局的同一步写进这一行的成因词**,列由意图定({@link claimCauseColumn}):`cancel`(VOID)写 `ending_cause` = 终态成因
345
+ * ({@link SettleCause} 里的 `run_cancelled` / `run_exited`;决断口 410 / 晚答 404 / 跨副本的败方读数读这一列),`expire`(PARKING)写 `park_trigger` = 转 park 的触发成因
346
+ * ({@link ParkTriggerCause}:`ttl_expired` / `broadcast_lost` / `stream_disconnected` / `undeliverable` / `shutdown_before_park`;跨副本的悬挂腿与败方读数读这一列)。
347
+ * 所以这一行自己记着「为什么」,读面不靠任何进程内的东西;缺席 ⇒ 列保持 NULL(读面缺席即缺席)。
336
348
  * `refuse` 不读它(落的是一条决议,来源位 `policy`,不是结局成因);`expire` 撤兄弟时店自己写 `batch_sibling_withdrawn`(那是这条事务本身的事实)。
337
349
  */
338
350
  export interface AskTerminalClaimOpts {
@@ -348,6 +360,11 @@ export declare const POLICY_REFUSAL_DECISION: {
348
360
  readonly decision: "deny";
349
361
  readonly settledBy: Extract<HostSettledBy, "policy">;
350
362
  };
363
+ /**
364
+ * 带成因的终局 claim 把成因写进哪一列(穷举 switch:意图闭集加一个词 ⇒ 这里编译期先红)。一列一义:`cancel` 赢下的是终态(`ending_cause`),
365
+ * `expire` 赢下的是转 park(`park_trigger`);`refuse` 落的是一条决议,不带成因(不进本表)。SQL 两孪生读这一只;内存孪生的两臂各自落同一格(`transitionAsk` 的补丁 / `expireAsk`)。
366
+ */
367
+ export declare function claimCauseColumn(intent: Exclude<AskTerminalClaimIntent, "refuse">): "ending_cause" | "park_trigger";
351
368
  /** 终局 claim 意图 → 本次 CAS 的目标态(穷举 switch:意图闭集加一个词 ⇒ 这里编译期先红)。 */
352
369
  export declare function claimTargetState(intent: AskTerminalClaimIntent): AskState;
353
370
  /**
@@ -374,10 +391,11 @@ export type AskTerminalClaimCurrent = {
374
391
  /** [ref]:赢家决议的**结算来源**(语义与两态读法见 {@link AskRow.settledBy})。**必填**(不是可选)——
375
392
  * 可选会让投影漏挑这一列时静默编译过,而漏挑的后果恰是本批要修的病:CAS 输家把赢家的策略拒读成人拒。 */
376
393
  settledBy: HostSettledBy | null;
377
- /** 行上记着的成因,经问账行成因的唯一读口 `causeOfAskRow` 读出(`kind` 说它在这一行上的意思:VOID / DENIED = 终态成因,PARKING / PARKED = 转 park 的触发成因);
378
- * **只在读得出时在场**(没写 / 与 state 不配 ⇒ 缺席;DECIDED 的败方读数逐字不变)。CAS 输家据它按**行上那个成因**结算(而不是把别人撤的卡读成一次拒绝);
379
- * 投影的唯一函数是 {@link projectAskTerminalClaimTruth},三 twin 不各自挑列,原始列值不出店层。 */
380
- cause?: AskRowCause;
394
+ /** 行上记着的终态成因(`ending_cause` 列,经唯一读口 `askRowEndingCause` 读出)。**只在读得出时在场**(没写 / 与 state 不配 ⇒ 缺席;DECIDED 的败方读数逐字不变)。
395
+ * CAS 输家据它按**行上那个成因**结算(而不是把别人撤的卡读成一次拒绝);投影的唯一函数是 {@link projectAskTerminalClaimTruth},三 twin 不各自挑列,原始列值不出店层。 */
396
+ endingCause?: AskRowEndingCause;
397
+ /** 行上记着的转 park 触发成因(`park_trigger` 列,经唯一读口 `askRowParkTrigger` 读出;行走过 PARKING 才可能在场)。在场律同 {@link endingCause}。 */
398
+ parkTrigger?: ParkTriggerCause;
381
399
  };
382
400
  /**
383
401
  * 单次原子往返的答案(语义律的唯一规范陈述点 = core `checkpoint-store.ts` `claimTerminal` 契约 JSDoc,
@@ -578,6 +596,20 @@ export declare const APPROVAL_ASK_EXECUTION_LANE_COLUMN: {
578
596
  readonly mysql: "execution_lane VARCHAR(32) NOT NULL DEFAULT ''";
579
597
  readonly pg: "execution_lane VARCHAR(32) COLLATE \"C\" NOT NULL DEFAULT ''";
580
598
  };
599
+ /**
600
+ * 问行两列成因的列定义(两方言;唯一属主 —— 建表 DDL 内联它们,存量库的改名 / 加列只由一次性迁移做,那一步也读这里:窗口见 `plugins/one-time-migrations.ts` 登记表 M34)。
601
+ * 宽度 64 = 成因闭集里最长的词有余(钉在 `test/approval-reconciler.test.ts`)。语义见 {@link AskRow.endingCause} / {@link AskRow.parkTrigger}。
602
+ */
603
+ export declare const APPROVAL_ASK_ENDING_CAUSE_COLUMN: {
604
+ readonly name: "ending_cause";
605
+ readonly mysql: "ending_cause VARCHAR(64) NULL";
606
+ readonly pg: "ending_cause VARCHAR(64) COLLATE \"C\"";
607
+ };
608
+ export declare const APPROVAL_ASK_PARK_TRIGGER_COLUMN: {
609
+ readonly name: "park_trigger";
610
+ readonly mysql: "park_trigger VARCHAR(64) NULL";
611
+ readonly pg: "park_trigger VARCHAR(64) COLLATE \"C\"";
612
+ };
581
613
  export declare const APPROVAL_BATCH_TABLE = "approval_batch";
582
614
  /**
583
615
  * TiDB/MySQL-protocol 侧的两条建表语句,**抽成导出数组**(车3 刀 3a):`tidb-pool.ts` 的
@@ -1,6 +1,6 @@
1
1
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
2
2
  import { ASK_TERMINAL, canAskTransition } from "../approval-ask-machine.js";
3
- import { APPROVAL_ENDING_CAUSES, causeOfAskRow } from "../approval-deny-reasons.js";
3
+ import { APPROVAL_ENDING_CAUSES, askRowEndingCause, askRowParkTrigger } from "../approval-deny-reasons.js";
4
4
  import { isDupKeyError, isMissingColumnError } from "./sql-errors.js";
5
5
  import { describeProbedTableDrift } from "./schema-drift-sentence.js";
6
6
  import { decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn, readTriStateJsonValueColumn } from "./sql-json-column.js";
@@ -61,6 +61,18 @@ async function withReadSignal(op, signal, where) {
61
61
  }
62
62
  }
63
63
  export const POLICY_REFUSAL_DECISION = { decision: "deny", settledBy: "policy" };
64
+ export function claimCauseColumn(intent) {
65
+ switch (intent) {
66
+ case "cancel":
67
+ return "ending_cause";
68
+ case "expire":
69
+ return "park_trigger";
70
+ default: {
71
+ const unreachable = intent;
72
+ throw new Error(`approval-ask-store: no cause column for terminal claim intent ${String(unreachable)}`);
73
+ }
74
+ }
75
+ }
64
76
  export function claimTargetState(intent) {
65
77
  switch (intent) {
66
78
  case "expire":
@@ -78,14 +90,16 @@ export function claimTargetState(intent) {
78
90
  export function projectAskTerminalClaimTruth(row) {
79
91
  if (row === null || row.state === "STREAM_PENDING")
80
92
  return { state: "absent" };
81
- const cause = causeOfAskRow(row);
93
+ const endingCause = askRowEndingCause(row);
94
+ const parkTrigger = askRowParkTrigger(row);
82
95
  return {
83
96
  state: row.state,
84
97
  rev: row.rev,
85
98
  decision: row.decision,
86
99
  ...(row.updatedInput !== undefined ? { updatedInput: row.updatedInput } : {}),
87
100
  settledBy: row.settledBy,
88
- ...(cause !== undefined ? { cause } : {}),
101
+ ...(endingCause !== undefined ? { endingCause } : {}),
102
+ ...(parkTrigger !== undefined ? { parkTrigger } : {}),
89
103
  };
90
104
  }
91
105
  export function askRowOnCall(row, call) {
@@ -105,6 +119,16 @@ export const APPROVAL_ASK_EXECUTION_LANE_COLUMN = {
105
119
  mysql: "execution_lane VARCHAR(32) NOT NULL DEFAULT ''",
106
120
  pg: `execution_lane VARCHAR(32) COLLATE "C" NOT NULL DEFAULT ''`,
107
121
  };
122
+ export const APPROVAL_ASK_ENDING_CAUSE_COLUMN = {
123
+ name: "ending_cause",
124
+ mysql: "ending_cause VARCHAR(64) NULL",
125
+ pg: `ending_cause VARCHAR(64) COLLATE "C"`,
126
+ };
127
+ export const APPROVAL_ASK_PARK_TRIGGER_COLUMN = {
128
+ name: "park_trigger",
129
+ mysql: "park_trigger VARCHAR(64) NULL",
130
+ pg: `park_trigger VARCHAR(64) COLLATE "C"`,
131
+ };
108
132
  function executionLaneColumnOf(raw, askId) {
109
133
  const lane = typeof raw === "string" ? raw : "";
110
134
  if (!isExecutionLane(lane)) {
@@ -163,7 +187,10 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
163
187
  -- 编辑实参是整只工具入参,VARCHAR/TEXT 会造静默截断面。语义三态与读法见 AskRow.updatedInput 顶注。
164
188
  updated_input LONGTEXT NULL,
165
189
  decided_at_ms BIGINT NULL,
166
- denied_reason VARCHAR(64) NULL,
190
+ -- 成因两列,一列一义(语义见 AskRow.endingCause / AskRow.parkTrigger):ending_cause = 终态成因(到 VOID / DENIED 的转移写);
191
+ -- park_trigger = 转 park 的触发成因(只由 expire claim 写一次,终态不覆盖)。
192
+ ${APPROVAL_ASK_ENDING_CAUSE_COLUMN.mysql},
193
+ ${APPROVAL_ASK_PARK_TRIGGER_COLUMN.mysql},
167
194
  gate_token VARCHAR(120) NULL,
168
195
  gate_bound_call_id VARCHAR(255) NULL,
169
196
  gate_bound_input_hash VARCHAR(255) NULL,
@@ -244,7 +271,9 @@ export async function ensurePgApprovalAskSchema(q) {
244
271
  -- updated_input(S-26 / #304)—— MySQL 孪生逐字同形(语义/类型理由见那侧行内注;PG TEXT 无长度上限)。
245
272
  updated_input TEXT COLLATE "C",
246
273
  decided_at_ms BIGINT,
247
- denied_reason VARCHAR(64) COLLATE "C",
274
+ -- 成因两列 —— MySQL 孪生逐字同形(语义见那侧行内注)。
275
+ ${APPROVAL_ASK_ENDING_CAUSE_COLUMN.pg},
276
+ ${APPROVAL_ASK_PARK_TRIGGER_COLUMN.pg},
248
277
  gate_token VARCHAR(120) COLLATE "C",
249
278
  gate_bound_call_id VARCHAR(255) COLLATE "C",
250
279
  gate_bound_input_hash VARCHAR(255) COLLATE "C",
@@ -290,7 +319,7 @@ function askCols(dialect) {
290
319
  jsonColumnText(dialect, "decision_actor"),
291
320
  "decision_note, settled_by",
292
321
  jsonColumnText(dialect, "updated_input"),
293
- "decided_at_ms, denied_reason, gate_token",
322
+ "decided_at_ms, ending_cause, park_trigger, gate_token",
294
323
  "gate_bound_call_id, gate_bound_input_hash, idempotency_key",
295
324
  jsonColumnText(dialect, "card_json"),
296
325
  "rule_command, rule_scope_root, schema_version, expires_at_ms",
@@ -338,7 +367,8 @@ function mapAskRowBody(r, askId) {
338
367
  settledBy: strOrNull(r.settled_by),
339
368
  ...(r.updated_input == null ? {} : { updatedInput: decodeJsonColumn(r.updated_input, readTriStateJsonValueColumn, `${APPROVAL_ASK_TABLE}.updated_input for ask ${askId}`) }),
340
369
  decidedAtMs: numOrNull(r.decided_at_ms),
341
- deniedReason: strOrNull(r.denied_reason),
370
+ endingCause: strOrNull(r.ending_cause),
371
+ parkTrigger: strOrNull(r.park_trigger),
342
372
  gateToken: strOrNull(r.gate_token),
343
373
  gateBoundCallId: strOrNull(r.gate_bound_call_id),
344
374
  gateBoundInputHash: strOrNull(r.gate_bound_input_hash),
@@ -373,8 +403,8 @@ function patchColumns(patch, json) {
373
403
  out.push(["decision_note", patch.decisionNote]);
374
404
  if (patch.decidedAtMs !== undefined)
375
405
  out.push(["decided_at_ms", patch.decidedAtMs]);
376
- if (patch.deniedReason !== undefined)
377
- out.push(["denied_reason", patch.deniedReason]);
406
+ if (patch.endingCause !== undefined)
407
+ out.push(["ending_cause", patch.endingCause]);
378
408
  if (patch.gateToken !== undefined)
379
409
  out.push(["gate_token", patch.gateToken]);
380
410
  if (patch.gateBoundCallId !== undefined)
@@ -427,9 +457,9 @@ export class SqlApprovalAskStore {
427
457
  try {
428
458
  await conn.begin();
429
459
  await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES (?, ?, 'OPEN', 0, ?, ?)`, `INSERT INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES ($1, $2, 'OPEN', 0, $3, $4) ON CONFLICT (batch_id) DO NOTHING`), [row.batchId, row.taskId, now, now]);
430
- const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
431
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
432
- "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $12, $13, $14, $15, $16, $17, $18) ON CONFLICT (ask_id) DO NOTHING"), [
460
+ const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, ending_cause, park_trigger, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
461
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, execution_lane, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, settled_by, decided_at_ms, ending_cause, park_trigger, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, rule_command, rule_scope_root, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
462
+ "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $12, $13, $14, $15, $16, $17, $18) ON CONFLICT (ask_id) DO NOTHING"), [
433
463
  row.askId,
434
464
  row.taskId,
435
465
  row.sourceTaskId,
@@ -566,7 +596,7 @@ export class SqlApprovalAskStore {
566
596
  ? await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, decision = ${ph(dialect, 2)}, settled_by = ${ph(dialect, 3)}, decided_at_ms = ${ph(dialect, 4)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 5)} WHERE ${pendingClaimWhere(dialect, 6)}`, [to, POLICY_REFUSAL_DECISION.decision, POLICY_REFUSAL_DECISION.settledBy, now, now, askId, batchId])
567
597
  : (intent === "cancel" || intent === "expire") && opts?.cause !== undefined
568
598
  ?
569
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, denied_reason = ${ph(dialect, 2)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 3)} WHERE ${pendingClaimWhere(dialect, 4)}`, [to, opts.cause, now, askId, batchId])
599
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, ${claimCauseColumn(intent)} = ${ph(dialect, 2)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 3)} WHERE ${pendingClaimWhere(dialect, 4)}`, [to, opts.cause, now, askId, batchId])
570
600
  : await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 2)} WHERE ${pendingClaimWhere(dialect, 3)}`, [
571
601
  to,
572
602
  now,
@@ -585,7 +615,7 @@ export class SqlApprovalAskStore {
585
615
  await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=? WHERE batch_id=? AND state='OPEN'`, `UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='OPEN'`), [now, batchId]);
586
616
  let voidedSiblings;
587
617
  if (this.db.dialect === "pg") {
588
- const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=$4, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN]);
618
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=$4, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId, APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN]);
589
619
  voidedSiblings = res.rows.map((r) => String(r.ask_id));
590
620
  }
591
621
  else {
@@ -593,7 +623,7 @@ export class SqlApprovalAskStore {
593
623
  voidedSiblings = sel.rows.map((r) => String(r.ask_id));
594
624
  if (voidedSiblings.length > 0) {
595
625
  const inList = voidedSiblings.map(() => "?").join(", ");
596
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [
626
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [
597
627
  APPROVAL_ENDING_CAUSES.BATCH_SIBLING_WITHDRAWN,
598
628
  now,
599
629
  ...voidedSiblings,
@@ -639,7 +669,7 @@ export class SqlApprovalAskStore {
639
669
  }
640
670
  let voidedSiblings;
641
671
  if (this.db.dialect === "pg") {
642
- const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=$4, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='PARKING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId, APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE]);
672
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=$4, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='PARKING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId, APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE]);
643
673
  voidedSiblings = res.rows.map((r) => String(r.ask_id));
644
674
  }
645
675
  else {
@@ -647,7 +677,7 @@ export class SqlApprovalAskStore {
647
677
  voidedSiblings = sel.rows.map((r) => String(r.ask_id));
648
678
  if (voidedSiblings.length > 0) {
649
679
  const inList = voidedSiblings.map(() => "?").join(", ");
650
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='PARKING'`, [APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE, now, ...voidedSiblings]);
680
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='PARKING'`, [APPROVAL_ENDING_CAUSES.BATCH_BOUND_ELSEWHERE, now, ...voidedSiblings]);
651
681
  }
652
682
  }
653
683
  await conn.commit();
@@ -677,7 +707,7 @@ export class SqlApprovalAskStore {
677
707
  }
678
708
  let voided;
679
709
  if (this.db.dialect === "pg") {
680
- const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=$3, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' RETURNING ask_id`, [now, batchId, APPROVAL_ENDING_CAUSES.BATCH_ABORTED]);
710
+ const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=$3, rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' RETURNING ask_id`, [now, batchId, APPROVAL_ENDING_CAUSES.BATCH_ABORTED]);
681
711
  voided = res.rows.map((r) => String(r.ask_id));
682
712
  }
683
713
  else {
@@ -685,7 +715,7 @@ export class SqlApprovalAskStore {
685
715
  voided = sel.rows.map((r) => String(r.ask_id));
686
716
  if (voided.length > 0) {
687
717
  const inList = voided.map(() => "?").join(", ");
688
- await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', denied_reason=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [APPROVAL_ENDING_CAUSES.BATCH_ABORTED, now, ...voided]);
718
+ await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', ending_cause=?, rev=rev+1, updated_at_ms=? WHERE ask_id IN (${inList}) AND state='STREAM_PENDING'`, [APPROVAL_ENDING_CAUSES.BATCH_ABORTED, now, ...voided]);
689
719
  }
690
720
  }
691
721
  await conn.commit();
@@ -1,6 +1,6 @@
1
1
  import { type UsageRow } from "../usage-analytics.js";
2
2
  import type { TaskStatus } from "@sema-agent/core";
3
- import { StoreBootRefusalError, type FenceLanded, type FencedRun, type InProcessClaimFence, type CreateRunOutcome, type NewRunMeta, type RunIdemBinding, type RunListQuery, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
3
+ import { StoreBootRefusalError, type FenceLanded, type FencedRun, type InProcessClaimFence, type CreateRunOutcome, type NewRunMeta, type RunIdemBinding, type RunListQuery, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef, type SettledLegMemoryObservation } from "./store-contracts.js";
4
4
  import type { RunStoreCheckpointProbe } from "./memory-run-store.js";
5
5
  import type { LedgerEventType } from "../trace/ledger-events.js";
6
6
  import { type SpaceCommitClaim, type SpaceCommitReading, type SpaceCommitState } from "../workspace-space-commit.js";
@@ -169,6 +169,11 @@ export declare class FileRunStore implements InProcessClaimFence {
169
169
  * `createdAt DESC, taskId DESC`,与本店 {@link FileRunStore.listSessions} 挑 `rn=1` 那一行的比较器逐字同一份)。
170
170
  */
171
171
  latestRunForSession(sessionId: string): Promise<LatestRunRef | undefined>;
172
+ /**
173
+ * [ref] —— 这个会话**最近一条已结腿**与它的记忆观测(规则属主 = `store-contracts.ts` 的 {@link SettledLegMemoryObservation};
174
+ * {@link SqlRunStore.latestMemoryObservationForSession} 的孪生:同一只已结谓词(`isTerminalRunStatus`)、同一个排序口径 —— 与本店 {@link FileRunStore.latestRunForSession} 的比较器逐字同一份)。
175
+ */
176
+ latestMemoryObservationForSession(sessionId: string): Promise<SettledLegMemoryObservation | undefined>;
172
177
  /** DISTINCT sessions newest-first by last activity (the CC /resume picker): latest run's preview/status + first/last/count. */
173
178
  listSessions(opts: {
174
179
  owner?: string;
@@ -7,7 +7,7 @@ import { randomBytes } from "node:crypto";
7
7
  import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
8
8
  import { recordFailOpen } from "../observability/fail-open.js";
9
9
  import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
10
- import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError, readLegacyHolderDeathCode } from "./store-contracts.js";
10
+ import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError, readLegacyHolderDeathCode, isTerminalRunStatus, settledLegObservationOf } from "./store-contracts.js";
11
11
  import { notifyRunTerminal } from "../observability/run-terminal.js";
12
12
  import { readRunIdentityCells, runIdentityCells } from "../identity-source.js";
13
13
  import { createInProcessSpaceCommitTable, fenceSpaceCommitAtTerminal, SPACE_COMMIT_RECORD_SCHEMA } from "../workspace-space-commit.js";
@@ -595,6 +595,16 @@ export class FileRunStore {
595
595
  }
596
596
  return latest === undefined ? undefined : { taskId: latest.taskId, owner: latest.owner, createdAt: latest.createdAt.toISOString() };
597
597
  }
598
+ async latestMemoryObservationForSession(sessionId) {
599
+ let latest;
600
+ for (const r of this.runs.values()) {
601
+ if (r.sessionId !== sessionId || !isTerminalRunStatus(r.status))
602
+ continue;
603
+ if (latest === undefined || r.createdAt.getTime() > latest.createdAt.getTime() || (r.createdAt.getTime() === latest.createdAt.getTime() && r.taskId > latest.taskId))
604
+ latest = r;
605
+ }
606
+ return latest === undefined ? undefined : settledLegObservationOf(latest.taskId, latest.result);
607
+ }
598
608
  async listSessions(opts) {
599
609
  const bySession = new Map();
600
610
  for (const r of this.runs.values()) {
@@ -349,9 +349,9 @@ export declare class LocalSessionStore implements SessionStore {
349
349
  writeSessionRowCells(sessionId: string, cells: SessionRowPatch, how: ReplaySessionRowWrite): Promise<ReplaySessionRowWriteOutcome<SessionSummary>>;
350
350
  /**
351
351
  * 写原语锁住的那一行(调用方持这只会话的写锁并已 {@link loadRowSidecars}):会话元数据那一格(属主 `owner ?? null` —— 与头读、列表行同读;化身经
352
- * `incarnationFromCell` 的 `absent` 读法;执行位 `mirror`)+ 列表面上有它(会话仓里有;`forking` 中间态的存档不在 ⇒ 不在列表上)+ 两只边档的格。
353
- * 没有那一格或不在列表面上 ⇒ `undefined`(没有行)。委派分区行(placed)不上列表面(repo 的 `list()` 按存档行头排除,义务 2)⇒ 走到这里的行恒非 placed,
354
- * `placed: false` 是列表面那一判的投影,不是另一读。
352
+ * `incarnationFromCell` 的 `absent` 读法;执行位 `mirror`)+ 行集里有它({@link rowHeaderOf};`forking` 中间态的存档不在 ⇒ 不在列表上)+ 两只边档的格。
353
+ * 没有那一格或不在列表面上 ⇒ `undefined`(没有行)。委派分区行(placed)不上列表面(repo 的 `list()` 按存档行头排除,义务 2;行头读不出的行没有可读的 placement 记录、
354
+ * 按普通行读 —— 文件头注那一条)⇒ `placed: false` 是列表面那一判的投影,不是另一读。存档读不出的行照样是一行(行格住在边档,写得了;HTTP 改名口的坏档判据排在本原语之前)。
355
355
  */
356
356
  private lockedRowOf;
357
357
  /** 两只边档上这只会话的可写格(已发布的值;缺键 = 缺省)。调用方先 {@link loadRowSidecars}。 */
@@ -361,10 +361,35 @@ export declare class LocalSessionStore implements SessionStore {
361
361
  /** 应答行(锁内另读):列表行同一只合成 + 读到它时那一格上的住客。不在列表面上 / 没有那一格 ⇒ `undefined`。 */
362
362
  private listRowOf;
363
363
  /** ONE session's list row — {@link listSessions}' projection ({@link summaryOf}, the same synthesis) for one id;
364
- * `undefined` = not a row of the list face (not in the session repo). */
364
+ * `undefined` = not a row of the list face ({@link rowHeaders}). */
365
365
  sessionSummaryOf(sessionId: string): Promise<SessionSummary | undefined>;
366
- /** 会话仓里这只会话的列表元数据(列表面上的行 = `repo.list()` 里的一项)。 */
367
- private listedMeta;
366
+ /**
367
+ * 本店的**行集** —— 唯一的会话枚举(列表 {@link listSessions}、一次性迁移 M33 的步 1 读它;按 id 读的 {@link rowHeaderOf} 是同一只判据的一只 id 形):
368
+ * · 引擎仓的列表(core `repo.list()`:存档**行头**读得出、非 placed)—— 行头读得出的行;
369
+ * · ∪ 会话元数据边档里有格、不在引擎仓列表里的 id,经 {@link headerUnreadableRowOf}:存档头读了抛、且不是 placed 行 ⇒ 行头读不出的行(带那只故障);
370
+ * 不抛(存档不在:fork 中间态 / 删除中;或读间新建)/ placed 行(行头读得出,repo 的 `listPlaced` 认得它,尾部再坏也不上列表面)⇒ 不是行集里的行。
371
+ * 边档里没有格、存档头又读不出的会话(7.104.0 之前写下、此后从没被读过)没有一格可读的行事实 —— 属主读不出 ⇒ 不进行集(fail-closed,与归属门同读:
372
+ * {@link metaOf} 判不了它在不在 ⇒ 整只拒);运维面由 core 列表的披露座与起服 warn 点名它。
373
+ * 「行头读不读得出」(行集,M33 回填行事实问的就是它)与「叶子读不读得出」(列表行的 `unreadable`,{@link listedRowOf})是两件事、各一只读法。
374
+ */
375
+ private rowHeaders;
376
+ /**
377
+ * 行集里的这一行({@link rowHeaders} 的按 id 一形:同一只判据,只判这一只 id 的行 —— 别的会话读不出不改它的答)。`undefined` = 不是行集里的行。
378
+ * ⚠️ 如实:取它的行头仍经引擎仓列表(`repo.list()` 读一遍各会话的头),所以别的会话的头坏了,这一次读照旧为它报一次读坏披露;按 id 直读不在本形里。
379
+ */
380
+ private rowHeaderOf;
381
+ /** 不在引擎仓列表里、边档有格的一只 id:存档头读了抛、且不是 placed 行 ⇒ 行头读不出的那一行;否则 `undefined`(见 {@link rowHeaders})。 */
382
+ private headerUnreadableRowOf;
383
+ /**
384
+ * 委派分区行的 id 集的读者(repo 记 placement ⇒ 它的 `listPlaced`,行头那一读;不记 ⇒ 空集 —— 那种 repo 里没有 placed 行)。惰性、一次读:只在有一只 id 的存档头读抛、
385
+ * 要分「行头读不出」与「行头读得出的 placed 行尾部坏了」时才读。
386
+ */
387
+ private createPlacedIdsReader;
388
+ /**
389
+ * 列表面上的一行的内容那一半 = 头读的**叶子**那一读({@link archiveHeadOf},与 {@link headOf} 的叶子同一读):行头读不出 ⇒ 读不出(那只故障);行头读得出 ⇒ 再读叶子,
390
+ * 抛 ⇒ 读不出(尾部 I/O 故障之类:行头好、头读照样抛);否则读得出。所以行上的 `unreadable` 与头读、读口对同一只会话的答复同读。
391
+ */
392
+ private listedRowOf;
368
393
  /** [ref] —— 换源通告的基线读(local 孪生;三态读法与 SQL 两孪生的 `session_meta.instruction_source` 同一只编解码)。 */
369
394
  readInstructionSource(sessionId: string): Promise<InstructionSourceBaseline>;
370
395
  /**
@@ -481,6 +506,10 @@ export declare class LocalSessionStore implements SessionStore {
481
506
  * `lastTaskRunId` = that run's `result.runId`). One call per list (not per row); a session with no
482
507
  * run row ⇒ `0 / "" / null` (TiDB's null→"" mapping); a run-store read failure throws (never `0 / ""`).
483
508
  *
509
+ * - `unreadable` — present-iff the row's head leaf cannot be read ({@link listedRowOf}: the archive header, or the tail the head read
510
+ * scans): the row's facts above still come from its session-meta cell; `objectivePreview` is null; the marker's
511
+ * `errorCode` is what the session's own reads answer.
512
+ *
484
513
  * Keyset matches the contract: STRICTLY older than the cursor, ties broken by id DESC; `limit` is honored after
485
514
  * the in-memory sort. Cheap (a local single-user store holds few sessions). Errors reading one session's entries
486
515
  * degrade that row's preview to null rather than failing the whole list.
@@ -693,7 +722,7 @@ export declare class LocalSessionStore implements SessionStore {
693
722
  /**
694
723
  * 一次性迁移 M33 的 local 腿(`LocalBackend.runOneTimeMigrations` 调;起服、先于任何请求;窗内每次起服都跑,第二次起零命中):
695
724
  * 0. 恢复先行({@link settleArchiveSnapshots},幂等);
696
- * 1. **枚举**:id 全集 = 引擎仓的列表(读得出头的存档)∪ 边档全部键;只在边档里的、头读抛的 ⇒ 计 unreadable(步 3 整档读抛的同计)。两边都不在的读不出存档(列表跳过它、边档里没它的格)
725
+ * 1. **枚举**(行集 `rowHeaders`):id 全集 = 引擎仓的列表(读得出头的存档)∪ 边档全部键;只在边档里的、头读抛且不是 placed 行(行头读得出)的 ⇒ 计 unreadable(步 3 整档读抛的同计)。两边都不在的读不出存档(列表跳过它、边档里没它的格)
697
726
  * 对本店与引擎同样不可见,不计;
698
727
  * 2. **行回填**:列表里每一只存档 —— 格缺 ⇒ 建一格(化身新铸、属主「从没被记下」,与补铸同义);格缺建档时刻 ⇒ 写存档头的;格缺谱系而存档头有 ⇒ 写它。只写行(边档的一次提交),
699
728
  * 然后一道**落盘屏障**(边档文件与目录各 fsync)—— 在这之前不碰任何存档;
@@ -4,7 +4,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
7
- import { StampedSessionArchiveError } from "../session-error-wire.js";
7
+ import { StampedSessionArchiveError, sessionUnreadableOf } from "../session-error-wire.js";
8
8
  import { FileSessionRepo, SessionError, StoredSession, admitSessionPlacement, atomicWriteFile, sessionLogMarkOf, settledForkOptions, uuidv7, validateEntriesForImport } from "@sema-agent/core";
9
9
  import { createPlacedSessionRow, sourceFacesVia } from "../delegated-session-owner.js";
10
10
  import { placementCapabilityOf } from "./session-placement.js";
@@ -677,7 +677,7 @@ export class LocalSessionStore {
677
677
  }
678
678
  async lockedRowOf(sessionId) {
679
679
  const meta = this.metaCellOf(sessionId);
680
- if (meta === undefined || (await this.listedMeta(sessionId)) === undefined)
680
+ if (meta === undefined || (await this.rowHeaderOf(sessionId)) === undefined)
681
681
  return undefined;
682
682
  return { owner: meta.owner ?? null, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent"), placed: false, mirror: meta.execution?.role === "mirror", cells: this.rowCellsOf(sessionId) };
683
683
  }
@@ -713,19 +713,62 @@ export class LocalSessionStore {
713
713
  }
714
714
  async listRowOf(sessionId) {
715
715
  await this.loadRowSidecars();
716
- const listed = await this.listedMeta(sessionId);
716
+ const header = await this.rowHeaderOf(sessionId);
717
717
  const meta = this.metaCellOf(sessionId);
718
- if (listed === undefined || meta === undefined)
718
+ if (header === undefined || meta === undefined)
719
719
  return undefined;
720
- return { row: await this.summaryOf(listed, await this.runRollups()), occupant: { owner: meta.owner ?? null, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent") } };
720
+ return { row: await this.summaryOf(await this.listedRowOf(header), await this.runRollups()), occupant: { owner: meta.owner ?? null, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent") } };
721
721
  }
722
722
  async sessionSummaryOf(sessionId) {
723
723
  await this.loadRowSidecars();
724
- const meta = await this.listedMeta(sessionId);
725
- return meta === undefined ? undefined : this.summaryOf(meta, await this.runRollups());
724
+ const header = await this.rowHeaderOf(sessionId);
725
+ return header === undefined ? undefined : this.summaryOf(await this.listedRowOf(header), await this.runRollups());
726
726
  }
727
- async listedMeta(sessionId) {
728
- return (await this.repo.list()).find((m) => m.id === sessionId);
727
+ async rowHeaders() {
728
+ await this.meta.ensureLoaded();
729
+ const listed = await this.repo.list();
730
+ const listedIds = new Set(listed.map((row) => row.id));
731
+ const placed = this.createPlacedIdsReader();
732
+ const rows = listed.map((header) => ({ sessionId: header.id, header }));
733
+ for (const id of [...this.meta.state.entries.keys()].sort()) {
734
+ if (listedIds.has(id))
735
+ continue;
736
+ const row = await this.headerUnreadableRowOf(id, placed);
737
+ if (row !== undefined)
738
+ rows.push(row);
739
+ }
740
+ return rows;
741
+ }
742
+ async rowHeaderOf(sessionId) {
743
+ await this.meta.ensureLoaded();
744
+ const header = (await this.repo.list()).find((m) => m.id === sessionId);
745
+ if (header !== undefined)
746
+ return { sessionId, header };
747
+ return this.meta.state.entries.has(sessionId) ? this.headerUnreadableRowOf(sessionId, this.createPlacedIdsReader()) : undefined;
748
+ }
749
+ async headerUnreadableRowOf(sessionId, placed) {
750
+ try {
751
+ await this.archiveHeadOf(sessionId);
752
+ return undefined;
753
+ }
754
+ catch (fault) {
755
+ return (await placed()).has(sessionId) ? undefined : { sessionId, headerFault: fault };
756
+ }
757
+ }
758
+ createPlacedIdsReader() {
759
+ let ids;
760
+ return () => (ids ??= (async () => new Set((await this.listPlaced?.("subagent"))?.map((row) => row.sessionId) ?? []))());
761
+ }
762
+ async listedRowOf(row) {
763
+ if ("headerFault" in row)
764
+ return { sessionId: row.sessionId, unreadable: row.headerFault };
765
+ try {
766
+ await this.archiveHeadOf(row.sessionId);
767
+ return { sessionId: row.sessionId, readable: row.header };
768
+ }
769
+ catch (fault) {
770
+ return { sessionId: row.sessionId, unreadable: fault };
771
+ }
729
772
  }
730
773
  async readInstructionSource(sessionId) {
731
774
  await this.instructionSources.ensureLoaded();
@@ -953,14 +996,16 @@ export class LocalSessionStore {
953
996
  }
954
997
  async listSessions(opts) {
955
998
  await this.loadRowSidecars();
956
- const metas = await this.repo.list();
999
+ const rows = [];
1000
+ for (const header of await this.rowHeaders())
1001
+ rows.push(await this.listedRowOf(header));
957
1002
  const rollups = await this.runRollups();
958
1003
  const all = [];
959
- for (const meta of metas) {
960
- const owner = this.ownerCellOf(meta.id);
1004
+ for (const row of rows) {
1005
+ const owner = this.ownerCellOf(row.sessionId);
961
1006
  if (opts.owner !== undefined && owner !== opts.owner && !(opts.includeUnowned === true && owner === null))
962
1007
  continue;
963
- all.push(await this.summaryOf(meta, rollups));
1008
+ all.push(await this.summaryOf(row, rollups));
964
1009
  }
965
1010
  const qlc = opts.q?.toLowerCase();
966
1011
  const filtered = qlc ? all.filter((r) => r.objectivePreview?.toLowerCase().includes(qlc)) : all;
@@ -983,23 +1028,25 @@ export class LocalSessionStore {
983
1028
  const cell = this.meta.state.entries.get(sessionId);
984
1029
  return cell === undefined ? null : (readSessionMetaCell(cell).owner ?? null);
985
1030
  }
986
- async summaryOf(meta, rollups) {
987
- const firstActivityAt = rowCreatedAtOf(meta.id, this.metaCellOf(meta.id));
988
- const owner = this.ownerCellOf(meta.id);
989
- const cells = this.rowCellsOf(meta.id);
990
- const rowTag = this.rowTagKeyOf(meta.id, owner, cells);
991
- const lastActivityAt = this.lastActivity.get(meta.id) ?? firstActivityAt;
992
- const objectivePreview = await this.previewOf(meta);
1031
+ async summaryOf(listed, rollups) {
1032
+ const { sessionId } = listed;
1033
+ const firstActivityAt = rowCreatedAtOf(sessionId, this.metaCellOf(sessionId));
1034
+ const owner = this.ownerCellOf(sessionId);
1035
+ const cells = this.rowCellsOf(sessionId);
1036
+ const rowTag = this.rowTagKeyOf(sessionId, owner, cells);
1037
+ const lastActivityAt = this.lastActivity.get(sessionId) ?? firstActivityAt;
1038
+ const objectivePreview = "readable" in listed ? await this.previewOf(listed.readable) : null;
993
1039
  return {
994
- sessionId: meta.id,
1040
+ sessionId,
995
1041
  owner,
996
1042
  lastActivityAt,
997
1043
  firstActivityAt,
998
1044
  objectivePreview,
999
- ...this.runProjectionOf(meta.id, rollups),
1045
+ ...this.runProjectionOf(sessionId, rollups),
1000
1046
  title: cells.title,
1001
1047
  pinned: cells.pinned,
1002
1048
  ...rowTag,
1049
+ ...("unreadable" in listed ? { unreadable: sessionUnreadableOf(listed.unreadable) } : {}),
1003
1050
  };
1004
1051
  }
1005
1052
  rowTagKeyOf(sessionId, owner, cells) {
@@ -1381,19 +1428,16 @@ export class LocalSessionStore {
1381
1428
  if (!this.repo.readRow || !this.repo.importEntries)
1382
1429
  throw new SessionError("storage", "LocalSessionStore: the one-time migration M33 needs a session repo that reads a whole row and imports a log (readRow / importEntries)");
1383
1430
  await this.meta.ensureLoaded();
1384
- const listed = await this.repo.list();
1385
- const listedIds = new Set(listed.map((row) => row.id));
1431
+ const rowHeaders = await this.rowHeaders();
1432
+ const listed = [];
1386
1433
  const unreadable = [];
1387
- for (const id of [...this.meta.state.entries.keys()].sort()) {
1388
- if (listedIds.has(id))
1434
+ for (const row of rowHeaders) {
1435
+ if ("header" in row) {
1436
+ listed.push(row.header);
1389
1437
  continue;
1390
- try {
1391
- await this.archiveHeadOf(id);
1392
- }
1393
- catch (e) {
1394
- const errno = errnoOf(e);
1395
- unreadable.push({ sessionId: id, ...(errno !== undefined ? { errno } : {}) });
1396
1438
  }
1439
+ const errno = errnoOf(row.headerFault);
1440
+ unreadable.push({ sessionId: row.sessionId, ...(errno !== undefined ? { errno } : {}) });
1397
1441
  }
1398
1442
  let rowFactsBackfilled = 0;
1399
1443
  const committed = await this.meta.commit((draft) => {
@@ -1,18 +1,28 @@
1
- import { type TaskAttachmentStore, type TaskAttachmentRecord } from "./task-attachment-store.js";
1
+ import { type AttachmentStoreBindOutcome, type TaskAttachmentStore, type TaskAttachmentRecord } from "./task-attachment-store.js";
2
2
  export declare class LocalTaskAttachmentStore implements TaskAttachmentStore {
3
3
  private readonly dir;
4
4
  private metas;
5
5
  private hydration;
6
+ /** 变更串行段的尾(见 `serialized`):每只变更口排在上一只的落盘与合回之后。本店唯一一只。 */
7
+ private mutationTail;
6
8
  constructor(rootDir: string);
7
9
  private hydrate;
10
+ /**
11
+ * 本店**唯一**的变更串行段:凡改 `this.metas` 或删盘上行的口(put / bind / delete / deleteBySession / reapUnbound)把**整条操作**
12
+ * (判 + 落盘 + 合回内存)交进来,于是任何一只变更口都观察不到别的变更口做到一半的中间态 —— bind 落盘期间「副本已判、内存未合」
13
+ * 的那一段,收割 / 删除既不会判到旧内存、也不会把盘上正在落的行删掉。读口不进来:它们只读内存,不排队。
14
+ * 一只变更失败不堵后面的(尾巴吞掉失败,错原样上抛给它自己的调用方)。操作体里不得再调 `serialized`(会排在自己后面)。
15
+ */
16
+ private serialized;
8
17
  put(rec: TaskAttachmentRecord & {
9
18
  content: Uint8Array;
10
19
  }): Promise<void>;
11
20
  get(owner: string, id: string): Promise<TaskAttachmentRecord | null>;
12
21
  getContent(owner: string, id: string): Promise<Uint8Array | null>;
13
22
  private persistMeta;
14
- bind(owner: string, ids: string[], sessionId: string, boundAtMs: number): Promise<number>;
23
+ bind(owner: string, ids: readonly string[], sessionId: string, boundAtMs: number): Promise<AttachmentStoreBindOutcome>;
15
24
  listBySession(sessionId: string): Promise<TaskAttachmentRecord[]>;
25
+ /** 删一行(meta + bytes + 内存索引)。只在 `serialized` 的操作体里调用。 */
16
26
  private removeRow;
17
27
  delete(owner: string, id: string): Promise<boolean>;
18
28
  deleteBySession(sessionId: string): Promise<number>;