@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,71 +1,23 @@
1
- /**
2
- * #228 —— OpenAI 兼容的 embedder(黑板 [3590] 裁②:core 只留了 `PgEmbedder` 这个**纯接口**
3
- * (`{ embed(text) → number[]; dimensions }`),没有任何现成实现件;部署半场归本仓)。
4
- *
5
- * ## 形
6
- *
7
- * `POST {endpoint}/embeddings`,body `{ model, input }`,读 `data[0].embedding` —— OpenAI /
8
- * DashScope / TEI / vllm / ollama 的 `/v1` 面同形。auth 可选(自托管端点通常没有 key)。
9
- *
10
- * ## 为什么校验做得这么硬
11
- *
12
- * 向量列写错维度是**静默毒库**形([3606] 裁③):错长度向量既不会让 DB 报错(本仓 pg 记忆表的
13
- * embedding 列是 `jsonb`,无维度约束),也不会让检索报错(维度不匹配的行只是悄悄退回词面档)——
14
- * 于是「向量面开着」这句话变成一句谎,而且没有任何一层会说出来。所以:
15
- * · 响应形用 zod 校验(`data[0].embedding` 必须是 `number[]`),不符 = 抛带上下文的错;
16
- * · 长度与声明维度不等 = 抛,**绝不截断 / 补零**(那正是把毒喂进库的两种手法);
17
- * · 非 2xx / 非 JSON = 抛(status + body 前 200 字节,memory-sync transport 同款诊断姿势)。
18
- *
19
- * 抛出去之后由调用方处置:`PgMemoryEngineBackend.embeddingParam` 的长度门会让该行退回词面档,
20
- * `search` 的词面腿 `.catch(() => null)` 会让检索继续跑 —— 即「embedder 坏了 = 退回 lexical」,
21
- * 而不是「embedder 坏了 = 写坏向量」。
22
- *
23
- * ## 超时
24
- *
25
- * `AbortSignal.timeout(timeoutMs)`(缺省 30s,`MEMORY_EMBEDDER_TIMEOUT_MS` 覆盖,越界/坏值拒启)。
26
- * 记忆写入路径是同步等 embed 的,挂死的端点会把整条 harvest 吊住(undici 默认 headers 超时 300s 太钝)。
27
- */
28
1
  import { z } from "zod";
29
- /** OpenAI `/v1/embeddings` 响应的**最小**契约:只读第一条向量,其余键(usage/object/model)不关心。 */
30
2
  const EmbeddingsResponse = z.object({
31
3
  data: z.array(z.object({ embedding: z.array(z.number()) })).min(1),
32
4
  });
33
- /**
34
- * 端点归一:`MEMORY_EMBEDDER_ENDPOINT` 收两种写法 —— OpenAI 兼容**基址**(`https://x/v1`)或**完整**
35
- * 端点(`https://x/v1/embeddings`)。两种都是 operator 手上真实存在的抄法,而一律硬拼 `/embeddings`
36
- * 会产出 `/v1/embeddings/embeddings`(这条键最常见的手滑形)。归一不是降级:两条路都指向同一个真 URL,
37
- * 没有任何一种输入被静默改成**别的**语义。
38
- *
39
- * ⚠️ 只动 `pathname`(codex 复审 F3,已核真):第一版在**整串**上做 `endsWith`/拼接,于是带 query 的
40
- * 端点全错 —— Azure OpenAI 形 `https://h/v1/embeddings?api-version=1` 会被拼成 query 值里带
41
- * `1/embeddings`,`https://h/v1?api-version=1` 则永远拿不到 `/embeddings` 路径。两种都过得了启动期的
42
- * URL 合法性门,只在运行期炸(而运行期的炸法就是上面那条「写入失败/检索退档」的静默病)。
43
- */
44
5
  export function embeddingsUrl(endpoint) {
45
- const u = new URL(endpoint); // config 层已校验;直调工厂时坏 URL 在这里响亮抛
46
- u.hash = ""; // fragment 对服务端无意义,带上只会污染日志
6
+ const u = new URL(endpoint);
7
+ u.hash = "";
47
8
  const path = u.pathname.replace(/\/+$/, "");
48
- // 「已经带尾巴了吗」按**解码后**的末段判(codex 二轮 F3,已核真):`/v1/%65mbeddings` 与
49
- // `/v1/embeddings` 是同一条路径,只比对序列化文本会给前者再叠一层。只解 unreserved 转义
50
- // (RFC 3986 的 `A-Za-z0-9-._~`)—— 其余转义原样保留,decodeURIComponent 的坏 `%` 抛错面也一并避开。
51
9
  u.pathname = decodeUnreservedEscapes(path).endsWith("/embeddings") ? path : `${path}/embeddings`;
52
10
  return u.toString();
53
11
  }
54
- /** 只把 unreserved 字符的百分号转义解回来(不抛、不改变其它转义)。 */
55
12
  function decodeUnreservedEscapes(s) {
56
13
  return s.replace(/%([0-9A-Fa-f]{2})/g, (whole, hex) => {
57
14
  const c = String.fromCharCode(Number.parseInt(hex, 16));
58
15
  return /[A-Za-z0-9\-._~]/.test(c) ? c : whole;
59
16
  });
60
17
  }
61
- /**
62
- * 诊断面用的**脱敏** URL(codex 复审 F4,已核真):端点里可能带 userinfo(`https://user:pass@h/v1`)
63
- * 或把凭据放 query(Azure 的 `api-key=`),而错误文本会进日志、进 PatchReport 的 conflict reason。
64
- * 主机与路径保留(诊断价值全在这里),userinfo 抹掉、query 值一律换成 `***`(键名保留,便于认形)。
65
- */
66
18
  export function redactEndpointForLog(raw) {
67
19
  if (!URL.canParse(raw))
68
- return "<invalid-url>"; // 谓词先行,不用 catch 兜(#191 静默降级门:catch 回默认值是被数的形)
20
+ return "<invalid-url>";
69
21
  const u = new URL(raw);
70
22
  u.username = "";
71
23
  u.password = "";
@@ -73,28 +25,20 @@ export function redactEndpointForLog(raw) {
73
25
  u.searchParams.set(k, "***");
74
26
  return u.toString();
75
27
  }
76
- /** 端点 query 上的**值**(Azure 形把 key 放 query;这些字面量要从任何诊断文本里洗掉)。 */
77
28
  function credentialsInQuery(endpoint) {
78
29
  if (!URL.canParse(endpoint))
79
30
  return [];
80
31
  return [...new URL(endpoint).searchParams.values()].filter((v) => v.length > 0);
81
32
  }
82
- /** 造一个 OpenAI 兼容的 `PgEmbedder`(工厂命名律:返回带行为的对象 ⇒ `create*`)。 */
83
33
  export function createOpenAiCompatEmbedder(opts) {
84
34
  const url = embeddingsUrl(opts.endpoint);
85
35
  const shownUrl = redactEndpointForLog(url);
86
36
  const fetchImpl = opts.fetchImpl ?? fetch;
87
37
  const { model, dimensions, timeoutMs, apiKey, onFailure } = opts;
88
- // 洗白名单 = 本部署已知的**全部**凭据字面量:apiKey + 端点 query 上的值(Azure 形把 key 放 query;
89
- // codex 二轮 F1)。userinfo 在 config 层就被拒了,这里不再单列。
90
38
  const secrets = [...(apiKey !== undefined ? [apiKey] : []), ...credentialsInQuery(opts.endpoint)];
91
- /** 供应商 body / 传输层错误进诊断文本前先洗一遍:回显自家凭据的代理/网关是真实存在的形。 */
92
39
  const scrub = (raw) => secrets.reduce((acc, s) => acc.replaceAll(s, "***"), raw);
93
40
  const safeSlice = (raw) => scrub(raw).slice(0, 200);
94
41
  const call = async (text) => {
95
- // 传输层失败(DNS/连不上/超时)也要走脱敏形(codex 二轮 F1):undici 的错误链里可能带上完整 URL,
96
- // 而这条错会进 warn 日志与 PatchReport 的 conflict reason。**刻意不挂 cause**:挂上等于把没洗过的
97
- // 原始文本从后门放回诊断面(打印错误链的人一样看得见),这里只带洗过的文本。
98
42
  let res;
99
43
  try {
100
44
  res = await fetchImpl(url, {
@@ -113,8 +57,6 @@ export function createOpenAiCompatEmbedder(opts) {
113
57
  const raw = await res.text().catch((readErr) => {
114
58
  throw new Error(`memory embedder POST ${shownUrl}: response body unreadable — ${safeSlice(String(readErr))}`);
115
59
  });
116
- // 诊断面纪律:回显 status + body 前缀(端点的 typed error body 就是最好的线索),但 URL 走脱敏形、
117
- // body 先洗掉全部已知凭据字面量 —— 错误文本会进日志与 PatchReport 的 conflict reason。
118
60
  if (!res.ok)
119
61
  throw new Error(`memory embedder POST ${shownUrl}: HTTP ${res.status} ${safeSlice(raw)}`);
120
62
  let json;
@@ -141,25 +83,17 @@ export function createOpenAiCompatEmbedder(opts) {
141
83
  return await call(text);
142
84
  }
143
85
  catch (err) {
144
- // 先让部署面看见,再**原样**抛(观测腿绝不改变结果,连真错的属性都不碰)
145
86
  try {
146
87
  onFailure?.(err);
147
88
  }
148
89
  catch (hookErr) {
149
- // 观测腿自己坏了:①不许顶替真故障(拿监控的病盖住业务的病是最坏的一种交换);②不许改动真错
150
- // ——第一版往 err.cause 上挂,冻结/不可写的 err 会让**赋值本身**抛,于是 TypeError 顶替了
151
- // 真错(codex 二轮 F2,已核真);③也不许无声 —— 走 process.emitWarning,不碰真错分毫。
152
90
  process.emitWarning(`memory embedder onFailure hook threw: ${String(hookErr)}`, "MemoryEmbedderObservabilityWarning");
153
91
  }
154
- throw err; // 原对象原样抛出(identity 不变)
92
+ throw err;
155
93
  }
156
94
  },
157
95
  };
158
96
  }
159
- /**
160
- * 装配腿:配置在场 ⇒ 造 embedder,缺席 ⇒ `undefined`(= core 的三档推断落回 lexical)。
161
- * 这一层单独存在是为了让「注入真的接上了」可测 —— `openStores` 本身要真 DB 才跑得起来。
162
- */
163
97
  export function memoryEmbedderFor(config, deps = {}) {
164
98
  const cfg = config.memoryEmbedder;
165
99
  if (cfg === undefined)
@@ -1,43 +1,15 @@
1
- // ⚠️ ORIGIN: @sema-agent/core 1.267.0 `src/core/memory-engine/pg-backend.ts`(core commit 3071dd3)——
2
- // 边界重切整段移交:core 从此只养 File backend+MemoryBackend seam+memoryBackendContract
3
- // 契约套件;Pg/TiDB 方言实现归 service(数据面方言=deployment 半场,TiDBRunStore/PgRunStore 先例)。
4
- // 移交纪律:保留此头注双向可追;core 下版撤 PgMemoryEngineBackend 公开导出。
5
- // 改动 vs origin:import 面重指(types/frontmatter 从 @sema-agent/core 公开面;memory-vector 三函数+
6
- // isUniqueViolation 内联于 memory-engine-vector-util.ts(core 1.424 示例适配器退包面后回归内联,勿再切 re-export)。
7
- // design/138 S3-core — PgMemoryEngineBackend: the SECOND MemoryBackend implementation (TOB/multi-tenant
8
- // authority disk), proving the S1 entry-transactional seam holds off the file plane. Same contract as
9
- // FileMemoryEngineBackend; radically simpler internals, because Postgres retires the file backend's
10
- // control-plane machinery instead of porting it (S3 拍板, design/138 §S3-core):
11
- // - committed-rev ledger → the row's `rev` column IS the committed rev (no zero-copy problem: the
12
- // model edits file PROJECTIONS, never DB rows, so "read current rev" cannot self-conflict);
13
- // - redo journal → per-statement conditional writes (`UPDATE … WHERE rev = $expected
14
- // RETURNING`, the pg.ts checkpoint OCC template) — the DB's atomicity replaces staging/journaling;
15
- // - shadow/ quarantine/ → dropped: a DB row cannot go "missing" outside an explicit delete patch,
16
- // so the "缺失≠删除" recovery axis has no Pg counterpart; inbound writes are engine-only (a
17
- // deployment sharing the tables with other writers owns validation — same posture as stores/pg.ts);
18
- // - scope registry → dropped: `scope` is a plain column; tenant keying is the deployment's.
19
- // Vector rungs (design/81, re-mounted here per §4): lexical (jaccard, byte-equal to FileBackend) →
20
- // portable (jsonb vectors, in-process cosine) → native (pgvector `<=>` SQL). All three share the
21
- // cosine-distance contract score ∈ [0,2] — upgrading recall is a config change, not a recalibration.
22
1
  import { cosineDistance, jaccardDistance, termSet } from "@sema-agent/core";
23
2
  import { isUniqueViolation } from "./memory-engine-vector-util.js";
24
3
  import { computeEntryRev, serializeEntryFile } from "@sema-agent/core";
25
4
  import { pgSafeJsonStringify, pgHasUnstorable } from "./pg-safe-json.js";
26
- import { assertMemoryScopeWidth, memoryEntryKeyWidthRefusal } from "./memory-key-guards.js"; // R5 批γ + #271 件1:scope/slug 写前宽守卫
27
- // #307 件6(core 5.46.0 design/336 §2.3-2):external-origin 不可变律 —— 两支 SQL 孪生共用**一份**判据。
5
+ import { assertMemoryScopeWidth, memoryEntryKeyWidthRefusal } from "./memory-key-guards.js";
28
6
  import { ambiguousOriginRefusal, createBatchCommittedBaseline, exposureBandOf, exposureCarriage, originMarkedSqlPredicate, originWhitewashRefusal, rememberBatchCommitted, distilledWhitewashRefusal, distilledCarriage, unknownPatchOpRefusal, } from "./memory-origin-law.js";
29
- /** Table names (single source). Deliberately DISJOINT from the legacy `agent_memory*` tables —
30
- * the retired MemoryStore plane and this entry plane must never cross-write. */
31
7
  export const PG_MEMORY_ENGINE_TABLES = {
32
8
  entry: "agent_memory_engine_entry",
33
9
  cursor: "agent_memory_engine_cursor",
34
10
  };
35
- /** Idempotent DDL for the entry plane. Call once at startup (or run through your migration tool). */
36
11
  export async function ensurePgMemoryEngineSchema(query, opts = {}) {
37
12
  const vec = opts.memoryVector;
38
- // dimensions must be a finite positive integer — NaN/Infinity would interpolate
39
- // invalid DDL (`vector(NaN)`), and a silent fractional truncation could disagree with the embedder's
40
- // declared dimensions. Boot-time config error ⇒ throw loudly, never emit broken DDL.
41
13
  if (vec !== undefined && (!Number.isInteger(vec.dimensions) || vec.dimensions < 1)) {
42
14
  throw new Error(`ensurePgMemoryEngineSchema: memoryVector.dimensions must be a positive integer (got ${String(vec.dimensions)})`);
43
15
  }
@@ -59,27 +31,16 @@ export async function ensurePgMemoryEngineSchema(query, opts = {}) {
59
31
  await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_ENGINE_TABLES.entry}_scope ON ${PG_MEMORY_ENGINE_TABLES.entry} (scope)`);
60
32
  }
61
33
  catch {
62
- /* pg-mem: partial CREATE INDEX support — the index is a perf add-on, never correctness */
63
34
  }
64
- // The (scope, slug) UNIQUE constraint is CORRECTNESS, not perf — two
65
- // instances racing an add of the same scope+slug (different ids) could both probe "free" and both
66
- // insert, leaving two entries whose file projections overwrite each other at materialize. The DB
67
- // constraint is the only true arbiter; the add path catches the violation and retries the next
68
- // suffix. NOT wrapped in try/catch — if this constraint cannot be created, the backend is unsafe.
69
35
  await query(`CREATE UNIQUE INDEX IF NOT EXISTS uq_${PG_MEMORY_ENGINE_TABLES.entry}_scope_slug ON ${PG_MEMORY_ENGINE_TABLES.entry} (scope, slug)`);
70
36
  await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.cursor} (
71
37
  scope varchar(190) COLLATE "C" PRIMARY KEY,
72
38
  cursor text COLLATE "C" NOT NULL
73
39
  )`);
74
40
  }
75
- /** pgvector input literal — the same `[…]` string JSON.parse reads back from jsonb (one format, two columns). */
76
41
  function vectorLiteral(v) {
77
42
  return `[${v.join(",")}]`;
78
43
  }
79
- /** `$k, $k+1, …` for a dynamic IN list. Used instead of `= ANY($1)` — pg-mem (the offline test rig)
80
- * silently returns ZERO rows for `ANY(param)` on an INDEXED column (index-path bug, verified
81
- * 2026-07-11: unindexed ANY works, indexed ANY is empty, indexed `=` works). IN-lists are plain SQL
82
- * on both engines, so the production path never bends toward the emulator. */
83
44
  function inPlaceholders(first, count) {
84
45
  return Array.from({ length: count }, (_, i) => `$${first + i}`).join(", ");
85
46
  }
@@ -92,9 +53,7 @@ export class PgMemoryEngineBackend {
92
53
  pgvectorSql;
93
54
  clock;
94
55
  history;
95
- /** Cumulative lost-row count — audit-line breakage must itself be auditable. */
96
56
  historyRowsLost = 0;
97
- /** design/81 rung indicator — same semantics as PgMemoryStore.vectorMode. */
98
57
  get vectorMode() {
99
58
  return this.pgvectorSql ? "native" : this.embedder ? "portable" : "lexical";
100
59
  }
@@ -107,12 +66,9 @@ export class PgMemoryEngineBackend {
107
66
  if (this.pgvectorSql && !this.embedder)
108
67
  throw new Error("PgMemoryEngineBackend: pgvectorSql requires an embedder");
109
68
  }
110
- /** The embed/search haystack — BYTE-EQUAL to FileBackend's lexical haystack (file-backend search). */
111
69
  haystackOf(slug, fm, body) {
112
70
  return `${fm.name ?? slug} ${fm.description ?? ""} ${body}`;
113
71
  }
114
- /** Dimension-guarded embedding parameter (mirrors PgMemoryStore.embeddingParam — wrong-length ⇒ null,
115
- * the row falls open to the lexical stand-in instead of poisoning reads). */
116
72
  async embeddingParam(text) {
117
73
  if (!this.embedder)
118
74
  return null;
@@ -133,10 +89,7 @@ export class PgMemoryEngineBackend {
133
89
  mtimeMs: Number(r.mtime_ms),
134
90
  rev: r.rev,
135
91
  sizeBytes: Number(r.size_bytes),
136
- // #319(design/336 §5.2):committed external-origin 事实随**每一个**头面(listHeaders + search)。
137
- // 归一化只有一份(memory-origin-law.exposureCarriage → core 的 committedOriginOf),两支方言同源。
138
92
  ...exposureCarriage(fm),
139
- // #337(conformance c38,TiDB 孪生同注):distilled 事实(carrierRev + supersedes)随每一个头面。
140
93
  ...distilledCarriage(fm),
141
94
  };
142
95
  }
@@ -151,9 +104,6 @@ export class PgMemoryEngineBackend {
151
104
  };
152
105
  }
153
106
  async listHeaders(scopes) {
154
- // Dedupe (all three read paths): a repeated key must not repeat rows — real Postgres IN-lists
155
- // don't, but pg-mem duplicates matches per repeated placeholder value; dedupe is also the honest
156
- // semantic (a caller repeating an id asks about ONE entry).
157
107
  const uniq = [...new Set(scopes)];
158
108
  if (uniq.length === 0)
159
109
  return [];
@@ -165,52 +115,31 @@ export class PgMemoryEngineBackend {
165
115
  if (uniq.length === 0)
166
116
  return [];
167
117
  const res = await this.query(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id IN (${inPlaceholders(1, uniq.length)}) ORDER BY id`, uniq);
168
- return res.rows.map((r) => this.entryFromRow(r)); // unknown ids: silently absent
118
+ return res.rows.map((r) => this.entryFromRow(r));
169
119
  }
170
120
  async search(query, scopes, opts) {
171
- // #319(design/336 §5.2):两带序 —— 未标记先、已标记后,**带序在截断之前**(理由见
172
- // memory-origin-law.exposureBandOf 的顶注)。缺席/false ⇒ 单带序,与本方法 pre-336 形逐字节同。
173
121
  const bands = opts?.exposureBands === true;
174
- /** 契约序:(带号 ‖ 缺席时恒 0) → score 升 → id。三档共用一只比较器,谁都别自己写一份。 */
175
122
  const contractOrder = (a, b) => (bands ? exposureBandOf(a) - exposureBandOf(b) : 0) || a.score - b.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
176
- // Normalize the limit ONCE for all three rungs — `Math.floor`
177
- // makes a fractional limit (0.5) behave like File's `slice` (→ 0 → empty), `!(lim > 0)` also
178
- // rejects NaN (NaN > 0 is false — it previously reached the native rung's interpolated LIMIT as
179
- // a SQL syntax error). Infinity stays legal ("no cap") and is clamped before interpolation.
180
123
  const rawLimit = opts?.limit ?? 20;
181
124
  const limit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : rawLimit;
182
125
  if (scopes.length === 0 || !(limit > 0))
183
126
  return [];
184
- // The EMPTY-QUERY contract is backend-uniform — File returns [] when the
185
- // query has no terms, so the vector rungs must not answer it either (an embedder may happily
186
- // embed "" and score every row). One guard ahead of ALL rungs.
187
127
  const q = termSet(query);
188
128
  if (q.size === 0)
189
129
  return [];
190
- // Native rung: rank in SQL. Excludes embedding-less rows (documented migration note).
191
130
  if (this.pgvectorSql && this.embedder) {
192
131
  const qv = await this.embedder.embed(query);
193
132
  if (Array.isArray(qv) && qv.length === this.embedder.dimensions) {
194
133
  const vecParam = scopes.length + 1;
195
- // #319:本档的排序**和截断都在 SQL 里**,所以两带序也必须进 ORDER BY —— 在进程里对一页
196
- // 已截断的结果重排是排不出来的(那正是契约要拒的「干净条目被饿死」)。带号谓词=不可变律
197
- // 模块里那条 `committedOriginOf` 的下推形(逐元素跑 core 的行首正则,与 JS 判据机器对表;
198
- // 为什么不能图省事写成 LIKE,见 originMarkedSqlPredicate 的顶注)。
199
134
  const bandExpr = bands ? `(CASE WHEN ${originMarkedSqlPredicate("frontmatter")} THEN 1 ELSE 0 END) ASC, ` : "";
200
135
  const res = await this.query(`SELECT id, scope, slug, frontmatter, rev, mtime_ms, size_bytes, embedding <=> $${vecParam}::vector AS dist
201
136
  FROM ${PG_MEMORY_ENGINE_TABLES.entry}
202
137
  WHERE scope IN (${inPlaceholders(1, scopes.length)}) AND embedding IS NOT NULL
203
138
  ORDER BY ${bandExpr}embedding <=> $${vecParam}::vector ASC, id ASC
204
139
  LIMIT ${Math.min(limit, 100_000)}`, [...scopes, vectorLiteral(qv)]);
205
- // ⚠️ `id ASC` 必须写在 SQL 的 ORDER BY 里、也就是 LIMIT **之前**(codex 对抗复审 R1-[medium]):
206
- // 同分(同一条向量距离)超过 limit 条时,决定「谁被截掉」的是这一句;缺了它,留下哪几条由执行
207
- // 计划随机决定,页内怎么重排都换不回被截掉的那条。列是 `text COLLATE "C"` ⇒ 字节序,与下面
208
- // 进程内比较器的定序同向。
209
140
  const hits = res.rows.map((r) => ({ ...this.headerFromRow(r), score: Number(r.dist) }));
210
- // 页内再排一次:契约序的最终裁决在进程侧(单带时是恒等重排)。
211
141
  return hits.sort(contractOrder);
212
142
  }
213
- // Wrong-dimension query vector (transient embedder fault) — fall open to the in-process path.
214
143
  }
215
144
  const res = await this.query(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes, embedding FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE scope IN (${inPlaceholders(1, scopes.length)})`, [...scopes]);
216
145
  const queryVec = this.embedder ? await this.embedder.embed(query).catch(() => null) : null;
@@ -219,8 +148,6 @@ export class PgMemoryEngineBackend {
219
148
  for (const raw of res.rows) {
220
149
  const r = raw;
221
150
  const fm = fromJson(r.frontmatter);
222
- // Portable rung: cosine over the stored jsonb vector when both sides are dimension-valid;
223
- // any missing/invalid side falls open to the lexical floor for THIS row (never dropped).
224
151
  if (queryVec && Array.isArray(queryVec) && queryVec.length === dims && r.embedding != null) {
225
152
  const stored = fromJson(r.embedding);
226
153
  if (Array.isArray(stored) && stored.length === dims) {
@@ -233,24 +160,14 @@ export class PgMemoryEngineBackend {
233
160
  continue;
234
161
  scored.push({ ...this.headerFromRow(r), score: d });
235
162
  }
236
- // opus 审 C4: deterministic id tie-break — tied scores must not order by row-arrival accident
237
- // (the cross-backend equivalence assertion compares ordered projections; File mirrors this).
238
- // #319:整档在进程内排完**再**切片 ⇒ 两带序天然落在截断之前(SQL 侧无 LIMIT 可绕过)。
239
163
  scored.sort(contractOrder);
240
164
  return scored.slice(0, limit);
241
165
  }
242
166
  async applyPatches(patches) {
243
167
  const report = { applied: [], conflicts: [] };
244
- // At most ONE non-delete op per id per batch (file-backend parity — a COPY keeping its frontmatter
245
- // id would leave the projection↔applied binding ambiguous; refuse the later one).
246
168
  const nonDeleteIds = new Set();
247
- // In-batch (scope, slug) claims so two adds of one slug in one batch suffix deterministically.
248
169
  const plannedSlugs = new Set();
249
- // 142-S5.1: one history row per APPLIED patch, flushed as one batch below (append-only audit face).
250
170
  const historyRows = [];
251
- // #307 件6(design/336 §2.3-2):external-origin 不可变律的**批次基线**。本后端逐条 patch 直打真库,
252
- // 所以同批 delete 之后再 add 时库里已无该行 —— 只看当下会把它误判成「合法新生命」。按本批第一次
253
- // 触碰记下已提交 origin,整批按那一份判(理由与两半判据见 memory-origin-law.ts 顶注)。
254
171
  const originBaseline = createBatchCommittedBaseline();
255
172
  for (const patch of patches) {
256
173
  try {
@@ -267,17 +184,11 @@ export class PgMemoryEngineBackend {
267
184
  report.conflicts.push({ op: patch.op, id: patch.id, reason: `io error: ${err instanceof Error ? err.message : String(err)}` });
268
185
  }
269
186
  }
270
- // BEST-EFFORT history append (see PgMemoryEngineBackendOptions.history for the honest trade-off:
271
- // the entry writes above are already committed; a failed audit append must not fail the report —
272
- // swallow + warn, never throw. appendHistory is (id,rev)-idempotent, so a caller retry is safe.
273
187
  if (this.history && historyRows.length > 0) {
274
188
  try {
275
189
  await this.history.appendHistory(historyRows);
276
190
  }
277
191
  catch (err) {
278
- // Every warn carries the CUMULATIVE lost count — a persistent silent failure
279
- // (history table dropped, grant flipped) shows as a growing number, not a one-off line.
280
- // The metrics counter leg lives in the main.ts sink wrapper (memory_history_append_failed_total).
281
192
  this.historyRowsLost += historyRows.length;
282
193
  console.warn(`PgMemoryEngineBackend: best-effort history append failed (${historyRows.length} row(s) lost, ${this.historyRowsLost} cumulative — audit face only, entries committed): ${err instanceof Error ? err.message : String(err)}`);
283
194
  }
@@ -286,9 +197,6 @@ export class PgMemoryEngineBackend {
286
197
  }
287
198
  async applyOne(patch, report, plannedSlugs, historyRows, originBaseline) {
288
199
  const T = PG_MEMORY_ENGINE_TABLES.entry;
289
- // #307 件6(codex R1-[high]):op 闭集**前置**判 —— 词表外的 op 会跳过两道白洗守卫(它们都写成
290
- // `if (patch.op === "update")`)、落进 update 的 SQL 并报成一次 applied。零 I/O 拒绝(理由与
291
- // 今日可达性见 memory-origin-law.ts 的 unknownPatchOpRefusal 顶注;tidb 孪生同处同判)。
292
200
  {
293
201
  const refusal = unknownPatchOpRefusal(patch.op);
294
202
  if (refusal !== undefined) {
@@ -296,18 +204,10 @@ export class PgMemoryEngineBackend {
296
204
  return;
297
205
  }
298
206
  }
299
- // A patch whose entry carries a DIFFERENT id than the patch addresses is
300
- // malformed — the two backends would otherwise diverge (File writes the entry's id, Pg would
301
- // key the SELECT on patch.id but the write on entry.id). Refused up front, both backends alike.
302
207
  if (patch.op !== "delete" && patch.entry !== undefined && patch.entry.id !== patch.id) {
303
208
  report.conflicts.push({ op: patch.op, id: patch.id, reason: `patch id ${JSON.stringify(patch.id)} != entry id ${JSON.stringify(patch.entry.id)} (malformed patch refused)` });
304
209
  return;
305
210
  }
306
- // #271 件1(R-1):键宽在**任何 I/O 之前**判,add/update 两条腿一处收(tidb 版同注)。R5 那批只
307
- // 护住了 add 腿的 slug ⇒ 一次改名(update `SET scope=…, slug=…`)就能把超宽键送进 DB;而超宽键
308
- // 的两个结局都坏——PG 落不可分类的裸 22001,非严格 MySQL **静默截断**(截断后的键指向**另一个**
309
- // scope/slug:两租户的盘折叠 / 两条目互串)。拒绝式,与 unstorable_bytes 同族(后端能力差异诚实
310
- // 暴露,重试恒同答=幂等成立);写语句一条都不发出去。
311
211
  if (patch.op !== "delete" && patch.entry !== undefined) {
312
212
  const refusal = memoryEntryKeyWidthRefusal(patch.entry.scope, patch.entry.slug);
313
213
  if (refusal !== undefined) {
@@ -315,9 +215,6 @@ export class PgMemoryEngineBackend {
315
215
  return;
316
216
  }
317
217
  }
318
- // #319 增量([4768] 勘误 + core `1a5ebaa1`):**歧义 origin 表示**零写拒。判位与 File 的 planOne
319
- // 逐字同 —— 表示合法性先于状态比较:排在键形守卫之后、add/update 两支的 guard/跨 scope/白洗/CAS
320
- // 之前,且在任何 I/O 之前。判据用 core 导出的谓词(禁手抄),文案照抄 File(跨后端同一输入同一答复)。
321
218
  if (patch.op !== "delete" && patch.entry !== undefined) {
322
219
  const refusal = ambiguousOriginRefusal(patch.entry.frontmatter);
323
220
  if (refusal !== undefined) {
@@ -331,28 +228,13 @@ export class PgMemoryEngineBackend {
331
228
  report.conflicts.push({ op: "add", id: patch.id, reason: "add patch without an entry" });
332
229
  return;
333
230
  }
334
- // codex R2-H3→R3(终形):**拒绝式**——sync 协议的 rev/CAS/幂等全部对拍原文,存储层任何改写
335
- // (有损或 canonical)都让 ack rev≠库内 rev(响应丢失重试=假冲突,R3 实锚)。含不可存字节的
336
- // entry 在 PG 后端 fail-loud 报 conflict(reason 指路客户端清源);File 后端能存=后端能力差,
337
- // 诚实暴露优于静默改写。重试恒同答=幂等成立。
338
231
  if (pgHasUnstorable(rawEntry.frontmatter) || pgHasUnstorable(rawEntry.body) || pgHasUnstorable(rawEntry.slug)) {
339
232
  report.conflicts.push({ op: "add", id: rawEntry.id, reason: "unstorable_bytes (PG cannot store NUL/lone surrogates; strip them at the source — the store never rewrites content)" });
340
233
  return;
341
234
  }
342
- // (R5 的 add-only slug 守卫已上提到 applyOne 顶部的键宽块 —— add/update 同判,见那里的注。)
343
235
  const entry = rawEntry;
344
- // opus 审 C3: an add whose id already lives in a DIFFERENT scope is refused explicitly — the bare
345
- // `ON CONFLICT (id) DO UPDATE SET scope=…` would silently MOVE the row across scopes (and diverge
346
- // from the File backend, which never scans other scopes on add). The engine's call surface never
347
- // produces this input (harvest/materialize are scope-local); a conflict report is the honest
348
- // fail-closed answer. Same-scope re-add stays the idempotent overwrite (File parity).
349
- // One probe serves BOTH the E-02 guard below and the cross-scope refusal (File-backend parity).
350
236
  const prior = await this.query(`SELECT scope, slug, rev, frontmatter FROM ${T} WHERE id = $1`, [entry.id]);
351
237
  const priorRow = prior.rows.length > 0 ? prior.rows[0] : undefined;
352
- // #307 件6(design/336 §2.3-2)—— external-origin 不可变律,**判在 guard 之前**:core File backend
353
- // 的 planOne 也是先判 origin 再判 `guard:"absent"`,而契约试剂盒对「带 guard 的洗白 add」要的是
354
- // `/malformed patch refused/`;顺序反了会先报 `add_guard_absent_conflict`,同一输入两后端两个答复。
355
- // 基线取「本批第一次触碰」那一份(同批 delete 之后的 re-add 照样被这条拒;已提交的墓碑才是合法出口)。
356
238
  {
357
239
  const committedBaseFm = rememberBatchCommitted(originBaseline, entry.id, priorRow !== undefined ? fromJson(priorRow.frontmatter) : undefined);
358
240
  const refusal = originWhitewashRefusal("add", committedBaseFm, entry.frontmatter);
@@ -360,19 +242,12 @@ export class PgMemoryEngineBackend {
360
242
  report.conflicts.push({ op: "add", id: entry.id, reason: refusal });
361
243
  return;
362
244
  }
363
- // #337(conformance c36,design/339,TiDB 孪生同注):distilled 不可变律的 add 腿 —— 与 origin
364
- // 律同基线同判位(guard 之前;同批 delete+re-add 靠基线拒,已提交墓碑后的 add 放行)。
365
245
  const dRefusal = distilledWhitewashRefusal("add", committedBaseFm, entry.frontmatter);
366
246
  if (dRefusal !== undefined) {
367
247
  report.conflicts.push({ op: "add", id: entry.id, reason: dRefusal });
368
248
  return;
369
249
  }
370
250
  }
371
- // E-02 add-if-absent guard (core 1.275.1/1.276.0 contract clause, design/142 S2.5): a
372
- // `guard:"absent"` add must NEVER blind-overwrite an existing id — same rev AND same
373
- // (scope, slug) projection = idempotent fall-through; different rev OR different projection =
374
- // REPORTED `add_guard_absent_conflict` carrying currentRev (File backend semantics verbatim;
375
- // caught live by the contract suite on real TiDB 2026-07-13 — the twins had no guard leg).
376
251
  if (patch.guard === "absent" && priorRow) {
377
252
  const currentRev = String(priorRow.rev);
378
253
  if (currentRev !== computeEntryRev(entry)) {
@@ -383,7 +258,6 @@ export class PgMemoryEngineBackend {
383
258
  report.conflicts.push({ op: "add", id: entry.id, reason: `add_guard_absent_conflict (id already exists at the same rev but a different projection ${JSON.stringify(`${String(priorRow.scope)}/${String(priorRow.slug)}`)}; add-if-absent refuses to duplicate)`, currentRev });
384
259
  return;
385
260
  }
386
- // same rev AND same projection ⇒ idempotent; fall through to the normal add (no-op re-write).
387
261
  }
388
262
  if (priorRow) {
389
263
  const held = String(priorRow.scope);
@@ -392,18 +266,10 @@ export class PgMemoryEngineBackend {
392
266
  return;
393
267
  }
394
268
  }
395
- // Slug collision with a DIFFERENT entry (same scope+slug, other id) → deterministic `-n` suffix
396
- // (file-backend parity). In-batch planned slugs count as taken too.
397
- // The SELECT probe alone is a race window — two instances can both observe "free" and both
398
- // insert one slug. The INSERT itself sits INSIDE the loop: a (scope,slug) unique-violation from
399
- // the concurrent loser retries the next suffix, so the DB constraint (ensureSchema) is the one
400
- // true arbiter and the probe is only an optimization.
401
- const cEntry = entry; // R3 终形:拒绝式守卫后恒干净
269
+ const cEntry = entry;
402
270
  const embedding = await this.embeddingParam(this.haystackOf(cEntry.slug, cEntry.frontmatter, cEntry.body));
403
271
  let slug = cEntry.slug;
404
272
  for (let n = 2;; n++) {
405
- // #271 件1(codex 对抗复审 H1;tidb 版同注):**派生**候选也要吃守卫 —— 顶部那道只判原始 slug,
406
- // 而撞键派生的 `${slug}-${n}` 可以越过列宽(511 + "-2" = 513),此前直奔 INSERT 落裸 22001。
407
273
  const derivedRefusal = memoryEntryKeyWidthRefusal(entry.scope, slug);
408
274
  if (derivedRefusal !== undefined) {
409
275
  report.conflicts.push({ op: "add", id: entry.id, reason: `${derivedRefusal} (slug collision suffix "-${n - 1}" no longer fits the column — shorten the entry name)` });
@@ -424,14 +290,12 @@ export class PgMemoryEngineBackend {
424
290
  catch (err) {
425
291
  if (isUniqueViolation(err)) {
426
292
  slug = `${entry.slug}-${n}`;
427
- continue; // concurrent winner took this (scope,slug) — try the next suffix
293
+ continue;
428
294
  }
429
295
  throw err;
430
296
  }
431
297
  plannedSlugs.add(key);
432
298
  report.applied.push({ op: "add", id: stored.id, slug: stored.slug });
433
- // History: chain head — prevRev absent by contract (a same-scope idempotent re-add of
434
- // identical content dedupes on the (id, rev) PK; changed content starts a fresh head).
435
299
  if (this.history)
436
300
  historyRows.push({ id: stored.id, rev, op: "add", scope: stored.scope, snapshot: { ...stored, rev }, writtenAtMs: this.clock() });
437
301
  return;
@@ -440,8 +304,6 @@ export class PgMemoryEngineBackend {
440
304
  slug = `${entry.slug}-${n}`;
441
305
  }
442
306
  }
443
- // update/delete address an EXISTING entry by immutable id. (scope in the projection = the
444
- // history row's scope for delete — a tombstone can't be joined back to the deleted row.)
445
307
  const cur = await this.query(`SELECT slug, rev, scope, frontmatter FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id = $1`, [patch.id]);
446
308
  if (cur.rows.length === 0) {
447
309
  report.conflicts.push({ op: patch.op, id: patch.id, reason: "unknown id (entry not found in backend)" });
@@ -450,13 +312,7 @@ export class PgMemoryEngineBackend {
450
312
  const currentRev = String(cur.rows[0].rev);
451
313
  const currentSlug = String(cur.rows[0].slug);
452
314
  const currentScope = String(cur.rows[0].scope);
453
- // Read-side whitewash refusal (core 1.302 contract I-2, File-backend parity incl. ordering: judged
454
- // BEFORE the rev CAS): an update may not strip provenance/trust off a repo-ingested entry. Judged on
455
- // the committed frontmatter of THIS SELECT — a write racing past it flips rev and the conditional
456
- // write below re-reports as a CAS conflict, so the window stays closed.
457
315
  const committedFm = fromJson(cur.rows[0].frontmatter);
458
- // #307 件6:**delete 腿也要记基线** —— 同批「delete 掉一条带标记的行、再 add 一条不带标记的」正是
459
- // 洗白的最直接写法,而 delete 一旦落库,后面那条 add 的 SELECT 就什么都看不到了。
460
316
  const committedBaseFm = rememberBatchCommitted(originBaseline, patch.id, committedFm);
461
317
  if (patch.op === "update" && patch.entry !== undefined) {
462
318
  if (committedFm.provenance?.kind === "repo_file") {
@@ -472,15 +328,11 @@ export class PgMemoryEngineBackend {
472
328
  return;
473
329
  }
474
330
  }
475
- // #307 件6:origin 不可变律的 update 腿。顺序与 core File backend 逐字同 —— repo_file 白洗判据
476
- // 在前、origin 在后、两者都在 rev CAS 之前(judged on the committed frontmatter of THIS SELECT)。
477
331
  const refusal = originWhitewashRefusal("update", committedBaseFm, patch.entry.frontmatter);
478
332
  if (refusal !== undefined) {
479
333
  report.conflicts.push({ op: "update", id: patch.id, reason: refusal });
480
334
  return;
481
335
  }
482
- // #337(conformance c36/c37,TiDB 孪生同注):distilled 不可变律的 update 腿 —— 同样先于 rev CAS
483
- // (c37:骑着 stale baseRev 的 strip 答 malformed refusal,不是 rev mismatch)。
484
336
  const dRefusal = distilledWhitewashRefusal("update", committedBaseFm, patch.entry.frontmatter);
485
337
  if (dRefusal !== undefined) {
486
338
  report.conflicts.push({ op: "update", id: patch.id, reason: dRefusal });
@@ -488,28 +340,16 @@ export class PgMemoryEngineBackend {
488
340
  }
489
341
  }
490
342
  if (patch.baseRev !== undefined && patch.baseRev !== currentRev) {
491
- // Per-id CAS (§2.4 并发): NEVER blind-write over a concurrent change. No shadow-restore leg here —
492
- // the DB row IS the committed state; nothing on a disk projection can shadow it.
493
- // ABA note (opus 审 C1): rev is a CONTENT hash, so an A→B→A round-trip by another writer makes a
494
- // stale baseRev "current" again and this CAS passes. Benign by construction — same hash ⇒ same
495
- // canonical content, the overwrite is idempotent; only the intervening-write SIGNAL is lost.
496
- // Shared with the File backend (same computeEntryRev), inherent to content-hash CAS.
497
343
  report.conflicts.push({ op: patch.op, id: patch.id, reason: "rev mismatch (concurrent change)", baseRev: patch.baseRev, currentRev });
498
344
  return;
499
345
  }
500
346
  if (patch.op === "delete") {
501
- // Conditional on the rev we just judged — a write that lands between the SELECT and this DELETE
502
- // re-reports as the same CAS conflict instead of silently deleting the newer committed state.
503
347
  const res = await this.query(`DELETE FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id = $1 AND rev = $2 RETURNING id`, [patch.id, currentRev]);
504
348
  if (res.rows.length === 0) {
505
349
  report.conflicts.push({ op: "delete", id: patch.id, reason: "rev mismatch (concurrent change)", baseRev: patch.baseRev, currentRev: await this.freshRev(patch.id, currentRev) });
506
350
  return;
507
351
  }
508
352
  report.applied.push({ op: "delete", id: patch.id, slug: currentSlug });
509
- // History tombstone. rev must be DISTINCT from the deleted version's rev — the (id, rev) PK
510
- // already holds a row for currentRev (the add/update that committed it), and ON CONFLICT DO
511
- // NOTHING would silently drop a colliding tombstone. Deterministic derived key: revs are
512
- // 16-hex content hashes (computeEntryRev), so the prefixed form stays well under varchar(128).
513
353
  if (this.history)
514
354
  historyRows.push({ id: patch.id, rev: `tombstone:${currentRev}`, prevRev: currentRev, op: "delete", scope: currentScope, writtenAtMs: this.clock() });
515
355
  return;
@@ -523,7 +363,7 @@ export class PgMemoryEngineBackend {
523
363
  report.conflicts.push({ op: "update", id: patch.id, reason: "unstorable_bytes (PG cannot store NUL/lone surrogates; strip them at the source — the store never rewrites content)" });
524
364
  return;
525
365
  }
526
- const cEntry = entry; // R3 终形:拒绝式后恒干净,rev 对拍原文
366
+ const cEntry = entry;
527
367
  const rev = computeEntryRev(cEntry);
528
368
  const embedding = await this.embeddingParam(this.haystackOf(cEntry.slug, cEntry.frontmatter, cEntry.body));
529
369
  const res = await this.query(`UPDATE ${PG_MEMORY_ENGINE_TABLES.entry}
@@ -535,13 +375,9 @@ export class PgMemoryEngineBackend {
535
375
  return;
536
376
  }
537
377
  report.applied.push({ op: "update", id: entry.id, slug: entry.slug });
538
- // History: rev-chain link — snapshot carries the committed entry (rev = the NEW content hash).
539
378
  if (this.history)
540
379
  historyRows.push({ id: entry.id, rev, prevRev: currentRev, op: "update", scope: entry.scope, snapshot: { ...entry, rev }, writtenAtMs: this.clock() });
541
380
  }
542
- /** When the conditional write returns zero rows (a write landed inside the
543
- * SELECT→write window), the report's `currentRev` must be the rev NOW — re-reporting the pre-race
544
- * SELECT value could claim `baseRev === currentRev` alongside a "rev mismatch" reason. */
545
381
  async freshRev(id, fallback) {
546
382
  const res = await this.query(`SELECT rev FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id = $1`, [id]);
547
383
  return res.rows.length > 0 ? String(res.rows[0].rev) : fallback;
@@ -551,7 +387,7 @@ export class PgMemoryEngineBackend {
551
387
  return res.rows.length > 0 ? String(res.rows[0].cursor) : undefined;
552
388
  }
553
389
  async setConsolidationCursor(scope, cursor) {
554
- assertMemoryScopeWidth(scope); // #271 件1:游标表 PK 也是 varchar(190) —— 截断=把两个盘的整合游标折叠成一个
390
+ assertMemoryScopeWidth(scope);
555
391
  await this.query(`INSERT INTO ${PG_MEMORY_ENGINE_TABLES.cursor} (scope, cursor) VALUES ($1, $2)
556
392
  ON CONFLICT (scope) DO UPDATE SET cursor = $2`, [scope, cursor]);
557
393
  }