@sema-agent/server 7.4.0 → 7.6.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 (201) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +18 -3
  3. package/README.zh-CN.md +14 -3
  4. package/USAGE.md +80 -1
  5. package/dist/approval-card.d.ts +15 -3
  6. package/dist/approval-card.js +41 -7
  7. package/dist/approval-reconciler.d.ts +109 -12
  8. package/dist/approval-reconciler.js +152 -24
  9. package/dist/boot/config-center.js +15 -2
  10. package/dist/boot/coordinators.js +10 -2
  11. package/dist/boot/execution-env.js +1 -1
  12. package/dist/boot/org-memory.d.ts +6 -0
  13. package/dist/boot/org-memory.js +1 -1
  14. package/dist/boot/parked-revive-gate.d.ts +78 -0
  15. package/dist/boot/parked-revive-gate.js +114 -0
  16. package/dist/boot/reapers.d.ts +2 -0
  17. package/dist/boot/reapers.js +11 -4
  18. package/dist/boot/resolve-spec.d.ts +3 -19
  19. package/dist/boot/resolve-spec.js +73 -67
  20. package/dist/boot/runner-deps.d.ts +23 -1
  21. package/dist/boot/runner-deps.js +8 -11
  22. package/dist/boot/workflow-orchestration.d.ts +8 -3
  23. package/dist/boot/workflow-orchestration.js +23 -1
  24. package/dist/budget.d.ts +1 -1
  25. package/dist/budget.js +1 -1
  26. package/dist/capabilities/repo-tools.d.ts +1 -1
  27. package/dist/capabilities/repo-tools.js +8 -2
  28. package/dist/config-center/apply-effective.js +33 -10
  29. package/dist/config-provider.d.ts +1 -0
  30. package/dist/config-provider.js +23 -3
  31. package/dist/config-types.d.ts +27 -9
  32. package/dist/config.d.ts +6 -1
  33. package/dist/config.js +61 -15
  34. package/dist/deployment-governance.d.ts +168 -0
  35. package/dist/deployment-governance.js +206 -0
  36. package/dist/env-facts.d.ts +3 -1
  37. package/dist/env-facts.js +3 -1
  38. package/dist/fleet/fleet-bus.d.ts +17 -2
  39. package/dist/fleet/fleet-bus.js +68 -3
  40. package/dist/fleet/fleet-terminal-window.d.ts +98 -0
  41. package/dist/fleet/fleet-terminal-window.js +316 -0
  42. package/dist/governance-ask-marks.d.ts +31 -0
  43. package/dist/governance-ask-marks.js +122 -0
  44. package/dist/hooks/hook-runner.d.ts +28 -0
  45. package/dist/hooks/hook-runner.js +149 -25
  46. package/dist/http/routes/approvals-assistant.js +6 -7
  47. package/dist/http/routes/diagnostics.js +10 -5
  48. package/dist/http/routes/fleet.js +160 -14
  49. package/dist/http/routes/memory-policy.d.ts +2 -1
  50. package/dist/http/routes/memory-policy.js +77 -13
  51. package/dist/http/routes/runs.js +6 -2
  52. package/dist/http/routes/tasks.js +59 -22
  53. package/dist/http/routes/trace-usage.js +3 -4
  54. package/dist/http/send.d.ts +23 -0
  55. package/dist/http/send.js +23 -0
  56. package/dist/http/server.d.ts +9 -0
  57. package/dist/http/server.js +28 -14
  58. package/dist/http/sse-log.js +3 -4
  59. package/dist/http/wire-types.d.ts +7 -2
  60. package/dist/leader/diffout.d.ts +10 -0
  61. package/dist/leader/diffout.js +14 -2
  62. package/dist/leader/diffup.js +3 -2
  63. package/dist/leader/planner.js +7 -0
  64. package/dist/main.js +39 -31
  65. package/dist/observability/fail-open.d.ts +17 -2
  66. package/dist/observability/fail-open.js +19 -4
  67. package/dist/observability/prompt-manifest.d.ts +5 -1
  68. package/dist/orchestration/workflow-notify-journal.d.ts +58 -2
  69. package/dist/orchestration/workflow-notify-journal.js +130 -45
  70. package/dist/parked-decide.js +9 -4
  71. package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
  72. package/dist/plugins/approval-ask-store-memory.js +3 -2
  73. package/dist/plugins/approval-ask-store-sql.d.ts +60 -5
  74. package/dist/plugins/approval-ask-store-sql.js +75 -35
  75. package/dist/plugins/background-agent-store-sql.js +16 -16
  76. package/dist/plugins/background-shell-support.d.ts +1 -1
  77. package/dist/plugins/background-shell-support.js +2 -2
  78. package/dist/plugins/breaker-state-sql.js +2 -2
  79. package/dist/plugins/checkpoint-store-sql.d.ts +67 -8
  80. package/dist/plugins/checkpoint-store-sql.js +76 -13
  81. package/dist/plugins/image-bake-store-sql.d.ts +1 -1
  82. package/dist/plugins/image-bake-store-sql.js +27 -27
  83. package/dist/plugins/image-index-sql.js +15 -15
  84. package/dist/plugins/local-checkpoint-store.d.ts +20 -1
  85. package/dist/plugins/local-checkpoint-store.js +19 -0
  86. package/dist/plugins/mailbox-store-sql.d.ts +4 -10
  87. package/dist/plugins/mailbox-store-sql.js +59 -6
  88. package/dist/plugins/memory-engine-pg.js +9 -9
  89. package/dist/plugins/memory-engine-tidb.js +7 -7
  90. package/dist/plugins/memory-sync-store-pg.js +13 -13
  91. package/dist/plugins/memory-sync-store-tidb.js +5 -5
  92. package/dist/plugins/outcome-ledger-sql.js +7 -7
  93. package/dist/plugins/pg-cost-quota.js +3 -3
  94. package/dist/plugins/pg-pool.js +84 -75
  95. package/dist/plugins/pg-rate-limiter.js +3 -3
  96. package/dist/plugins/pg-session-storage.d.ts +1 -1
  97. package/dist/plugins/pg-session-storage.js +12 -13
  98. package/dist/plugins/remote-env-host.js +3 -1
  99. package/dist/plugins/remote-env-local-docker.js +6 -3
  100. package/dist/plugins/remote-env-ssh.d.ts +13 -1
  101. package/dist/plugins/roster-store-sql.js +8 -8
  102. package/dist/plugins/store-contracts.d.ts +19 -0
  103. package/dist/plugins/store-contracts.js +42 -0
  104. package/dist/plugins/task-attachment-store.js +5 -5
  105. package/dist/plugins/task-list-store-sql.js +1 -1
  106. package/dist/plugins/tidb-cost-quota.js +1 -1
  107. package/dist/plugins/tidb-pool.js +83 -60
  108. package/dist/plugins/tidb-rate-limiter.js +1 -1
  109. package/dist/plugins/tidb-session-store.js +2 -5
  110. package/dist/plugins/tool-result-store-sql.js +2 -2
  111. package/dist/plugins/usage-window-store-sql.js +13 -13
  112. package/dist/plugins/write-behind-counter.d.ts +10 -2
  113. package/dist/plugins/write-behind-counter.js +13 -3
  114. package/dist/resource-suspend.d.ts +3 -1
  115. package/dist/resource-suspend.js +3 -1
  116. package/dist/run-local.d.ts +73 -1
  117. package/dist/run-local.js +146 -5
  118. package/dist/runs.d.ts +11 -1
  119. package/dist/runs.js +18 -3
  120. package/dist/runtime-governance.d.ts +18 -0
  121. package/dist/runtime-governance.js +90 -3
  122. package/dist/security.d.ts +12 -0
  123. package/dist/security.js +12 -0
  124. package/dist/session-sync-kernel.d.ts +13 -0
  125. package/dist/session-sync-kernel.js +13 -0
  126. package/dist/task-settings.d.ts +3 -9
  127. package/dist/task-settings.js +16 -13
  128. package/dist/tool-approval.d.ts +33 -6
  129. package/dist/tool-approval.js +80 -23
  130. package/dist/trace/core-keyset-guard.d.ts +18 -4
  131. package/dist/trace/project.d.ts +10 -1
  132. package/dist/trace/project.js +31 -0
  133. package/package.json +3 -3
  134. package/dist/boot/lexical-path-env.d.ts +0 -10
  135. package/dist/boot/lexical-path-env.js +0 -88
  136. package/dist/capabilities/oa-tools.d.ts +0 -15
  137. package/dist/capabilities/oa-tools.js +0 -54
  138. package/dist/finance/cost-taxonomy.d.ts +0 -34
  139. package/dist/finance/cost-taxonomy.js +0 -26
  140. package/dist/plugins/approval-store-sql.d.ts +0 -116
  141. package/dist/plugins/approval-store-sql.js +0 -151
  142. package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
  143. package/dist/plugins/file-workflow-journal-store.js +0 -12
  144. package/dist/plugins/pg-approval-store.d.ts +0 -9
  145. package/dist/plugins/pg-approval-store.js +0 -9
  146. package/dist/plugins/pg-breaker-state.d.ts +0 -8
  147. package/dist/plugins/pg-breaker-state.js +0 -8
  148. package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
  149. package/dist/plugins/pg-checkpoint-store.js +0 -10
  150. package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
  151. package/dist/plugins/pg-file-snapshot-store.js +0 -8
  152. package/dist/plugins/pg-image-bake.d.ts +0 -12
  153. package/dist/plugins/pg-image-bake.js +0 -11
  154. package/dist/plugins/pg-image-index.d.ts +0 -12
  155. package/dist/plugins/pg-image-index.js +0 -11
  156. package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
  157. package/dist/plugins/pg-outcome-ledger.js +0 -11
  158. package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
  159. package/dist/plugins/pg-resume-anchor-store.js +0 -7
  160. package/dist/plugins/pg-run-store.d.ts +0 -9
  161. package/dist/plugins/pg-run-store.js +0 -9
  162. package/dist/plugins/pg-session-policy-store.d.ts +0 -7
  163. package/dist/plugins/pg-session-policy-store.js +0 -7
  164. package/dist/plugins/pg-session-store.d.ts +0 -12
  165. package/dist/plugins/pg-session-store.js +0 -12
  166. package/dist/plugins/pg-tool-result-store.d.ts +0 -9
  167. package/dist/plugins/pg-tool-result-store.js +0 -9
  168. package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
  169. package/dist/plugins/pg-workflow-journal-store.js +0 -9
  170. package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
  171. package/dist/plugins/pg-workflow-run-store.js +0 -9
  172. package/dist/plugins/tidb-approval-store.d.ts +0 -8
  173. package/dist/plugins/tidb-approval-store.js +0 -8
  174. package/dist/plugins/tidb-breaker-state.d.ts +0 -7
  175. package/dist/plugins/tidb-breaker-state.js +0 -7
  176. package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
  177. package/dist/plugins/tidb-checkpoint-store.js +0 -9
  178. package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
  179. package/dist/plugins/tidb-file-snapshot-store.js +0 -8
  180. package/dist/plugins/tidb-image-bake.d.ts +0 -12
  181. package/dist/plugins/tidb-image-bake.js +0 -11
  182. package/dist/plugins/tidb-image-index.d.ts +0 -12
  183. package/dist/plugins/tidb-image-index.js +0 -11
  184. package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
  185. package/dist/plugins/tidb-outcome-ledger.js +0 -12
  186. package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
  187. package/dist/plugins/tidb-resume-anchor-store.js +0 -7
  188. package/dist/plugins/tidb-run-store.d.ts +0 -10
  189. package/dist/plugins/tidb-run-store.js +0 -9
  190. package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
  191. package/dist/plugins/tidb-session-policy-store.js +0 -7
  192. package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
  193. package/dist/plugins/tidb-tool-result-store.js +0 -10
  194. package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
  195. package/dist/plugins/tidb-workflow-journal-store.js +0 -9
  196. package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
  197. package/dist/plugins/tidb-workflow-run-store.js +0 -10
  198. package/dist/plugins/workflow-journal-limits.d.ts +0 -12
  199. package/dist/plugins/workflow-journal-limits.js +0 -12
  200. package/dist/sema-registry.d.ts +0 -41
  201. package/dist/sema-registry.js +0 -40
@@ -71,13 +71,46 @@ export const APPROVAL_BATCHES_TABLE = "approval_batches";
71
71
  *
72
72
  * SCHEMA POLICY 同 `tidb-pool.ts` 头注:纯 CREATE,禁 ALTER 增量 seam,改列直接改这里 + 重建库
73
73
  * (`docs/schema/baseline-mysql.sql` 是由这些文本机器录制出的**产物**,永不手改)。
74
+ *
75
+ * ── 列宽的**测量依据**(#192 A10;U2 逐列取证在档)──────────────────────────────────────────────────
76
+ * 本表家族原先一律 VARCHAR(255),是"先建表后想"的产物。按"每列的值到底由谁铸、有没有入口上限"重新裁:
77
+ *
78
+ * 收窄(有硬上限背书,证据即上限本身):
79
+ * · `ask_id` / `batch_id` / `bound_ask_id` → 64:`deriveAskId`/`deriveBatchId` 恒是 sha256 hex 取前
80
+ * 64 字符(`approval-ask-machine.ts` deterministicId),按构造不可能更长。
81
+ * · `task_id` → 64:入口 `UUIDV7_RE`(`security.ts:134`)只放行 36 字符的规范 uuidv7;与兄弟列
82
+ * `task_run.task_id VARCHAR(64)` 同宽(此前 255 是本家族独有的偏离,无依据)。
83
+ * · `session_id` → 64:提交入口硬拒 >64(`http/server.ts` "sessionId must be at most 64 characters"),
84
+ * 与 `session_meta`/`task_run` 同宽。
85
+ * · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts:350`)硬拒更长者;
86
+ * 全仓每一根 owner/scope 轴都是 190,此前 255 同样是无依据偏离。
87
+ * · `gate_token` → 120:它抄的是 checkpoint 的 token(core `mintCheckpointToken` = 16 字节 hex = 32 字符),
88
+ * 取与**被抄那一列** `checkpoint.token VARCHAR(120)` 同宽 —— 同一个值在两张表上宽度必须一致,
89
+ * 否则抄的那一步就是一道静默截断。512 是原先的 16 倍冗余。
90
+ *
91
+ * 🔑 上面四条收窄之所以**不引入任何新暴露面**,靠的不是入口断言而是一条更硬的事实:同一个 session_id /
92
+ * task_id / owner **早就**同时躺在 `session_meta.session_id VARCHAR(64)` / `task_run.task_id VARCHAR(64)` /
93
+ * `task_run.owner VARCHAR(190)` 里。任何长到能撑爆本表新宽度的值,在写到那几张**更中心**的表时就已经
94
+ * 先炸了 —— 本表此前的 255 从来不是一道额外的安全余量,只是一处与全仓不一致的偏离。
95
+ * ⇒ 收窄的风险上界 = 0;真正要担心的是反过来:留着 255 会让人误以为这里可以存更长的 id。
96
+ *
97
+ * 🔴 **刻意不收窄**(测量结果不支持,记在这里免得下一个人以为是漏了):
98
+ * · `source_task_id` / `tool_call_id` / `parent_tool_call_id` / `gate_bound_call_id` —— 这四列的值是
99
+ * **模型/引擎铸的原始 id**,服务端在写行这一步**没有任何长度断言**(`approval-card.ts` 的
100
+ * `MAX_IDENT` 只裁了给人看的那份 card 投影,不是本列)。没有入口上限就收窄 = 把"存不下"这件事
101
+ * 推迟到 INSERT 才炸(MySQL 更糟:静默截断),换来的只是几十字节。要收窄,先补入口断言
102
+ * (照 `assertPrincipalShape`/`assertIdempotencyKeyShape` 的姿势),那是另一件事。
103
+ * · `bound_input_hash` / `gate_bound_input_hash` —— 按约定是 sha256 hex(64),但 `readBoundInputHash`
104
+ * 只校验"非空字符串",没校长度/字形 ⇒ 同上,先有断言再谈收窄。
105
+ * · `idempotency_key` —— 255 **正是**入口断言的上限本身(zod `.max(255)` + 店内 `assertIdempotencyKeyShape`
106
+ * 双执法),宽度与上限同源,恰好正确,动它反而制造截断面。
74
107
  */
75
108
  export const TIDB_APPROVAL_ASK_STATEMENTS = [
76
109
  `CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCHES_TABLE} (
77
- batch_id VARCHAR(255) NOT NULL,
78
- task_id VARCHAR(255) NOT NULL,
110
+ batch_id VARCHAR(64) NOT NULL,
111
+ task_id VARCHAR(64) NOT NULL,
79
112
  state VARCHAR(32) NOT NULL,
80
- bound_ask_id VARCHAR(255) NULL,
113
+ bound_ask_id VARCHAR(64) NULL,
81
114
  version INT NOT NULL DEFAULT 0,
82
115
  created_at_ms BIGINT NOT NULL,
83
116
  updated_at_ms BIGINT NOT NULL,
@@ -85,12 +118,12 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
85
118
  KEY idx_batch_task (task_id)
86
119
  ) COLLATE utf8mb4_bin`,
87
120
  `CREATE TABLE IF NOT EXISTS ${APPROVAL_ASKS_TABLE} (
88
- ask_id VARCHAR(255) NOT NULL,
89
- task_id VARCHAR(255) NOT NULL,
121
+ ask_id VARCHAR(64) NOT NULL,
122
+ task_id VARCHAR(64) NOT NULL,
90
123
  source_task_id VARCHAR(255) NOT NULL,
91
- session_id VARCHAR(255) NOT NULL,
92
- owner VARCHAR(255) NULL,
93
- batch_id VARCHAR(255) NOT NULL,
124
+ session_id VARCHAR(64) NOT NULL,
125
+ owner VARCHAR(190) NULL,
126
+ batch_id VARCHAR(64) NOT NULL,
94
127
  tool_call_id VARCHAR(255) NOT NULL,
95
128
  -- leg_key: 车3 §3.2′ 身份轴——sha256(resume checkpoint token) 的 hex(64 字符),首腿存空串。
96
129
  -- 取代原来的数值 leg 列:一腿一凭据,零 DDL 依赖 task_run、零 markResuming 改动。NOT NULL(空串
@@ -110,7 +143,7 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
110
143
  decision_note TEXT NULL,
111
144
  decided_at_ms BIGINT NULL,
112
145
  denied_reason VARCHAR(64) NULL,
113
- gate_token VARCHAR(512) NULL,
146
+ gate_token VARCHAR(120) NULL,
114
147
  gate_bound_call_id VARCHAR(255) NULL,
115
148
  gate_bound_input_hash VARCHAR(255) NULL,
116
149
  idempotency_key VARCHAR(255) NULL,
@@ -141,10 +174,10 @@ export async function ensureTiDBApprovalAskSchema(pool) {
141
174
  }
142
175
  export async function ensurePgApprovalAskSchema(q) {
143
176
  await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCHES_TABLE} (
144
- batch_id VARCHAR(255) NOT NULL,
145
- task_id VARCHAR(255) NOT NULL,
146
- state VARCHAR(32) NOT NULL,
147
- bound_ask_id VARCHAR(255),
177
+ batch_id VARCHAR(64) COLLATE "C" NOT NULL,
178
+ task_id VARCHAR(64) COLLATE "C" NOT NULL,
179
+ state VARCHAR(32) COLLATE "C" NOT NULL,
180
+ bound_ask_id VARCHAR(64) COLLATE "C",
148
181
  version INT NOT NULL DEFAULT 0,
149
182
  created_at_ms BIGINT NOT NULL,
150
183
  updated_at_ms BIGINT NOT NULL,
@@ -152,33 +185,33 @@ export async function ensurePgApprovalAskSchema(q) {
152
185
  )`);
153
186
  await q(`CREATE INDEX IF NOT EXISTS idx_batch_task ON ${APPROVAL_BATCHES_TABLE} (task_id)`);
154
187
  await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_ASKS_TABLE} (
155
- ask_id VARCHAR(255) NOT NULL,
156
- task_id VARCHAR(255) NOT NULL,
157
- source_task_id VARCHAR(255) NOT NULL,
158
- session_id VARCHAR(255) NOT NULL,
159
- owner VARCHAR(255),
160
- batch_id VARCHAR(255) NOT NULL,
161
- tool_call_id VARCHAR(255) NOT NULL,
188
+ ask_id VARCHAR(64) COLLATE "C" NOT NULL,
189
+ task_id VARCHAR(64) COLLATE "C" NOT NULL,
190
+ source_task_id VARCHAR(255) COLLATE "C" NOT NULL,
191
+ session_id VARCHAR(64) COLLATE "C" NOT NULL,
192
+ owner VARCHAR(190) COLLATE "C",
193
+ batch_id VARCHAR(64) COLLATE "C" NOT NULL,
194
+ tool_call_id VARCHAR(255) COLLATE "C" NOT NULL,
162
195
  -- leg_key: 车3 §3.2′ 身份轴——sha256(resume checkpoint token) 的 hex(64 字符),首腿存空串
163
196
  -- (空串是首腿的真值,不是缺席,故 NOT NULL)。MySQL 侧 twin 逐字同形。
164
- leg_key VARCHAR(64) NOT NULL,
165
- parent_tool_call_id VARCHAR(255),
197
+ leg_key VARCHAR(64) COLLATE "C" NOT NULL,
198
+ parent_tool_call_id VARCHAR(255) COLLATE "C",
166
199
  -- bound_input_hash(车5 §9 C2)——MySQL twin 逐字同形(宽度/语义见那侧注)。
167
- bound_input_hash VARCHAR(255),
168
- state VARCHAR(32) NOT NULL,
200
+ bound_input_hash VARCHAR(255) COLLATE "C",
201
+ state VARCHAR(32) COLLATE "C" NOT NULL,
169
202
  -- clay 批 TINYINT(1) 归一先例:PG 侧统一 SMALLINT 0/1(此前本家族用过 BOOLEAN,已裁弃).
170
203
  provisional SMALLINT NOT NULL DEFAULT 0,
171
204
  version INT NOT NULL DEFAULT 0,
172
- decision VARCHAR(16),
173
- decision_actor TEXT,
174
- decision_note TEXT,
205
+ decision VARCHAR(16) COLLATE "C",
206
+ decision_actor TEXT COLLATE "C",
207
+ decision_note TEXT COLLATE "C",
175
208
  decided_at_ms BIGINT,
176
- denied_reason VARCHAR(64),
177
- gate_token VARCHAR(512),
178
- gate_bound_call_id VARCHAR(255),
179
- gate_bound_input_hash VARCHAR(255),
180
- idempotency_key VARCHAR(255),
181
- card_json TEXT NOT NULL,
209
+ denied_reason VARCHAR(64) COLLATE "C",
210
+ gate_token VARCHAR(120) COLLATE "C",
211
+ gate_bound_call_id VARCHAR(255) COLLATE "C",
212
+ gate_bound_input_hash VARCHAR(255) COLLATE "C",
213
+ idempotency_key VARCHAR(255) COLLATE "C",
214
+ card_json TEXT COLLATE "C" NOT NULL,
182
215
  schema_version INT NOT NULL,
183
216
  expires_at_ms BIGINT NOT NULL,
184
217
  created_at_ms BIGINT NOT NULL,
@@ -330,13 +363,17 @@ export class SqlApprovalAskStore {
330
363
  }
331
364
  async ensureAsk(row) {
332
365
  const now = row.createdAtMs;
366
+ /** 本次 ask 插入的 affected 行数(1 = 真插入 / 0 = 幂等命中)——判别位的**唯一**来源,见
367
+ * {@link EnsureAskResult}。两方言各自的语句形不同(`INSERT IGNORE` vs `ON CONFLICT DO NOTHING`),
368
+ * 但 affected 的语义是同一个,`SqlDriver` 已把 mysql2 的 `affectedRows` 与 pg 的 `rowCount` 归一。 */
369
+ let insertedRows = 0;
333
370
  const conn = await this.db.connect();
334
371
  try {
335
372
  await conn.begin();
336
373
  await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_BATCHES_TABLE} (batch_id, task_id, state, version, created_at_ms, updated_at_ms) VALUES (?, ?, 'OPEN', 0, ?, ?)`, `INSERT INTO ${APPROVAL_BATCHES_TABLE} (batch_id, task_id, state, version, 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]);
337
374
  // 🔴 `idempotency_key` **不在 INSERT 列清单里**(车4 §12-E 双写者收口):该列已归回决专用,只有
338
375
  // `decideAsk` 赢 CAS 时写它,铸行恒 NULL。留在这里的 NULL 字面量就是这条纪律的落笔处。
339
- await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASKS_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, version, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
376
+ const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASKS_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, version, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
340
377
  "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASKS_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, version, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
341
378
  "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $11, $12, $13, $14, $15) ON CONFLICT (ask_id) DO NOTHING"), [
342
379
  row.askId,
@@ -355,6 +392,7 @@ export class SqlApprovalAskStore {
355
392
  now,
356
393
  now,
357
394
  ]);
395
+ insertedRows = askInsert.affected;
358
396
  await conn.commit();
359
397
  }
360
398
  catch (err) {
@@ -372,7 +410,9 @@ export class SqlApprovalAskStore {
372
410
  const out = await this.getAsk(row.askId);
373
411
  if (!out)
374
412
  throw new Error(`ensureAsk: row vanished after insert (askId=${row.askId})`);
375
- return out;
413
+ // 判别位取**提交那一刻**引擎报的 affected,不是事后回读推断的:回读发生在提交之后,那时并发的
414
+ // 第二个调用可能已经改过行,任何「从行上反推谁插的」都是又一次猜。
415
+ return { row: out, inserted: insertedRows > 0 };
376
416
  }
377
417
  async transitionAsk(askId, from, to, patch) {
378
418
  // machine 层先判(非法转移=编程错误,throw 而非返回 false——设计定稿 §4 原话)。
@@ -163,7 +163,7 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
163
163
  handle VARBINARY(190) NOT NULL,
164
164
  scope VARCHAR(190) NOT NULL,
165
165
  owner VARCHAR(190) NOT NULL,
166
- session_scoped TINYINT NOT NULL,
166
+ session_scoped TINYINT(1) NOT NULL,
167
167
  session_id VARCHAR(190) NULL,
168
168
  parent_session_id VARCHAR(190) NULL,
169
169
  -- core 1.367 δ(additive):root 锚(listBySession 第二臂;NULL = 该记录无 root 锚,谓词天然拒)
@@ -198,29 +198,29 @@ export async function ensurePgBackgroundAgentSchema(q) {
198
198
  await q(`CREATE TABLE IF NOT EXISTS ${BACKGROUND_AGENT_TABLE} (
199
199
  scope_key VARCHAR(190) COLLATE "C" NOT NULL,
200
200
  handle VARCHAR(190) COLLATE "C" NOT NULL,
201
- scope VARCHAR(190) NOT NULL,
202
- owner VARCHAR(190) NOT NULL,
201
+ scope VARCHAR(190) COLLATE "C" NOT NULL,
202
+ owner VARCHAR(190) COLLATE "C" NOT NULL,
203
203
  session_scoped SMALLINT NOT NULL,
204
- session_id VARCHAR(190),
205
- parent_session_id VARCHAR(190),
204
+ session_id VARCHAR(190) COLLATE "C",
205
+ parent_session_id VARCHAR(190) COLLATE "C",
206
206
  -- core 1.367 δ(additive,TiDB 同案):root 锚(listBySession 第二臂)
207
- root_session_id VARCHAR(190),
208
- session_anchor VARCHAR(190),
209
- name VARCHAR(255),
210
- agent_type VARCHAR(190),
211
- status VARCHAR(16) NOT NULL,
207
+ root_session_id VARCHAR(190) COLLATE "C",
208
+ session_anchor VARCHAR(190) COLLATE "C",
209
+ name VARCHAR(255) COLLATE "C",
210
+ agent_type VARCHAR(190) COLLATE "C",
211
+ status VARCHAR(16) COLLATE "C" NOT NULL,
212
212
  spawned_at_ms BIGINT NOT NULL,
213
213
  updated_at_ms BIGINT NOT NULL,
214
214
  settled_at_ms BIGINT,
215
- usage_json TEXT,
215
+ usage_json TEXT COLLATE "C",
216
216
  rev BIGINT NOT NULL,
217
217
  -- core 1.383([1565] / design/153 件3a)ε(TiDB 同案注):updateIf 的 CAS 守卫专用列
218
- parked_checkpoint_token VARCHAR(190),
219
- park_claim_id VARCHAR(190),
218
+ parked_checkpoint_token VARCHAR(190) COLLATE "C",
219
+ park_claim_id VARCHAR(190) COLLATE "C",
220
220
  -- core 5.14.0 #22(TiDB 同案注:三态与「写侧投影」口径见 MySQL 孪生的行内注)。
221
- admitted_org_scopes TEXT,
222
- admitted_org_write_scope VARCHAR(190),
223
- record_json TEXT NOT NULL,
221
+ admitted_org_scopes TEXT COLLATE "C",
222
+ admitted_org_write_scope VARCHAR(190) COLLATE "C",
223
+ record_json TEXT COLLATE "C" NOT NULL,
224
224
  PRIMARY KEY (scope_key, handle)
225
225
  )`);
226
226
  await q(`CREATE INDEX IF NOT EXISTS idx_bga_anchor ON ${BACKGROUND_AGENT_TABLE} (scope_key, session_anchor, spawned_at_ms)`);
@@ -11,7 +11,7 @@
11
11
  * - **owned-id isolation** — `poll`/`kill` look the shellId up in THIS manager's registry; an id from another env
12
12
  * (or a forged one) is `not_found`, never resolved against the provider (design/103 §3.8 / interface 越权契约).
13
13
  * - **maxConcurrent** — counts only `running` shells (terminal entries are kept for residual polling but free their
14
- * slot, so "KillShell one first" is actionable — matches the TOC `maxConcurrent 终态占槽` review fix).
14
+ * slot, so "TaskStop one first" is actionable — matches the TOC `maxConcurrent 终态占槽` review fix).
15
15
  * - **timeout hard wall** — a control-plane timer kills the shell and flips it to `killed` at the bounded BG
16
16
  * timeout (design/103 §3.6); the driver may set an additional provider-level backstop ≥ this wall.
17
17
  * - **dispose** — kills + cleans up EVERY shell, best-effort, MUST NOT throw, idempotent (design/103 §3.7).
@@ -11,7 +11,7 @@
11
11
  * - **owned-id isolation** — `poll`/`kill` look the shellId up in THIS manager's registry; an id from another env
12
12
  * (or a forged one) is `not_found`, never resolved against the provider (design/103 §3.8 / interface 越权契约).
13
13
  * - **maxConcurrent** — counts only `running` shells (terminal entries are kept for residual polling but free their
14
- * slot, so "KillShell one first" is actionable — matches the TOC `maxConcurrent 终态占槽` review fix).
14
+ * slot, so "TaskStop one first" is actionable — matches the TOC `maxConcurrent 终态占槽` review fix).
15
15
  * - **timeout hard wall** — a control-plane timer kills the shell and flips it to `killed` at the bounded BG
16
16
  * timeout (design/103 §3.6); the driver may set an additional provider-level backstop ≥ this wall.
17
17
  * - **dispose** — kills + cleans up EVERY shell, best-effort, MUST NOT throw, idempotent (design/103 §3.7).
@@ -207,7 +207,7 @@ export class BackgroundShellManager {
207
207
  /** The shared spawn/adopt body: limit check, opaque id, terminal-latch buffering, BG-timeout hard wall. */
208
208
  async register(launch, timeoutSec) {
209
209
  if (this.liveCount() >= this.caps.maxConcurrent) {
210
- return fail(new BackgroundShellError("limit_exceeded", `Too many running background shells (max ${this.caps.maxConcurrent}); KillShell one first.`));
210
+ return fail(new BackgroundShellError("limit_exceeded", `Too many running background shells (max ${this.caps.maxConcurrent}); TaskStop one first.`));
211
211
  }
212
212
  const bgTimeoutSec = this.boundedBgTimeoutSec(timeoutSec); // #131-T0:与 adoptSync 同座(NaN 当缺席)
213
213
  // Opaque brand — NOT derived from the provider job/pid (design/103 §3.8 越权红线).
@@ -4,8 +4,8 @@ import { mysqlDriver, pgDriver } from "./sql-driver.js";
4
4
  * centrally in tidb-pool.ts (no PG-side twin needed there — production wires ONE ensureTidbSchema). */
5
5
  export const PG_BREAKER_STATE_SCHEMA = [
6
6
  `CREATE TABLE IF NOT EXISTS circuit_breaker (
7
- breaker_key VARCHAR(190) NOT NULL,
8
- phase VARCHAR(16) NOT NULL,
7
+ breaker_key VARCHAR(190) COLLATE "C" NOT NULL,
8
+ phase VARCHAR(16) COLLATE "C" NOT NULL,
9
9
  failures INTEGER NOT NULL DEFAULT 0,
10
10
  opened_at TIMESTAMPTZ(3),
11
11
  updated_at TIMESTAMPTZ(3) NOT NULL,
@@ -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 Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor } from "@sema-agent/core";
3
+ import { type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor, type StoreDurability, type StoreFidelity } from "@sema-agent/core";
4
4
  import { type SqlDriver } from "./sql-driver.js";
5
5
  /**
6
6
  * design/80 D-1 (§3 invariant #3 — crash-safe reaper backstop): an ABSOLUTE upper bound on a pending
@@ -77,6 +77,16 @@ export interface CheckpointAskCandidate {
77
77
  /** `checkpoint.status` 列(权威;`pending` | `resolved` | `expired`)。不做过滤,由收敛器判活性。 */
78
78
  status: string;
79
79
  createdAtMs: number;
80
+ /**
81
+ * 🔴 和解三元组的第一维(#168 件1,黑板 [2897]③①)—— checkpoint 自己带的 `sourceTaskId`(core 在
82
+ * suspendAsk 铸行时恒填该腿的 `sessionId`)。**没有对应的列**,只能从 blob 窄读;读不出/缺席 = null。
83
+ *
84
+ * 为什么必须进候选集:读口按 `(scope, session_id, tool_call_id)` 查,而委派子代那条腿的 ask 落行时
85
+ * `session_id` 记的是**投递上下文**(根会话),park 却发生在**子代自己**的 sessionId 上 —— 只靠
86
+ * (toolCallId, hash) 两维,一条根腿的 park 会被一只子代的 ask 认领(摘要相同的两次同 args 调用完全
87
+ * 正常)。三元组全同才算同一件事。
88
+ */
89
+ sourceTaskId: string | null;
80
90
  /** 该 park 绑定的 tool call(= `pendingAction.toolCallId`)。`unparseable` 行为 null。 */
81
91
  boundCallId: string | null;
82
92
  /** 服务端铸的入参摘要——判据 1 的第二道硬等式(与 `approval_asks.bound_input_hash` 相等才算命中)。 */
@@ -109,13 +119,54 @@ export declare class SqlCheckpointStore implements CheckpointStore {
109
119
  protected readonly logger?: {
110
120
  info?(msg: string, meta?: unknown): void;
111
121
  } | undefined;
122
+ /**
123
+ * `CheckpointStore.durability` 声明(#167 欠账,#168 件5)—— 行落在 MySQL-protocol / PostgreSQL 的
124
+ * `checkpoint` 表里,进程重启、副本轮换、整机重建都不丢 ⇒ `"durable"`,如实。
125
+ *
126
+ * 🔴 为什么这一格空着是有代价的:core 的 `resolveDeclaredDurability` 把**缺席**折成 `"process-local"`
127
+ * (它不能替一个没表态的店猜),于是静态装配面对每一个真持久部署都读出 `process_local`,
128
+ * `GET /v1/diagnostics/wiring` 的 park 车道读数与启动自检的那条警告都因此不可信 —— 而 park 正是流内
129
+ * 审批协议的降级目的地,「重启后还赎不赎得回」是运维必须能一眼看见的事。声明是店自己的责任,不是
130
+ * 消费侧靠 backend.kind 猜出来的。
131
+ */
132
+ readonly durability: StoreDurability;
133
+ /**
134
+ * `CheckpointStore.fidelity` 声明(core 5.17.0 [3052] 提货批 #172)——**如实按介质判**:本店把整个
135
+ * checkpoint 经 {@link SqlCheckpointStore.json} 编码进一个 JSON 列(TiDB 逐字文本 / PG lossless
136
+ * `::jsonb` 协议信封),读侧 `parseJson` 还原 ⇒ 能扛过 round-trip 的只有 JSON 值域,`"json"`。
137
+ *
138
+ * 🔴 为什么必须显式写、哪怕缺席也折向 json:core 的 `resolveDeclaredFidelity` 对缺席是 fail-closed
139
+ * (读 json),所以沉默不会立刻错——但沉默**表达不出**「我核对过我的介质就是这个宽度」。park 铸行
140
+ * 的 args / preview / 风险描述 / `boundInputHash` 全部从这一格算出的投影铸;哪天这四行编码里任何
141
+ * 一处改了介质(换存储格式、换列类型),声明在场才有东西可以红,沉默那格只会安静地按错宽度铸出
142
+ * 一份「审批人看到的 ≠ 盘上躺着的 ≠ resume 执行的」。同 `durability` 的 #168 件5 教训:表态是店
143
+ * 自己的责任,不是消费侧靠 backend.kind 猜。
144
+ *
145
+ * ⚠️ **已知残余(不是遮掩,是这两个词表达不了的那一格)**:PG 臂比 `"json"` 字面**略窄** ——
146
+ * {@link pgProtocolJsonStringify} 对含 NUL / lone surrogate 的串 fail-loud 拒绝(R4-H1 有意裁定:
147
+ * 复核面必须与真执行的 args 在 NUL 那一位上一致,「悄悄清洗再存」是不可接受的那一支),而 core 的
148
+ * json 宽度收下这些码位。`StoreFidelity` 的闭集只有 `"structured-clone" | "json"`,没有第三个词能说
149
+ * 「json 减去本介质存不下的码位」——声明 `"structured-clone"` 是大得多的谎,所以 `"json"` 仍是两者
150
+ * 里唯一诚实的选择。后果有界且 fail-closed:core 5.17.0 起 park 铸行失败会把 cause 带到 gate、追加
151
+ * 到 fallback 的 deny 上,这条 args 退回**同步门**由人判(不静默漏批、不挂死)。边界钉在
152
+ * `wiring-governance-operator.test.ts` 的 #172 组;已上报上游求一个能表达该宽度的词。
153
+ */
154
+ readonly fidelity: StoreFidelity;
112
155
  constructor(db: SqlDriver, logger?: {
113
156
  info?(msg: string, meta?: unknown): void;
114
157
  } | undefined);
115
158
  /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
116
159
  private q;
117
- /** JSON column binding: TiDB stores the string verbatim; PG goes through the LOSSLESS protocol envelope
118
- * (R4-H1 the operator's review surface must agree with the executed args at the NUL position). */
160
+ /** JSON column binding. TiDB: plain `JSON.stringify`, stored verbatim. PG: `pgProtocolJsonStringify`
161
+ * ordinary `JSON.stringify` that **refuses** (throws `PgUnstorableError`) when the value carries a code
162
+ * point PG cannot hold (NUL / lone surrogate); the `::jsonb` at the call site is just the bind cast, not
163
+ * an escaping layer.
164
+ *
165
+ * "LOSSLESS" in R4-H1 means exactly **"never silently lossy"**, not "encodes everything": the contrast is
166
+ * with `pgSanitizeText`, the lossy U+FFFD-scarring path used for CONTENT faces. An approval row is not a
167
+ * content face — the operator's review surface has to agree with the executed args AT the NUL position, so
168
+ * scrubbing the byte and storing the scrubbed row is the unacceptable arm; refusing loudly is the chosen one.
169
+ * That refusal is why {@link SqlCheckpointStore.fidelity} carries a documented residual (see it). */
119
170
  private json;
120
171
  private isDupKey;
121
172
  /** Create-once. core mints the token (`mintCheckpointToken`) and calls this during suspend. */
@@ -175,8 +226,11 @@ export declare class SqlCheckpointStore implements CheckpointStore {
175
226
  *
176
227
  * 🔴 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须自己有 CAS:两个并发 steer 读到同一份
177
228
  * 队列、后写者整个盖掉前者 —— 就是 #147 要消灭的静默丢失换了个位置复发。守卫是 `pending_steer_rev`
178
- * **专用整数列**(不是拿旧列值比:本表无显式 COLLATE,*_ci 排序规则会把只差大小写的两份信封判等 ⇒
179
- * CAS 假命中;也不是复用 resolve `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
229
+ * **专用整数列**(不是拿旧列值比:整数按构造唯一,而长 JSON 文本的相等性取决于排序规则/字段序/空白与
230
+ * 转义形,任一处判等松一点就是一次 CAS 假命中 = 悄悄丢一条 steer。#192 A11 更正:本注原先的理由写的是
231
+ * "本表无显式 COLLATE,*_ci 会把只差大小写的两份信封判等",那半已不成立——`checkpoint` 现在钉了
232
+ * `COLLATE utf8mb4_bin`/PG 侧 `COLLATE "C"`;结论不变,理由回到上面那条更根本的;
233
+ * 也不是复用 resolve 的 `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
180
234
  * CAS 输了就重读重算(队列只有 3 个位子,争用者天然极少);给足冗余仍全败 ⇒ **抛**,不返回 false ——
181
235
  * `false` 在调用方的语义是「没有 pending 行可转向」(HTTP 409「已不再挂起」),把争用报成那个是撒谎。
182
236
  *
@@ -268,9 +322,14 @@ export declare class SqlCheckpointStore implements CheckpointStore {
268
322
  * 扫描(§8 C-6)。什么算读不出:blob 的 `version` 超出本 build 支持(`get()` 那条前向兼容门在这里
269
323
  * 不能 throw,否则一条超前行会让整个 session 的对账永久卡死)、或 blob JSON 坏。
270
324
  *
271
- * 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId` 盖下来的权威投影,命中它
272
- * 的行零解析即候选;列为 NULL 的行(无工具动作的 park,或列存在之前的旧行)才回落去解 blob——解得出且
273
- * 相等才算候选,解不出就标 `unparseable`。
325
+ * 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId` 盖下来的权威投影,SQL 谓词
326
+ * 先按它(或 NULL)收窄;列为 NULL 的行(无工具动作的 park,或列存在之前的旧行)靠解 blob 补判——解得出
327
+ * 且相等才算候选,解不出就标 `unparseable`。
328
+ * 🔴 **没有「列命中即零解析」的快路径**(原注写过,已作废,别照它优化):函数体对**每一行**无条件解
329
+ * blob,原因有二 ——(a) 前向兼容门与 blob 可读性门必须门在**所有**命中路径之前(codex F3 + 确认轮:
330
+ * 列长得对不代表 blob 读得出,放行一条读不出的行去 `bindBatch` 会把 ask 钉成 PARKED + 一张本进程读不出
331
+ * 的 resume 坐标,而 PARKED 不可回滚);(b) `#168` 之后 blob 顶层的 `sourceTaskId` 是和解三元组的第一
332
+ * 维,列命中行结构上也必须解 blob 才拿得到它。
274
333
  */
275
334
  findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
276
335
  /**
@@ -99,8 +99,15 @@ function pendingActionToolCallId(blob) {
99
99
  }
100
100
  if (typeof parsed !== "object" || parsed === null)
101
101
  return { readable: false };
102
+ // 🔴 `sourceTaskId` 是 checkpoint 的**顶层**字段(不在 pendingAction 里),没有对应的列 ⇒ 只能从
103
+ // blob 读。它是和解三元组的第一维(#168 件1);形不合(有这个键但不是字符串)= 结构漂移,按本函数
104
+ // 一贯的收窄纪律判 `readable: false`,不静默折成缺席。
105
+ const rawSource = "sourceTaskId" in parsed ? parsed.sourceTaskId : null;
106
+ if (rawSource !== null && rawSource !== undefined && typeof rawSource !== "string")
107
+ return { readable: false };
108
+ const sourceTaskId = typeof rawSource === "string" ? rawSource : null;
102
109
  if (!("pendingAction" in parsed))
103
- return { readable: true, toolCallId: null, boundInputHash: null, kind: null }; // 无 pendingAction 的 park:确定不匹配
110
+ return { readable: true, toolCallId: null, boundInputHash: null, kind: null, sourceTaskId }; // 无 pendingAction 的 park:确定不匹配
104
111
  const pa = parsed.pendingAction;
105
112
  if (typeof pa !== "object" || pa === null)
106
113
  return { readable: false };
@@ -108,7 +115,7 @@ function pendingActionToolCallId(blob) {
108
115
  if (kind !== null && typeof kind !== "string")
109
116
  return { readable: false };
110
117
  if (!("toolCallId" in pa))
111
- return { readable: true, toolCallId: null, boundInputHash: null, kind }; // plan_review / task_done / resource_limit 腿
118
+ return { readable: true, toolCallId: null, boundInputHash: null, kind, sourceTaskId }; // plan_review / task_done / resource_limit 腿
112
119
  const id = pa.toolCallId;
113
120
  if (typeof id !== "string")
114
121
  return { readable: false };
@@ -122,7 +129,7 @@ function pendingActionToolCallId(blob) {
122
129
  const bih = "boundInputHash" in pa ? pa.boundInputHash : null;
123
130
  if (bih !== null && typeof bih !== "string")
124
131
  return { readable: false };
125
- return { readable: true, toolCallId: id, boundInputHash: bih, kind };
132
+ return { readable: true, toolCallId: id, boundInputHash: bih, kind, sourceTaskId };
126
133
  }
127
134
  const DUP_ENTRY = 1062; // MySQL/TiDB ER_DUP_ENTRY
128
135
  const PG_UNIQUE_VIOLATION = "23505";
@@ -228,6 +235,39 @@ const STEER_CAS_ATTEMPTS = 8;
228
235
  export class SqlCheckpointStore {
229
236
  db;
230
237
  logger;
238
+ /**
239
+ * `CheckpointStore.durability` 声明(#167 欠账,#168 件5)—— 行落在 MySQL-protocol / PostgreSQL 的
240
+ * `checkpoint` 表里,进程重启、副本轮换、整机重建都不丢 ⇒ `"durable"`,如实。
241
+ *
242
+ * 🔴 为什么这一格空着是有代价的:core 的 `resolveDeclaredDurability` 把**缺席**折成 `"process-local"`
243
+ * (它不能替一个没表态的店猜),于是静态装配面对每一个真持久部署都读出 `process_local`,
244
+ * `GET /v1/diagnostics/wiring` 的 park 车道读数与启动自检的那条警告都因此不可信 —— 而 park 正是流内
245
+ * 审批协议的降级目的地,「重启后还赎不赎得回」是运维必须能一眼看见的事。声明是店自己的责任,不是
246
+ * 消费侧靠 backend.kind 猜出来的。
247
+ */
248
+ durability = "durable";
249
+ /**
250
+ * `CheckpointStore.fidelity` 声明(core 5.17.0 [3052] 提货批 #172)——**如实按介质判**:本店把整个
251
+ * checkpoint 经 {@link SqlCheckpointStore.json} 编码进一个 JSON 列(TiDB 逐字文本 / PG lossless
252
+ * `::jsonb` 协议信封),读侧 `parseJson` 还原 ⇒ 能扛过 round-trip 的只有 JSON 值域,`"json"`。
253
+ *
254
+ * 🔴 为什么必须显式写、哪怕缺席也折向 json:core 的 `resolveDeclaredFidelity` 对缺席是 fail-closed
255
+ * (读 json),所以沉默不会立刻错——但沉默**表达不出**「我核对过我的介质就是这个宽度」。park 铸行
256
+ * 的 args / preview / 风险描述 / `boundInputHash` 全部从这一格算出的投影铸;哪天这四行编码里任何
257
+ * 一处改了介质(换存储格式、换列类型),声明在场才有东西可以红,沉默那格只会安静地按错宽度铸出
258
+ * 一份「审批人看到的 ≠ 盘上躺着的 ≠ resume 执行的」。同 `durability` 的 #168 件5 教训:表态是店
259
+ * 自己的责任,不是消费侧靠 backend.kind 猜。
260
+ *
261
+ * ⚠️ **已知残余(不是遮掩,是这两个词表达不了的那一格)**:PG 臂比 `"json"` 字面**略窄** ——
262
+ * {@link pgProtocolJsonStringify} 对含 NUL / lone surrogate 的串 fail-loud 拒绝(R4-H1 有意裁定:
263
+ * 复核面必须与真执行的 args 在 NUL 那一位上一致,「悄悄清洗再存」是不可接受的那一支),而 core 的
264
+ * json 宽度收下这些码位。`StoreFidelity` 的闭集只有 `"structured-clone" | "json"`,没有第三个词能说
265
+ * 「json 减去本介质存不下的码位」——声明 `"structured-clone"` 是大得多的谎,所以 `"json"` 仍是两者
266
+ * 里唯一诚实的选择。后果有界且 fail-closed:core 5.17.0 起 park 铸行失败会把 cause 带到 gate、追加
267
+ * 到 fallback 的 deny 上,这条 args 退回**同步门**由人判(不静默漏批、不挂死)。边界钉在
268
+ * `wiring-governance-operator.test.ts` 的 #172 组;已上报上游求一个能表达该宽度的词。
269
+ */
270
+ fidelity = "json";
231
271
  constructor(db, logger) {
232
272
  this.db = db;
233
273
  this.logger = logger;
@@ -236,8 +276,16 @@ export class SqlCheckpointStore {
236
276
  q(tidb, pg) {
237
277
  return this.db.dialect === "tidb" ? tidb : pg;
238
278
  }
239
- /** JSON column binding: TiDB stores the string verbatim; PG goes through the LOSSLESS protocol envelope
240
- * (R4-H1 the operator's review surface must agree with the executed args at the NUL position). */
279
+ /** JSON column binding. TiDB: plain `JSON.stringify`, stored verbatim. PG: `pgProtocolJsonStringify`
280
+ * ordinary `JSON.stringify` that **refuses** (throws `PgUnstorableError`) when the value carries a code
281
+ * point PG cannot hold (NUL / lone surrogate); the `::jsonb` at the call site is just the bind cast, not
282
+ * an escaping layer.
283
+ *
284
+ * "LOSSLESS" in R4-H1 means exactly **"never silently lossy"**, not "encodes everything": the contrast is
285
+ * with `pgSanitizeText`, the lossy U+FFFD-scarring path used for CONTENT faces. An approval row is not a
286
+ * content face — the operator's review surface has to agree with the executed args AT the NUL position, so
287
+ * scrubbing the byte and storing the scrubbed row is the unacceptable arm; refusing loudly is the chosen one.
288
+ * That refusal is why {@link SqlCheckpointStore.fidelity} carries a documented residual (see it). */
241
289
  json(value, label) {
242
290
  return dialectProtocolJsonEncoder(this.db.dialect)(value, label);
243
291
  }
@@ -300,8 +348,12 @@ export class SqlCheckpointStore {
300
348
  // design/80 D-1: surface the reopen-by-reason + OCC fields off the AUTHORITATIVE columns (the blob is the
301
349
  // suspend-time snapshot; resolve/reopen mutate only the columns). Core's resume reads these to validate an
302
350
  // env_failed re-resume against the persisted winner, and to require the `rev` it observed is still live (OCC).
303
- // resolvedOutcome is DERIVED from the stored full outcome (winnerFromOutcome {boundCallId,decision,
304
- // updatedInput?}), so it is preserved across a reopen for free (reopen leaves `outcome` untouched).
351
+ // resolvedOutcome is DERIVED from the stored full outcome by core's `winnerFromOutcome`, so it is preserved
352
+ // across a reopen for free (reopen leaves `outcome` untouched). Do NOT read a key list into this line: the
353
+ // derived shape is core's and it GROWS (`answer` since 5.7.0, `reason` since 5.16.0 — the operator's deny
354
+ // note IS persisted; an older copy of this comment enumerated only {boundCallId,decision,updatedInput?} and
355
+ // read as if it weren't). We store the whole outcome blob and round-trip it, so additive keys ride for free
356
+ // — `ResolvedOutcome` in core's `checkpoint-store.d.ts` is the single owner of that list.
305
357
  const outcomeRaw = parseJson(r.outcome);
306
358
  if (outcomeRaw)
307
359
  cp.resolvedOutcome = winnerFromOutcome(outcomeRaw);
@@ -402,8 +454,11 @@ export class SqlCheckpointStore {
402
454
  *
403
455
  * 🔴 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须自己有 CAS:两个并发 steer 读到同一份
404
456
  * 队列、后写者整个盖掉前者 —— 就是 #147 要消灭的静默丢失换了个位置复发。守卫是 `pending_steer_rev`
405
- * **专用整数列**(不是拿旧列值比:本表无显式 COLLATE,*_ci 排序规则会把只差大小写的两份信封判等 ⇒
406
- * CAS 假命中;也不是复用 resolve `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
457
+ * **专用整数列**(不是拿旧列值比:整数按构造唯一,而长 JSON 文本的相等性取决于排序规则/字段序/空白与
458
+ * 转义形,任一处判等松一点就是一次 CAS 假命中 = 悄悄丢一条 steer。#192 A11 更正:本注原先的理由写的是
459
+ * "本表无显式 COLLATE,*_ci 会把只差大小写的两份信封判等",那半已不成立——`checkpoint` 现在钉了
460
+ * `COLLATE utf8mb4_bin`/PG 侧 `COLLATE "C"`;结论不变,理由回到上面那条更根本的;
461
+ * 也不是复用 resolve 的 `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
407
462
  * CAS 输了就重读重算(队列只有 3 个位子,争用者天然极少);给足冗余仍全败 ⇒ **抛**,不返回 false ——
408
463
  * `false` 在调用方的语义是「没有 pending 行可转向」(HTTP 409「已不再挂起」),把争用报成那个是撒谎。
409
464
  *
@@ -595,9 +650,14 @@ export class SqlCheckpointStore {
595
650
  * 扫描(§8 C-6)。什么算读不出:blob 的 `version` 超出本 build 支持(`get()` 那条前向兼容门在这里
596
651
  * 不能 throw,否则一条超前行会让整个 session 的对账永久卡死)、或 blob JSON 坏。
597
652
  *
598
- * 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId` 盖下来的权威投影,命中它
599
- * 的行零解析即候选;列为 NULL 的行(无工具动作的 park,或列存在之前的旧行)才回落去解 blob——解得出且
600
- * 相等才算候选,解不出就标 `unparseable`。
653
+ * 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId` 盖下来的权威投影,SQL 谓词
654
+ * 先按它(或 NULL)收窄;列为 NULL 的行(无工具动作的 park,或列存在之前的旧行)靠解 blob 补判——解得出
655
+ * 且相等才算候选,解不出就标 `unparseable`。
656
+ * 🔴 **没有「列命中即零解析」的快路径**(原注写过,已作废,别照它优化):函数体对**每一行**无条件解
657
+ * blob,原因有二 ——(a) 前向兼容门与 blob 可读性门必须门在**所有**命中路径之前(codex F3 + 确认轮:
658
+ * 列长得对不代表 blob 读得出,放行一条读不出的行去 `bindBatch` 会把 ask 钉成 PARKED + 一张本进程读不出
659
+ * 的 resume 坐标,而 PARKED 不可回滚);(b) `#168` 之后 blob 顶层的 `sourceTaskId` 是和解三元组的第一
660
+ * 维,列命中行结构上也必须解 blob 才拿得到它。
601
661
  */
602
662
  async findCheckpointCandidatesForAsk(scope, sessionId, toolCallId, sinceMs) {
603
663
  const { rows } = await this.db.query(this.q("SELECT token, status, created_at, tool_call_id, bound_input_hash, version, checkpoint FROM checkpoint " +
@@ -605,11 +665,14 @@ export class SqlCheckpointStore {
605
665
  "WHERE scope=$1 AND session_id=$2 AND created_at>=$3 AND (tool_call_id=$4 OR tool_call_id IS NULL) ORDER BY created_at ASC"), [scope, sessionId, sinceMs, toolCallId]);
606
666
  const out = [];
607
667
  for (const r of rows) {
668
+ // `sourceTaskId: null` 是**所有 unparseable 臂的共同底**(读不出的行不许带出一个可用于身份比对的
669
+ // 值);健康臂在下面用 blob 解出来的值覆盖它。
608
670
  const base = {
609
671
  token: String(r.token),
610
672
  status: String(r.status),
611
673
  createdAtMs: Number(r.created_at),
612
674
  boundInputHash: r.bound_input_hash == null ? null : String(r.bound_input_hash),
675
+ sourceTaskId: null,
613
676
  };
614
677
  // 🔴 前向兼容门必须在**任何**命中路径之前(codex 复审 F3,2026-08-06 真缺陷):版本超前的行是
615
678
  // `get()` 明确拒读的行,而 `tool_call_id` 列命中与否跟能不能读懂 blob 毫无关系。若让列的快路径
@@ -652,7 +715,7 @@ export class SqlCheckpointStore {
652
715
  // 列在场时以列为准(权威投影);列为 NULL 的旧行回落用 blob 解出来的值。
653
716
  const effective = col ?? derived.toolCallId;
654
717
  if (effective === toolCallId)
655
- out.push({ ...base, boundInputHash: effectiveHash, boundCallId: effective });
718
+ out.push({ ...base, boundInputHash: effectiveHash, sourceTaskId: derived.sourceTaskId, boundCallId: effective });
656
719
  // 解得出、但不是这只 ask 的 callId(或这条 park 本来就没有工具动作)⇒ **确定**不是候选:
657
720
  // 既不返回也不标坏行(标坏行会让收敛器把一条明确的「不匹配」当成「不确定」)。
658
721
  }
@@ -7,7 +7,7 @@ import { type BakeStatus, type BakeState, type BakeRecord, type BakeEvent, type
7
7
  * TINYINT(1) 归一(clay 批 2026-07-26):布尔列的 PG 映射统一 SMALLINT 0/1(此前本家族用 BOOLEAN)——
8
8
  * 两方言驱动读回同为 number ⇒ 不再存在「忘了配的适配器」这个病族;写入参与 SQL 字面量一律 0/1,
9
9
  * 读侧 `!!`/`Boolean()` 保持,store 出口 JS 类型逐字节不变(真双库套件的严格值断言为证)。
10
- * The uq_line UNIQUE is given an explicit name so a 23505 can be attributed to it via err.constraint. */
10
+ * The line UNIQUE is given the explicit name uq_image_bake_event_line so a 23505 can be attributed to it via err.constraint. */
11
11
  export declare const PG_IMAGE_BAKE_SCHEMA: string[];
12
12
  /** Idempotent self-contained PG schema apply (for the integration test; central aggregation is done separately). */
13
13
  export declare function ensurePgImageBakeSchema(pool: PgPool | PoolClient, poolName?: string): Promise<void>;