@sema-agent/server 7.43.0 → 7.44.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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,15 +1,6 @@
1
- /**
2
- * LocalTaskAttachmentStore —— D-1 附件店的 local 形(seamless-local-cloud 铁律:StoreBackend
3
- * kind tidb|pg|local 三态,附件面不得 cloud-only)。
4
- *
5
- * 布局:`<root>/attachments/<id>.json`(meta)+ `<id>.bin`(bytes)。id=uuidv7(server 铸,不含用户
6
- * 输入 ⇒ 文件名安全);meta 内含 owner/session 轴,门在读侧比对(单机单进程,无并发租户;SQL 店才是
7
- * 多租户形)。写=tmp+rename 原子(半写行不可见);索引=首次使用时全目录水合进内存 Map,之后同进程
8
- * 内存权威(local 模式规模小;跨进程并发不在 local 契约内——与 file-run-store 同口径)。
9
- */
10
1
  import { promises as fsp } from "node:fs";
11
2
  import { join } from "node:path";
12
- const ID_RE = /^[0-9a-f-]{16,64}$/i; // uuid 形(server 铸);水合时非此形的文件一律跳过
3
+ const ID_RE = /^[0-9a-f-]{16,64}$/i;
13
4
  export class LocalTaskAttachmentStore {
14
5
  dir;
15
6
  metas;
@@ -18,8 +9,6 @@ export class LocalTaskAttachmentStore {
18
9
  this.dir = join(rootDir, "attachments");
19
10
  }
20
11
  hydrate() {
21
- // #131-1:记「在飞的 promise」而非「结果」(同仓正确形=local-session-store.loaded)。记结果时,
22
- // 同进程两条并发首触各自 readdir,晚到的空快照会覆盖已被 put() 写入的索引——盘上有档、内存答 null。
23
12
  this.hydration ??= (async () => {
24
13
  const m = new Map();
25
14
  try {
@@ -35,12 +24,10 @@ export class LocalTaskAttachmentStore {
35
24
  m.set(id, meta);
36
25
  }
37
26
  catch {
38
- /* 半写/损坏 meta:跳过(bin 无 meta 即孤儿,reap 收) */
39
27
  }
40
28
  }
41
29
  }
42
30
  catch {
43
- /* 目录不存在 = 空店 */
44
31
  }
45
32
  this.metas = m;
46
33
  return m;
@@ -55,7 +42,6 @@ export class LocalTaskAttachmentStore {
55
42
  const metaTmp = join(this.dir, `${rec.id}.json.tmp`);
56
43
  await fsp.writeFile(binTmp, Buffer.from(content));
57
44
  await fsp.rename(binTmp, join(this.dir, `${rec.id}.bin`));
58
- // meta 后落:meta 在场 ⇒ bytes 必在场(读侧无半态)
59
45
  await fsp.writeFile(metaTmp, JSON.stringify(meta));
60
46
  await fsp.rename(metaTmp, join(this.dir, `${rec.id}.json`));
61
47
  metas.set(rec.id, { ...meta });
@@ -72,7 +58,7 @@ export class LocalTaskAttachmentStore {
72
58
  return new Uint8Array(await fsp.readFile(join(this.dir, `${id}.bin`)));
73
59
  }
74
60
  catch {
75
- return null; // meta 在 bytes 丢(外力删档)——按不存在答,不抛
61
+ return null;
76
62
  }
77
63
  }
78
64
  async persistMeta(m) {
@@ -1,17 +1,11 @@
1
1
  import { pgHasUnstorable, pgSanitizeText, PgUnstorableError } from "./pg-safe-json.js";
2
2
  export const MAILBOX_TABLE = "mailbox";
3
3
  export const MAILBOX_MSG_TABLE = "mailbox_message";
4
- /** InMemory append 同文案形(default-deny 谓词精神:无 scope 的盒=路由不明,写入即拒)。 */
5
4
  function assertScope(scope) {
6
5
  if (scope === undefined || scope === "") {
7
6
  throw new Error("MailboxStore.append: refusing a message without a scope");
8
7
  }
9
8
  }
10
- /** 键长守卫(复审 F5):TiDB `INSERT IGNORE` 对超 190 字节键**静默截断**(warning 降级)——截断键
11
- * 与全长参数的字节比对恒 miss(seq 分配读空行 TypeError / 隔离键错位);PG 侧是干净 22001,同点
12
- * 提前拒 = 双方言同文案 fail-loud。 */
13
- /** D4(试剂盒揪出,RB-251 语义):身份键含 NUL/控制符——TiDB VARBINARY 能存但与文件头「两后端一致
14
- * 拒绝」的声明不符(该声明此前是谎);写入面双方言同精神拒绝(PG 臂经 PgUnstorableError 已拒)。 */
15
9
  function assertIdentityChars(what, v) {
16
10
  if ([...v].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f)) {
17
11
  throw new Error(`MailboxStore: ${what} contains control characters (refusing — identity keys must be byte-exact addressable)`);
@@ -25,25 +19,7 @@ function assertKeyBytes(what, v) {
25
19
  function bufToStr(v) {
26
20
  return Buffer.isBuffer(v) ? v.toString("utf8") : String(v);
27
21
  }
28
- /** `hopChain` 的 JSON 文本列上界(TiDB `TEXT` = 65535 字节)。core 侧链本就有界
29
- * (`PEER_ADMISSION_DEFAULTS.maxChainLength` = 28,`PEER_HOP_CHAIN_WINDOW` = 32,元素是 peerAxisToken
30
- * 短串)⇒ 正常形离这个数几个数量级远;守卫存在的理由是**关掉 MySQL 的静默截断类**(非严格模式下
31
- * 超长 TEXT 写入只发 warning),截断后的 JSON 文本读侧 parse 失败 = 整条链静默变缺席,而缺席在
32
- * mailbox 上是有语义的(见下 encodeHopChain 注)。同 assertKeyBytes 精神:写入面 fail-loud。 */
33
22
  const HOP_CHAIN_MAX_BYTES = 60_000;
34
- /**
35
- * `hopChain` 存取(core 5.17.0 design/176)。**JSON 文本列**,不是清洗面也不是身份键面:
36
- *
37
- * - 为什么不清洗:链元素参与 core 的 `hop_loop` **等值比对**。内容面的 lossy 清洗(pgSanitizeText 的
38
- * U+FFFD 标记)会让两个不同 token 塌成同一个(误报环路)或让一个 token 变形(漏判环路)——两个
39
- * 方向都是把准入判决改错,与 `content` 那种「读者读到近似文本仍可用」的容忍面性质不同。
40
- * - 为什么也不需要拒绝:`JSON.stringify` 是 **well-formed**(ES2019)——NUL 转义成 `\u0000`、lone
41
- * surrogate 转义成 `\udXXX`,产物恒是 ASCII 可存字节。于是 PG 的 unstorable 面根本不会被触及,
42
- * 两方言都拿到逐字 round-trip,不必像 `from`/`scope`/`handle` 那样走拒绝式。
43
- * - **缺席 ≠ 空数组**:core 明示 mailbox 的 `hopChain` 在场性本身携带语义(「经 sema 准入门录入」vs
44
- * 「外部/存量写入」,是整份契约里唯一允许从在场性推断的例外)。所以 NULL 读回 = 不铸键,`[]` 读回
45
- * = 铸一个空数组键,两者绝不互相折叠。
46
- */
47
23
  function encodeHopChain(hopChain) {
48
24
  if (hopChain === undefined)
49
25
  return null;
@@ -53,8 +29,6 @@ function encodeHopChain(hopChain) {
53
29
  }
54
30
  return text;
55
31
  }
56
- /** 读侧:NULL/缺席 ⇒ undefined(不铸键);坏行(非 JSON / 非字符串数组)⇒ 同样 undefined —— 一条读不
57
- * 懂的链只能诚实降级成「没有链」,绝不半解析出一条会被拿去做环路比对的残链。 */
58
32
  function decodeHopChain(raw) {
59
33
  if (raw === null || raw === undefined)
60
34
  return undefined;
@@ -80,8 +54,6 @@ function rowToMessage(r) {
80
54
  };
81
55
  }
82
56
  export async function ensureTiDBMailboxSchema(pool) {
83
- // VARBINARY 键列(bg-agent/roster 同案);lease_owner 同为字节等价(claim 的 owner 比对是身份判)。
84
- // content LONGTEXT:消息正文可超 64K TEXT(转录段落/大 diff 转述)。
85
57
  await pool.query(`CREATE TABLE IF NOT EXISTS ${MAILBOX_TABLE} (
86
58
  scope_key VARBINARY(190) NOT NULL,
87
59
  handle VARBINARY(190) NOT NULL,
@@ -157,15 +129,11 @@ export class TiDBMailboxStore {
157
129
  assertIdentityChars("handle", handle);
158
130
  assertKeyBytes("scope", scope);
159
131
  assertKeyBytes("handle", handle);
160
- const hopChain = encodeHopChain(msg.hopChain); // 事务外先编码:超界 fail-loud 必须在建盒/分配 seq 之前
161
- // ensure-first 形:先幂等 INSERT IGNORE 建盒(next_seq=1,首条/drop 后重建同臂)再 FOR UPDATE——
162
- // 「无行 FOR UPDATE + INSERT」的并发死锁/dup 撞车形整个消掉;seq 分配与 append 同事务(定谳 2)。
132
+ const hopChain = encodeHopChain(msg.hopChain);
163
133
  return this.tx(async (c) => {
164
134
  await c.query(`INSERT IGNORE INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES (?, ?, ?, 1)`, [scope, handle, scope]);
165
135
  let [rows] = (await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
166
136
  if (rows.length === 0) {
167
- // ensure 与锁之间盒被并发 drop/reap 提交(复审 #5 窗)——同事务重建一次(语义 = drop 后重建,
168
- // seq 从 1;同事务内自插行必可见,再空 = 真障碍 fail-loud)。
169
137
  await c.query(`INSERT IGNORE INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES (?, ?, ?, 1)`, [scope, handle, scope]);
170
138
  [rows] = (await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
171
139
  }
@@ -181,35 +149,24 @@ export class TiDBMailboxStore {
181
149
  if (boxes.length === 0)
182
150
  return null;
183
151
  const box = boxes[0];
184
- // 定谳 4:严格 expiresAt > now 才挡;定谳 1:同 owner 恒续期(字节等价比对——VARBINARY 读回 Buffer)。
185
- // 残余记账(F7 TiDB 臂):owner 含 lone surrogate 时 mysql2 编码层塌缩 U+FFFD,读回串 ≠ 原参数
186
- // → 同主续期被误拒(fail-closed 向,无越权);owner 是内部 mint 的 worker id(ASCII 域),接受。
187
152
  if (box.lease_owner !== null &&
188
153
  Number(box.lease_expires_at_ms) > now &&
189
154
  bufToStr(box.lease_owner) !== owner) {
190
155
  return null;
191
156
  }
192
- // FOR UPDATE = 当前读:TiDB 悲观事务的普通 SELECT 按 start_ts 快照(task-list twin 同案)——
193
- // 锁到 box 行后消息集必须读「现在」,否则 drop+重建窗内老消息混进新盒 lease(maxSeq 错位)。
194
157
  const [msgs] = (await c.query(`SELECT seq, from_id, content, sent_at_ms, hop_chain FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? ORDER BY seq FOR UPDATE`, [scope, handle]));
195
158
  if (msgs.length === 0)
196
- return null; // 定谳 5:空盒 claim = null(box 行在也 null)
159
+ return null;
197
160
  const maxSeq = Number(msgs[msgs.length - 1].seq);
198
161
  await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = ?, lease_expires_at_ms = ?, lease_max_seq = ? WHERE scope_key = ? AND handle = ?`, [owner, now + ttlMs, maxSeq, scope, handle]);
199
162
  return { messages: msgs.map(rowToMessage), maxSeq };
200
163
  });
201
164
  }
202
165
  async ack(scope, handle, owner, upToSeq) {
203
- // MB1/core 1.452 BREAKING:ack 带 **owner 围栏**(core InMemory 同语义:lease 缺席或 owner 不符 ⇒
204
- // 整个 ack no-op,一条消息都不删——原「定谳 3 无守卫,信任 claim 赢者链」被 [1937] MB1 推翻:
205
- // 任何人可删别人租约内消息)。锁序仍=盒行先(复审 F3 ABBA 死锁形);盒不存在=SELECT 空 ⇒ no-op。
206
166
  await this.tx(async (c) => {
207
167
  const [rows] = (await c.query(`SELECT lease_owner FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
208
- // 🔴 真双库抓的坑(2026-07-28,mock 无法抓):lease_owner 是 VARBINARY,mysql2 读回 **Buffer**——
209
- // `Buffer !== string` 恒真 ⇒ 首版围栏把正主也拒了(所有 ack 恒 no-op)。bufToStr 字节等价比对,
210
- // claim 的定谳 1 同坑同解(:186)。
211
168
  if (rows.length === 0 || rows[0].lease_owner == null || bufToStr(rows[0].lease_owner) !== owner)
212
- return; // 围栏:非当前持租者 ⇒ no-op
169
+ return;
213
170
  await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? AND seq <= ?`, [scope, handle, upToSeq]);
214
171
  await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL
215
172
  WHERE scope_key = ? AND handle = ? AND lease_max_seq IS NOT NULL AND lease_max_seq <= ?`, [scope, handle, upToSeq]);
@@ -224,8 +181,6 @@ export class TiDBMailboxStore {
224
181
  return Number(rows[0].n);
225
182
  }
226
183
  async drop(scope, handle) {
227
- // 整盒删含 box 行 = seq 生命周期终结(定谳 2;重建从 1)。**盒行先删**(锁序统一 + F2:反序在
228
- // 「消息已删、盒未删」窗放并发 append 穿过 → 盒重建撞孤儿消息行 PK,append 从此永败)。
229
184
  await this.tx(async (c) => {
230
185
  await c.query(`DELETE FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, handle]);
231
186
  await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, handle]);
@@ -235,9 +190,6 @@ export class TiDBMailboxStore {
235
190
  if (opts?.maxAgeMs === undefined)
236
191
  return 0;
237
192
  const cutoff = now - opts.maxAgeMs;
238
- // 全量真扫(定谳 6):事务外粗筛(索引面)+ 事务内逐盒「锁盒行 → 当前读重验 newest」(复审
239
- // F2/F4:粗筛与删除之间窗内新到的消息必须救活盒,绝不连带删信;GROUP BY 不能 FOR UPDATE,
240
- // 重验用 ORDER BY … LIMIT 1 当前读)。空盒不删(newest 无行);盒行已被并发 drop = 跳过。
241
193
  const [cands] = (await this.pool.query(`SELECT handle FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? GROUP BY handle HAVING MAX(sent_at_ms) < ?`, [scope, cutoff]));
242
194
  if (cands.length === 0)
243
195
  return 0;
@@ -280,7 +232,6 @@ export class PgMailboxStore {
280
232
  c.release();
281
233
  }
282
234
  }
283
- /** 身份键拒绝式([1439] 协议纪律):scope/handle/owner/from 清洗形变 = 路由/attribution 错位。 */
284
235
  assertIdentity(what, v) {
285
236
  if (v !== undefined && pgHasUnstorable(v))
286
237
  throw new PgUnstorableError(`mailbox ${what}`);
@@ -290,11 +241,10 @@ export class PgMailboxStore {
290
241
  this.assertIdentity("scope", scope);
291
242
  this.assertIdentity("handle", handle);
292
243
  this.assertIdentity("from", msg.from);
293
- const content = pgSanitizeText(msg.content); // 内容面 lossy(U+FFFD 标记,与 PG 转录 payload 同族)
244
+ const content = pgSanitizeText(msg.content);
294
245
  assertKeyBytes("scope", scope);
295
246
  assertKeyBytes("handle", handle);
296
- const hopChain = encodeHopChain(msg.hopChain); // TiDB 同注:事务外先编码,超界 fail-loud 不留半个盒
297
- // ensure-first 形(TiDB 同注):幂等建盒再 FOR UPDATE,死锁/dup 撞车形消。
247
+ const hopChain = encodeHopChain(msg.hopChain);
298
248
  return this.tx(async (c) => {
299
249
  await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES ($1, $2, $3, 1) ON CONFLICT (scope_key, handle) DO NOTHING`, [
300
250
  scope,
@@ -303,7 +253,6 @@ export class PgMailboxStore {
303
253
  ]);
304
254
  let { rows } = await c.query(`SELECT next_seq FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2 FOR UPDATE`, [scope, handle]);
305
255
  if (rows.length === 0) {
306
- // ensure 与锁之间盒被并发 drop/reap 提交(TiDB 同注)——同事务重建一次。
307
256
  await c.query(`INSERT INTO ${MAILBOX_TABLE} (scope_key, handle, scope, next_seq) VALUES ($1, $2, $3, 1) ON CONFLICT (scope_key, handle) DO NOTHING`, [
308
257
  scope,
309
258
  handle,
@@ -318,7 +267,7 @@ export class PgMailboxStore {
318
267
  });
319
268
  }
320
269
  async claimLease(scope, handle, owner, ttlMs, now = Date.now()) {
321
- this.assertIdentity("owner", owner); // 复审 F7:lease 排他键同为身份键,裸 22021/塌缩比对都不许
270
+ this.assertIdentity("owner", owner);
322
271
  return this.tx(async (c) => {
323
272
  const { rows: boxes } = await c.query(`SELECT lease_owner, lease_expires_at_ms FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2 FOR UPDATE`, [scope, handle]);
324
273
  if (boxes.length === 0)
@@ -335,32 +284,27 @@ export class PgMailboxStore {
335
284
  });
336
285
  }
337
286
  async ack(scope, handle, owner, upToSeq) {
338
- // MB1 owner 围栏(TiDB 孪生同注);锁序=盒行先(F3 ABBA 死锁形消)。
339
287
  await this.tx(async (c) => {
340
288
  const r = (await c.query(`SELECT lease_owner FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2 FOR UPDATE`, [scope, handle]));
341
- // PG 列是 VARCHAR COLLATE "C"(读回 string,无 TiDB 臂的 Buffer 坑),bufToStr 幂等包一层求同形。
342
289
  if (r.rows.length === 0 || r.rows[0].lease_owner == null || bufToStr(r.rows[0].lease_owner) !== owner)
343
- return; // 围栏:非当前持租者 ⇒ no-op
290
+ return;
344
291
  await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 AND seq <= $3`, [scope, handle, upToSeq]);
345
292
  await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL
346
293
  WHERE scope_key = $1 AND handle = $2 AND lease_max_seq IS NOT NULL AND lease_max_seq <= $3`, [scope, handle, upToSeq]);
347
294
  });
348
295
  }
349
296
  async releaseLease(scope, handle, owner) {
350
- this.assertIdentity("owner", owner); // F7 同注
297
+ this.assertIdentity("owner", owner);
351
298
  await this.pool.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL
352
299
  WHERE scope_key = $1 AND handle = $2 AND lease_owner = $3`, [scope, handle, owner]);
353
300
  }
354
301
  async peekCount(scope, handle) {
355
- // D4(试剂盒揪出):unstorable 身份键在**读面降级**返回 0(该键在写入面就被拒 ⇒ 盒必空),
356
- // 而不是把驱动层 22021 裸抛给调用方——与 tool-result 读面「degrades instead」同精神。
357
302
  if (pgHasUnstorable(scope) || pgHasUnstorable(handle))
358
303
  return 0;
359
304
  const { rows } = await this.pool.query(`SELECT COUNT(*) AS n FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, handle]);
360
305
  return Number(rows[0].n);
361
306
  }
362
307
  async drop(scope, handle) {
363
- // 盒行先删(TiDB 同注,F2 孤儿消息行窗关死)。
364
308
  await this.tx(async (c) => {
365
309
  await c.query(`DELETE FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, handle]);
366
310
  await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, handle]);
@@ -370,7 +314,6 @@ export class PgMailboxStore {
370
314
  if (opts?.maxAgeMs === undefined)
371
315
  return 0;
372
316
  const cutoff = now - opts.maxAgeMs;
373
- // 事务外粗筛 + 事务内逐盒锁定重验(TiDB 同注,F2/F4:窗内新信救活盒,不丢信)。
374
317
  const { rows: cands } = await this.pool.query(`SELECT handle FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 GROUP BY handle HAVING MAX(sent_at_ms) < $2`, [scope, cutoff]);
375
318
  if (cands.length === 0)
376
319
  return 0;
@@ -1,133 +1,24 @@
1
- /**
2
- * design/234 —— embedder **指纹门**(清空重建形,clay 终裁 [3647]⑤ 方向;稿=sema-internal
3
- * `server/design/design-234-embedder-fingerprint-20260812.md` v1+v2)。
4
- *
5
- * ## 病灶
6
- *
7
- * 向量行落在 `agent_memory_engine_entry.embedding`,**行上没有任何「产自哪个 embedder」的记账**。既有
8
- * 防线只有 embedder 的维度门(长度≠`MEMORY_EMBEDDER_DIM` ⇒ 抛),它只拦「换模型且维度恰好不同」;
9
- * **同维度换模型**(768 家族之间、1024 家族之间迁移=最常见形)完全静默——旧空间与新空间的向量在同一
10
- * 列里做 cosine,检索排序变噪音,没有任何一层会说出来。
11
- *
12
- * ## 形
13
- *
14
- * 指纹 = identity 两元组 `{ model, dimensions }`,**明文**存(不 hash:诊断可读性 > 紧凑,日志要能直接
15
- * 念出「从 X 换到了 Y」)。**endpoint 不进指纹**——换供应商域名/代理/端口不换语义空间,进了会造成大量
16
- * 误清;残余成文:同名模型在不同供应商若是不同实现(自托管 finetune 撞名)指纹辨不出,operator 明知
17
- * 异实现时改 `MEMORY_EMBEDDER_MODEL` 名或手动清列(USAGE 有一行 SQL)。
18
- *
19
- * 存储位 = **新单行元表**(additive,零删库重建窗)。不给主表加列:主表改形=触发本仓「无 ALTER、删库
20
- * 重建」BREAKING 窗;行级记账在「清空重建」语义下也无增益(全量清 ⇒ 不存在混合态)。
21
- *
22
- * ## 属主边界
23
- *
24
- * `memory-engine-pg.ts` 是 core 移入件(minimal-diff move-in 纪律)⇒ 元表 DDL + 比对清空腿全在**本
25
- * 文件**(server 侧),移入文件零改动;表名常量本文件自有(`PG_MEMORY_ENGINE_TABLES` 不动)。
26
- * 列名 `meta_key`/`meta_value`:`key` 是 MySQL 保留字,将来若补 TiDB 腿零反引号坑(K2)。
27
- *
28
- * ## 六态(v2 总表)
29
- *
30
- * | 态 | 判定 | 动作 |
31
- * |---|---|---|
32
- * | A | embedder 配置缺席 | **门不装**(连 ensure 都不调),元表不动——装配点的事,不在本文件 |
33
- * | B | 元表有行、identity 逐键相等 | 照常,**零写** |
34
- * | C | 元表有行、不等 | 清列 → upsert 新 identity(`reason:"changed"`) |
35
- * | D | 元表无行、库内**有**非 NULL 向量 | 同 C 清(存量库无法证明同源=保守清,`reason:"unattributed"`) |
36
- * | E | 元表无行、零向量行 | 只写 identity,不清(`memory_embedder_identity_armed`) |
37
- * | F | 元表有行但 value 解析失败 | 走清空臂(`reason:"unreadable"`)——**绝不当成匹配**(边界必 schema) |
38
- *
39
- * (F 的判据是 `model`/`dimensions` 缺失或类型不对;**多出来的未知键不算坏**,见 schema 处的注。)
40
- *
41
- * ## 三条不可动的判据
42
- *
43
- * 1. **次序:先清列、后写元表**。反序若中间崩溃 = 新 identity 已记而旧向量还在 = 正是被修的病(此后
44
- * 每次 boot 恒判 B,残留态永不复检);正序中间崩溃 = 下次 boot 无元表行、向量已 NULL ⇒ E 态补记,
45
- * 幂等安全。
46
- * 2. **CAS 循环**(v2 C1-2):元表写后**重读**,读回≠自写 ⇒ 另一副本并发写了**不同** identity(部署
47
- * 配置漂移)⇒ 响亮 `memory_embedder_identity_conflict`(两值并示)+ 重跑比对,上限
48
- * {@link CAS_MAX_ROUNDS} 轮,超限抛(拒启)。运行期旧副本混写非 CAS 可拦 ⇒ 归停机纪律(USAGE)。
49
- * 3. **门自身失败 = 抛(拒启)**。吞掉 = 元表停留旧值 = 下次 boot 再清一次,把一次性代价变成每次 boot
50
- * 的代价;且「门在场但没跑成」静默 = fail-open。
51
- *
52
- * ## 两条已知残余(codex 对抗复审 R1-F1/R1-F3,均判为真;处置=成文,不落码面)
53
- *
54
- * - **跨门回滚**(F1):指纹门**之前**的版本(7.14.x/7.15.x:有 embedder、无本门)照写 `embedding` 列却
55
- * 不认识元表 ⇒「装本版(元表=B)→ 回滚旧版跑一段(写 A 空间向量)→ 升回本版仍配 B」走完,元表与配置
56
- * 都说 B 而列里是 A 空间 ⇒ 此后恒判 B 态。**与滚动窗是同一族**(第二写者不维护指纹),码面根治只有
57
- * 行级记账列 —— 稿已列出界(触主表删库重建窗)。纪律面处置与滚动窗同:回滚前后手动清列,或删掉
58
- * 元表那一行让下次 boot 按 D 态保守清。USAGE 已成文。
59
- * - **崩溃丢审计行**(F3):「清过就必响亮」以**进程活到 emitOutcome** 为界。清列与写元表是两条各自
60
- * autocommit 的语句,日志在回读之后才发 ⇒ 清完即崩 ⇒ 下次 boot 元表无行、向量已 NULL ⇒ 走 E 态报
61
- * 「没有向量可清」;写完元表即崩 ⇒ 下次 boot 判 B 态、**一条日志都不发**。**数据面安全**(次序钉买的
62
- * 正是这个:绝不残留旧空间),丢的只有那一次的 cleared 计数。刻意**不做**两阶段 pending marker ——
63
- * 为纯审计价值给 boot 路加一张表 + 两阶段协议,代价与收益不成比例;承诺句在 USAGE 已标出这条边界。
64
- *
65
- * 计数取法:`PgQueryResult` 刻意无 `rowCount` ⇒ 用 CTE `WITH c AS (UPDATE … RETURNING 1) SELECT
66
- * count(*)::int`(禁 `RETURNING id` 全表拉回)。计数**按整次调用累计**、原因锚在第一次真清的那一轮 ——
67
- * 「清过就必响亮」对终局判 B 的那一支同样成立(codex R2 finding 2)。
68
- *
69
- * ## 射程外(稿 §出界)
70
- *
71
- * 主动回填件(清空后不回填,行被 patch 时自然重嵌)/ 行级 embedder 记账列(结构性根治滚动窗)/
72
- * pgvector 接线件的列 dim 校验 / TiDB 向量面(v1 lexical only)。
73
- *
74
- * 🔴 **不回填的真代价(#234 定界车 2026-08-14 实测更正,稿 §4/§V3 说轻了一档)**:稿把代价写成「那些行
75
- * 长期停留 lexical 档」,读起来像**排序退化**;真码是**可达性悬崖**——`memory-engine-pg.ts` 的进程内腿
76
- * 对无向量行落 `jaccardDistance`,core 的该函数在**零词交集**时返回 `null`,`search` 当场 `continue`,
77
- * 行被**丢出结果集**;仍有向量的行则走余弦、不被这道门丢(前提是查询侧也嵌得出来——embedder 故障那一
78
- * 刻查询拿不到向量,整轮退词面档,零交集的行一起丢)。即清空之后、重嵌之前,那些行对改述/同义/跨
79
- * 语言查询**检索不到**——而那恰恰是向量档存在的理由。真库端到端实证:db-integration 套件「清空后的
80
- * 代价」一钉(清空前 "zulu" 命中该行,清空后同查询恒空,有字面交集的查询仍命中)。
81
- * ⚠️ 这条只更正**成文口径**,不改裁定:主动回填仍在射程外(clay 已否);但将来复裁该件时,输入应当是
82
- * 「可达性悬崖」而不是「排序退化」。
83
- */
84
1
  import { z } from "zod";
85
2
  import { PG_MEMORY_ENGINE_TABLES } from "./memory-engine-pg.js";
86
- /** 元表名(本文件自有 const —— core 移入文件的 `PG_MEMORY_ENGINE_TABLES` 零改动)。 */
87
3
  export const PG_MEMORY_ENGINE_META_TABLE = "agent_memory_engine_meta";
88
- /** 单行元表里 embedder 指纹那一行的键。 */
89
4
  export const EMBEDDER_IDENTITY_META_KEY = "embedder_identity";
90
- /** CAS 循环上限(v2 C1-2)。超限 = 并发副本带着**不同** identity 在互相覆盖 ⇒ 拒启。 */
91
5
  export const CAS_MAX_ROUNDS = 3;
92
- /**
93
- * 指纹 schema —— 读回的 `meta_value` 必须过它(手改坏值/回滚残留一律走 F 态,禁 `JSON.parse as` 裸断言)。
94
- *
95
- * ⚠️ **未知键刻意放过**(zod 对象默认 strip,不加 `.strict()`):将来某个版本若往这行里多写一个键,
96
- * 老副本读到它应当照常按 `model`/`dimensions` 比对,而不是判 F 态把整列清掉 —— 「新副本加了个字段」
97
- * 与「这行坏了」不是一回事,后者才是 F 态要抓的。载荷只有这两个键,多出来的不参与判定。
98
- */
99
6
  const EmbedderIdentitySchema = z.object({
100
7
  model: z.string().min(1),
101
8
  dimensions: z.number().int().positive(),
102
9
  });
103
- /**
104
- * 元表 DDL —— **只发 CREATE**(K1:混入 SELECT/UPDATE 会让 `scripts/dump-schema.ts` 的录制门当场抛)。
105
- * 幂等,与其余 `ensurePg*Schema` 同姿势;新腿已注册 `PG_LEGS` + `docs/schema/baseline-pg.sql`。
106
- */
107
10
  export async function ensurePgMemoryEmbedderMetaSchema(query) {
108
11
  await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_META_TABLE} (
109
12
  meta_key varchar(190) COLLATE "C" PRIMARY KEY,
110
13
  meta_value jsonb NOT NULL
111
14
  )`);
112
15
  }
113
- /**
114
- * 元表值 → 三态。入参是 `meta_value::text`(**JSON 文本**),**只解析一次**。
115
- *
116
- * 🔴 为什么读 `::text` 而不是读列本身(codex 对抗复审 R2 finding 1,验真为真):node-pg 已经替我们
117
- * 解析过 jsonb,所以列里躺着一个 JSON **字符串**时,交到手上的就是一个 JS string —— 上一版对 string
118
- * 再 `JSON.parse` 一次「宽容一格」,于是**双重编码**的坏值(`'"{\"model\":…}"'::jsonb`)会被解回合法
119
- * 对象、判成「相等」,来源不明的旧向量原地存活 = 本模块要消灭的静默混空间换了个入口。读 `::text` 让
120
- * 「对象」与「驱动序列化出的字符串」不再有歧义:解析一次之后**必须是普通对象**,真正的 JSON 字符串
121
- * (以及数组/数字/null)一律落 F 态。顺带也不再依赖任何驱动的 jsonb 解析姿势。
122
- */
123
16
  function parseStoredText(text) {
124
17
  let candidate;
125
18
  try {
126
19
  candidate = JSON.parse(text);
127
20
  }
128
21
  catch (err) {
129
- // 解析错**当数据交出去**(不静默折成一句「读不出来」):这段文字会随 `reason:"unreadable"` 进
130
- // 那条响亮日志,排障时「元表里躺的到底是什么」是第一个要问的。
131
22
  return { kind: "unreadable", raw: `${text.slice(0, 160)} [JSON.parse: ${String(err).slice(0, 80)}]` };
132
23
  }
133
24
  if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
@@ -144,27 +35,19 @@ async function readStored(query) {
144
35
  if (row === undefined)
145
36
  return { kind: "absent" };
146
37
  const text = row.meta_value_text;
147
- // 列是 NOT NULL 且 `::text` 恒给文本 —— 拿不到文本 = 这条读腿被换过(池包装/驱动改形),响亮落 F,
148
- // 不猜。
149
38
  if (typeof text !== "string")
150
39
  return { kind: "unreadable", raw: `[meta_value::text did not return text: ${typeof text}]` };
151
40
  return parseStoredText(text);
152
41
  }
153
- /** D/E 的判据:库里还有没有**没主**的向量(元表无行 ⇒ 存量向量无从证明同源)。`LIMIT 1` —— 只问存在性,不拉全表。 */
154
42
  async function hasUnattributedVectors(query) {
155
43
  const r = await query(`SELECT 1 FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE embedding IS NOT NULL LIMIT 1`);
156
44
  return r.rows.length > 0;
157
45
  }
158
- /**
159
- * 清列 + 计数。`PgQueryResult` 无 `rowCount` ⇒ CTE 把受影响行数在 SQL 侧数完再回一行
160
- * (`RETURNING id` 会把全表主键拉回进程,量级白烧)。
161
- */
162
46
  async function clearEmbeddings(query) {
163
47
  const r = await query(`WITH c AS (UPDATE ${PG_MEMORY_ENGINE_TABLES.entry} SET embedding = NULL WHERE embedding IS NOT NULL RETURNING 1)
164
48
  SELECT count(*)::int AS n FROM c`);
165
49
  return Number(r.rows[0]?.n ?? 0);
166
50
  }
167
- /** 三臂统一 upsert(C2:并发首启两副本同时 INSERT 不撞 PK)。 */
168
51
  async function writeIdentity(query, identity) {
169
52
  await query(`INSERT INTO ${PG_MEMORY_ENGINE_META_TABLE} (meta_key, meta_value) VALUES ($1, $2::jsonb)
170
53
  ON CONFLICT (meta_key) DO UPDATE SET meta_value = EXCLUDED.meta_value`, [EMBEDDER_IDENTITY_META_KEY, JSON.stringify(identity)]);
@@ -172,33 +55,14 @@ async function writeIdentity(query, identity) {
172
55
  function sameIdentity(a, b) {
173
56
  return a.model === b.model && a.dimensions === b.dimensions;
174
57
  }
175
- /** 换模型这件事的运维纪律(日志与 USAGE 同句;滚动窗混写是纪律面残余,门管 boot 面)。 */
176
58
  const SCALE_ZERO_NOTE = "change the embedder identity with the fleet stopped (scale to 0, then start): during a ROLLING window the old replicas keep writing vectors in the OLD space after this gate cleared the column";
177
- /**
178
- * 指纹比对腿 —— boot 一次(env 族是 boot 快照,运行中不热换)。**门自身失败一律抛**(拒启)。
179
- *
180
- * ⚠️ 调用姿势:`ensurePgMemoryEmbedderMetaSchema` 之后、memory backend 构造之前;embedder 配置**在场**
181
- * 才调(A 态连 ensure 都不调)。
182
- */
183
59
  export async function reconcileEmbedderFingerprint(query, identity, opts = {}) {
184
60
  const { log } = opts;
185
61
  const now = opts.now ?? Date.now;
186
- // 自家入参也过 schema:装配点喂的是 config 解析结果(已校验),但这条腿也被脚本/测试直调,
187
- // 坏 identity 写进元表 = 下次 boot 恒判 F 态清全库。
188
62
  const want = EmbedderIdentitySchema.parse(identity);
189
63
  const firstStartedAt = now();
190
- // 🔴 跨轮累计(codex 对抗复审 R2 finding 2,验真为真):清空计数/原因/旧值**每轮重置**时,
191
- // 「round1 清了 N 行、回读撞冲突、round2 只能再清到 0」会把一次整列缓存失效报成 `cleared=0`;更坏的
192
- // 一支是 round2 恰好读回我方 identity ⇒ 从 match 臂直接返回、**一条日志都不发**,破坏性动作在运维
193
- // 记录里彻底消失(与「任何清空动作必响亮」直接冲突)。所以计数按**整次调用**累计,原因/旧值锚在
194
- // **第一次真清**的那一轮,并且只要清过就必发 `memory_embedder_identity_changed` —— 终局是 match
195
- // 也不例外。
196
64
  let clearedTotal = 0;
197
65
  let firstClear;
198
- // 🔴 **变更**本身也要跨轮记账,不能只记「清过行」(合并码重扫 codex 复审第二轮,验真为真):零向量库
199
- // 的 C/D/F 轮 `cleared` 恒 0 ⇒ 它进不了 `firstClear`;若那一轮的回读又撞上并发冲突、下一轮才收敛到
200
- // match,终局臂只看得见 `clearedTotal===0` 与 `terminalState==="match"`,于是一次**真发生过的** identity
201
- // 变更(元表已被我方改写)在日志里彻底消失 —— 与「清过必响亮」同族的漏报,只是漏的是零清空那一支。
202
66
  let firstChange;
203
67
  const emitOutcome = (terminalState, rounds) => {
204
68
  const elapsedMs = now() - firstStartedAt;
@@ -207,30 +71,19 @@ export async function reconcileEmbedderFingerprint(query, identity, opts = {}) {
207
71
  previous: firstClear?.previous ?? firstChange?.previous ?? null,
208
72
  next: want,
209
73
  cleared: clearedTotal,
210
- reason: firstClear?.reason ?? firstChange?.reason ?? terminalState, // "changed" | "unattributed" | "unreadable"
74
+ reason: firstClear?.reason ?? firstChange?.reason ?? terminalState,
211
75
  rounds,
212
76
  elapsedMs,
213
- // 🔴 代价按真码说(#234 定界车实测,真库端到端钉在 db-integration 的「清空后的代价」):
214
- // 「退回 lexical」是**说轻了**——词面档对与查询**零词交集**的行返回「无定义」,检索当场把该行
215
- // 跳过,于是清空之后、重嵌之前,那些行对改述/同义/跨语言查询是**不可达**的(带向量的行则恒有
216
- // 余弦读数、永不被丢)。operator 看见这条日志的那一刻就该知道丢的是可达性,不是名次。
217
77
  note: `${SCALE_ZERO_NOTE}; cleared vectors are NOT backfilled — rows re-embed only as they are next written, and until then each cleared row is reachable ONLY by a query sharing a literal term with it (the lexical floor drops a row with zero term overlap, whereas a row that still has a vector is scored by cosine as long as the query embeds too) — rewrite the corpus to restore vector reach immediately`,
218
78
  });
219
79
  return;
220
80
  }
221
- // 🔴 **identity 真换了但没有向量可清,同样响亮**(合并码重扫,验真后修)。旧判据只有「清过行」与
222
- // 「armed」两支,于是 C 态(不等)/ D 态 / F 态(元表行读不出来)碰上 `embedding` 列恰好全 NULL 时,
223
- // 元表被静默改写、一条日志都不发 —— 可达形:上一次 boot 已清空过、期间无新语料;或回滚残留的坏元表
224
- // 行 + 空向量列(恰恰是最该留痕的一形)。USAGE.md 把三个 reason 并列写进同一句承诺里,没有对
225
- // 「清了 0 行」开例外 ⇒ 换 embedder 这件事在启动日志里不许无痕。`match` 仍是零写零响亮(B 态)。
226
- // 判据是**本次调用里有没有真发生过 identity 变更**(`firstChange`),不是本轮的 `terminalState` ——
227
- // 后者在「首轮 changed(零清空)→ CAS 冲突 → 次轮 match」这条路上会把变更说成没发生过。
228
81
  if (firstChange !== undefined) {
229
82
  log?.info("memory_embedder_identity_changed", {
230
83
  previous: firstChange.previous,
231
84
  next: want,
232
85
  cleared: 0,
233
- reason: firstChange.reason, // "changed" | "unattributed" | "unreadable"
86
+ reason: firstChange.reason,
234
87
  rounds,
235
88
  elapsedMs,
236
89
  note: `${SCALE_ZERO_NOTE}; no vectors existed to clear on this boot — the identity itself changed, which is the event`,
@@ -245,38 +98,26 @@ export async function reconcileEmbedderFingerprint(query, identity, opts = {}) {
245
98
  elapsedMs,
246
99
  });
247
100
  }
248
- // B(且从未清过)= 零写零响亮:换模型才是事件。
249
101
  };
250
102
  for (let round = 1; round <= CAS_MAX_ROUNDS; round++) {
251
103
  const stored = await readStored(query);
252
- // ── 判定 ────────────────────────────────────────────────────────────────────────────────────
253
104
  let state;
254
105
  let previous = null;
255
106
  if (stored.kind === "ok") {
256
107
  previous = stored.value;
257
108
  if (sameIdentity(stored.value, want)) {
258
- // B:零写(连 upsert 都不发 —— 「匹配」这条路径上不许有任何写)。但前序轮若已经清过,
259
- // 那次清必须照样被记上、被说出来。
260
109
  emitOutcome("match", round);
261
110
  return { state: "match", cleared: clearedTotal, previous, identity: want, rounds: round };
262
111
  }
263
- state = "changed"; // C
112
+ state = "changed";
264
113
  }
265
114
  else if (stored.kind === "unreadable") {
266
- state = "unreadable"; // F —— 绝不当成匹配
115
+ state = "unreadable";
267
116
  }
268
117
  else {
269
- state = (await hasUnattributedVectors(query)) ? "unattributed" : "armed"; // D / E
118
+ state = (await hasUnattributedVectors(query)) ? "unattributed" : "armed";
270
119
  }
271
- // ── 动作:🔴 先清列、后写元表(次序即正确性,见头注 1)────────────────────────────────────
272
120
  if (state !== "armed" && firstChange === undefined) {
273
- // 变更的锚:第一次判出「不是 match、不是 armed」的那一轮(与清了几行无关)。
274
- // ⚠️ 如实登记的残余(codex 复审第三轮提出,判为**过报**而非误报,故不改 SQL 语义):两只**同配置**
275
- // 副本同时首启时,两边都可能先读到旧值 ⇒ 各自 clear+upsert(赢家清到行,输家清 0)⇒ 两边各发一条
276
- // `memory_embedder_identity_changed`(同一次逻辑变更在两个进程的日志里各留一条)。方向是安全的那
277
- // 一侧:多一条过报 < 漏一条(本键的全部理由是「换 embedder 不许无痕」),且两条都如实描述了各自
278
- // 观察到的世界(旧值 → 新值)。根治要把 upsert 换成「只在值真变时才写并回带是否变过」的条件形 ——
279
- // 那是元表写腿的 SQL 语义变更(双库门),不在本收口批射程,登记为后续件。
280
121
  firstChange = { reason: state, previous: previous ?? (stored.kind === "unreadable" ? { unreadable: stored.raw } : null) };
281
122
  }
282
123
  const cleared = state === "armed" ? 0 : await clearEmbeddings(query);
@@ -285,13 +126,11 @@ export async function reconcileEmbedderFingerprint(query, identity, opts = {}) {
285
126
  }
286
127
  clearedTotal += cleared;
287
128
  await writeIdentity(query, want);
288
- // ── CAS 回读(见头注 2)────────────────────────────────────────────────────────────────────
289
129
  const after = await readStored(query);
290
130
  if (after.kind === "ok" && sameIdentity(after.value, want)) {
291
131
  emitOutcome(state, round);
292
132
  return { state, cleared: clearedTotal, previous, identity: want, rounds: round };
293
133
  }
294
- // 读回≠自写 ⇒ 另一副本并发写了**不同** identity(部署配置漂移)。响亮 + 重跑比对。
295
134
  log?.error("memory_embedder_identity_conflict", {
296
135
  ours: want,
297
136
  theirs: after.kind === "ok" ? after.value : after.kind === "unreadable" ? { unreadable: after.raw } : null,