@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,47 +1,17 @@
1
- /**
2
- * PostgreSQL-backed {@link BaseSessionStorage} — the PG twin of {@link TiDBSessionStorage}
3
- * (src/plugins/tidb-session-storage.ts; clay 2026-06-21, the second session backend).
4
- *
5
- * A behavioural TWIN: identical class/method surface + identical semantics (the F2 optimistic-lock CAS on
6
- * `leaf_id`/`leaf_seq`, the F3 bounded `[floor..leaf]` wake, the sub-floor durable getEntry fallback), only the
7
- * SQL dialect differs. Kept in lock-step with the TiDB store by ONE shared real-DB integration suite
8
- * (test/pg-session-storage-integration.test.ts) that drives the SAME scenarios against BOTH engines. Dialect deltas:
9
- * - placeholders `$1..$n` (not `?`) - affected rows = `result.rowCount` (not affectedRows)
10
- * - `leaf_id IS NOT DISTINCT FROM $n` (not `<=>`) - dup-key SQLSTATE `23505` (not ER_DUP_ENTRY)
11
- * - `payload` is `jsonb`, bound `$n::jsonb`; node-pg PARSES jsonb → object on read (parseJson passes objects through)
12
- * - BIGINT (`seq`/`leaf_seq`) comes back as a STRING → always `Number(...)`
13
- * - transaction via `pool.connect()` + BEGIN/COMMIT/ROLLBACK/release (not getConnection/beginTransaction)
14
- *
15
- * Unlike the run/checkpoint PG stores, this store carries its OWN schema (PG_SESSION_SCHEMA + ensureSchema): the
16
- * session_meta/session_event tables are DISJOINT from the centrally-owned run-store/checkpoint tables, so a
17
- * self-contained CREATE IF NOT EXISTS cannot shadow another store's table. (Central aggregation into pg-pool is
18
- * done separately afterward.)
19
- */
20
1
  import { emitLeafAdvance } from "../session-leaf-bus.js";
21
- // 🔴 core 1.403 迁移(2026-07-25):runtime 构造用法,`Session` → `StoredSession`(见 audit.ts 同款注释)。
22
2
  import { BaseSessionStorage, StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAfterEntry, normalizePromptEpoch, } from "@sema-agent/core";
23
3
  import { escapeLike } from "./sql-escape.js";
24
4
  import { pgSafeJsonStringify } from "./pg-safe-json.js";
25
5
  import { toIso } from "./sql-row-helpers.js";
26
6
  import { assertPlacementAdmission, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SQL_SESSION_PLACEMENTS, } from "./session-placement.js";
27
7
  import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
28
- import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, // 两孪生店同源(理由见 kernel 处顶注)
29
- } from "../session-sync-kernel.js";
8
+ import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
30
9
  import { isPgUniqueViolation } from "./sql-errors.js";
31
10
  import { MANAGED_RETENTION } from "./retention-store-sql.js";
32
- /** 判据属主 = `sql-errors.ts`(A-032 P1-①)。 */
33
11
  const isDupKey = isPgUniqueViolation;
34
- /** node-pg parses jsonb → object already; only strings need JSON.parse, everything else passes through. */
35
12
  function parseEntry(p) {
36
13
  return (typeof p === "string" ? JSON.parse(p) : p);
37
14
  }
38
- /**
39
- * PG-dialect DDL for the L1 session log, translated from tidb-pool.ts SCHEMA_STATEMENTS:
40
- * JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3), inline `KEY`/`UNIQUE KEY`→separate CREATE INDEX (UNIQUE for uq_session_event_entry).
41
- *
42
- * SCHEMA POLICY: see the header of pg-pool.ts — the code is the single source of truth, schema changes are
43
- * drop-and-recreate, and NO new `ALTER TABLE` seams are added here; fold into the CREATE instead.
44
- */
45
15
  export const PG_SESSION_SCHEMA = [
46
16
  `CREATE TABLE IF NOT EXISTS session_meta (
47
17
  session_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -67,7 +37,6 @@ export const PG_SESSION_SCHEMA = [
67
37
  PRIMARY KEY (session_id)
68
38
  )`,
69
39
  `CREATE INDEX IF NOT EXISTS idx_session_meta_owner ON session_meta (owner)`,
70
- // listPlaced 的检索腿(TiDB 孪生的 KEY idx_session_meta_placement 同形)。
71
40
  `CREATE INDEX IF NOT EXISTS idx_session_meta_placement ON session_meta (placement_kind, placement_scope, placed_at)`,
72
41
  `CREATE TABLE IF NOT EXISTS session_event (
73
42
  session_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -81,17 +50,10 @@ export const PG_SESSION_SCHEMA = [
81
50
  )`,
82
51
  `CREATE UNIQUE INDEX IF NOT EXISTS uq_session_event_entry ON session_event (session_id, entry_id)`,
83
52
  ];
84
- /** Idempotent schema apply for this store's tables (the test calls this; production aggregation is separate). */
85
53
  export async function ensureSchema(pool) {
86
54
  for (const stmt of PG_SESSION_SCHEMA)
87
55
  await pool.query(stmt);
88
56
  }
89
- /**
90
- * PostgreSQL-backed {@link BaseSessionStorage} — the durable L1 event log for one session.
91
- *
92
- * Behaviourally identical to {@link TiDBSessionStorage}; see that file's class doc for the F2/F3 seams. The only
93
- * differences are dialectal (placeholders, `IS NOT DISTINCT FROM`, jsonb, rowCount, transaction handle).
94
- */
95
57
  export class PgSessionStorage extends BaseSessionStorage {
96
58
  pool;
97
59
  sessionId;
@@ -102,17 +64,14 @@ export class PgSessionStorage extends BaseSessionStorage {
102
64
  this.sessionId = sessionId;
103
65
  this.floorSeq = floorSeq;
104
66
  }
105
- /** @see TiDBSessionStorage.getEpochAnchor — [976] S3 RB-14 pull seam, PG dialect twin
106
- * (keyset-paged to exhaustion: a bounded scan over restatement-less pre-epoch compactions would
107
- * misreport an epoch-bearing session as pre-epoch — codex HIGH). */
108
67
  async getEpochAnchor() {
109
68
  if (this.floorSeq <= 0)
110
- return undefined; // full load — the walk already reaches the root pin
69
+ return undefined;
111
70
  let cursor = this.floorSeq;
112
71
  for (;;) {
113
72
  const res = await this.pool.query("SELECT seq, payload FROM session_event WHERE session_id = $1 AND seq < $2 AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100", [this.sessionId, cursor]);
114
73
  if (res.rows.length === 0)
115
- return undefined; // exhausted — a genuine pre-epoch session
74
+ return undefined;
116
75
  for (const r of res.rows) {
117
76
  const e = parseEntry(r.payload);
118
77
  const d = e.type === "prompt_epoch" ? normalizePromptEpoch(e) : normalizePromptEpoch(e.details?.promptEpoch);
@@ -122,23 +81,18 @@ export class PgSessionStorage extends BaseSessionStorage {
122
81
  cursor = Number(res.rows[res.rows.length - 1].seq);
123
82
  }
124
83
  }
125
- /** @see TiDBSessionStorage.wake — F3 bounded `[floor..leaf]` rebuild; null if the session does not exist. */
126
84
  static async wake(pool, sessionId) {
127
- const metaRes = await pool.query(
128
- // #292 C18(TiDB 孪生同形):placement 六列捎带在既有 meta SELECT 上,acquire 热路径零额外往返。
129
- `SELECT created_at, leaf_id, leaf_seq, ${PLACEMENT_SELECT_COLUMNS} FROM session_meta WHERE session_id = $1`, [sessionId]);
85
+ const metaRes = await pool.query(`SELECT created_at, leaf_id, leaf_seq, ${PLACEMENT_SELECT_COLUMNS} FROM session_meta WHERE session_id = $1`, [sessionId]);
130
86
  const meta = metaRes.rows[0];
131
87
  if (!meta)
132
88
  return null;
133
89
  const { floorEntryId, floorSeq } = await PgSessionStorage.computeFloor(pool, sessionId);
134
90
  const eventRes = await pool.query("SELECT payload FROM session_event WHERE session_id = $1 AND seq >= $2 ORDER BY seq ASC", [sessionId, floorSeq]);
135
91
  const entries = eventRes.rows.map((r) => parseEntry(r.payload));
136
- // #292 C18:持久化元组骑 core 5.48 的 `SessionMetadata.placement`(TiDB 孪生逐字同义)。
137
92
  const placement = placementFromRow(meta);
138
93
  const metadata = { id: sessionId, createdAt: toIso(meta.created_at), ...(placement !== undefined ? { placement } : {}) };
139
94
  return new PgSessionStorage(pool, sessionId, metadata, entries, meta.leaf_id ?? null, floorEntryId, floorSeq);
140
95
  }
141
- /** The bounded-window floor = last compaction's firstKeptEntryId (and its seq). @see TiDBSessionStorage.computeFloor */
142
96
  static async computeFloor(pool, sessionId) {
143
97
  const compRes = await pool.query("SELECT payload FROM session_event WHERE session_id = $1 AND type = 'compaction' ORDER BY seq DESC LIMIT 1", [sessionId]);
144
98
  if (!compRes.rows[0])
@@ -148,12 +102,10 @@ export class PgSessionStorage extends BaseSessionStorage {
148
102
  if (!fk)
149
103
  return { floorEntryId: null, floorSeq: 0 };
150
104
  const seqRes = await pool.query("SELECT seq FROM session_event WHERE session_id = $1 AND entry_id = $2", [sessionId, fk]);
151
- // firstKeptEntryId missing from the log (shouldn't happen) → degrade to full load.
152
105
  if (!seqRes.rows[0])
153
106
  return { floorEntryId: null, floorSeq: 0 };
154
107
  return { floorEntryId: fk, floorSeq: Number(seqRes.rows[0].seq) };
155
108
  }
156
- /** Sub-floor lookups (e.g. a label target) aren't in the loaded tail — fall back to the durable log. */
157
109
  async getEntry(id) {
158
110
  const inMem = await super.getEntry(id);
159
111
  if (inMem)
@@ -169,16 +121,10 @@ export class PgSessionStorage extends BaseSessionStorage {
169
121
  this.recordEntry(entry);
170
122
  }
171
123
  async setLeafId(leafId, opts) {
172
- // A leaf move is itself a persisted `leaf` entry whose parent is the current leaf.
173
124
  const leafEntry = this.createLeafEntry(leafId);
174
125
  await this.persist(leafEntry, opts);
175
126
  this.recordEntry(leafEntry);
176
127
  }
177
- /**
178
- * Atomically: CAS the meta leaf (+bump leaf_seq), then append the event at the new seq.
179
- * The `(session_id, seq)` primary key is the backstop — a racing writer that grabbed the same
180
- * seq makes our INSERT fail with a duplicate-key (23505), which we translate to a conflict too.
181
- */
182
128
  async persist(entry, opts) {
183
129
  const newLeaf = leafIdAfterEntry(entry);
184
130
  const conditional = opts !== undefined && opts.expectedLeafId !== undefined;
@@ -195,17 +141,16 @@ export class PgSessionStorage extends BaseSessionStorage {
195
141
  }
196
142
  const seqRes = await conn.query("SELECT leaf_seq, owner FROM session_meta WHERE session_id = $1", [this.sessionId]);
197
143
  const seq = Number(seqRes.rows[0].leaf_seq);
198
- const commitOwner = seqRes.rows[0].owner ?? null; // codex-2:S2 发射 owner=本事务当前行(tidb twin 同注)
144
+ const commitOwner = seqRes.rows[0].owner ?? null;
199
145
  await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [this.sessionId, seq, entry.id, entry.parentId ?? null, entry.type, pgSafeJsonStringify(entry), new Date(entry.timestamp)]);
200
146
  await conn.query("COMMIT");
201
- emitLeafAdvance(this.sessionId, newLeaf, commitOwner, seq); // S2 fast path — post-commit only, fire-and-forget (session-leaf-bus 契约;tidb twin 同点)
147
+ emitLeafAdvance(this.sessionId, newLeaf, commitOwner, seq);
202
148
  }
203
149
  catch (err) {
204
150
  try {
205
151
  await conn.query("ROLLBACK");
206
152
  }
207
153
  catch {
208
- /* ignore rollback error */
209
154
  }
210
155
  if (err instanceof SessionError)
211
156
  throw err;
@@ -219,19 +164,10 @@ export class PgSessionStorage extends BaseSessionStorage {
219
164
  }
220
165
  }
221
166
  }
222
- // ────────────────────────────────── PG twin of TiDBSessionStore ──────────────────────────────────
223
- /**
224
- * PostgreSQL-backed {@link SessionStore} — the PG twin of {@link TiDBSessionStore}. Maps the
225
- * `SessionStore` surface (acquire / register / ownerOf / touch / release) onto {@link PgSessionStorage}.
226
- */
227
167
  export class PgSessionStore {
228
168
  pool;
229
- /** #270 车1:托管留存声明(TiDB 孪生同格)。读法见 `retention-store-sql.ts` 的 {@link MANAGED_RETENTION}。 */
230
169
  retention = MANAGED_RETENTION;
231
- /** #292 C18(core 5.48.0 design/323):placement 声明(TiDB 孪生同格,义务逐条见
232
- * `src/plugins/session-placement.ts` 顶注)。 */
233
170
  placements = SQL_SESSION_PLACEMENTS;
234
- /** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one. */
235
171
  pending = new Map();
236
172
  constructor(pool) {
237
173
  this.pool = pool;
@@ -239,15 +175,11 @@ export class PgSessionStore {
239
175
  async acquire(sessionId, opts) {
240
176
  if (!sessionId) {
241
177
  const id = uuidv7();
242
- await this.register(id, null, opts?.placement); // #292:元组与行同一条 INSERT(TiDB 孪生同义)
178
+ await this.register(id, null, opts?.placement);
243
179
  return this.load(id, false, opts);
244
180
  }
245
- // design/114 Phase3 (F1): a `requireExisting` acquire NEVER creates and can REJECT (not_found) — it MUST bypass
246
- // the shared create-dedup `pending`, else a concurrent create-capable waiter awaiting that same promise would
247
- // inherit the not_found rejection instead of creating-on-miss. (Twin of TiDBSessionStore.acquire.)
248
181
  if (opts?.requireExisting)
249
182
  return this.load(sessionId, true, opts);
250
- // #292:准入按调用方逐一判(共享 promise 不许让 claim 形继承可信创建方的准入;TiDB 孪生同义)。
251
183
  const inflight = this.pending.get(sessionId);
252
184
  if (inflight) {
253
185
  const acquired = await inflight;
@@ -261,39 +193,29 @@ export class PgSessionStore {
261
193
  async load(sessionId, requireExisting = false, opts) {
262
194
  let storage = await PgSessionStorage.wake(this.pool, sessionId);
263
195
  if (storage) {
264
- assertPlacementAdmission(sessionId, (await storage.getMetadata()).placement, opts); // #292 claim 关门
196
+ assertPlacementAdmission(sessionId, (await storage.getMetadata()).placement, opts);
265
197
  return { session: new StoredSession(storage), sessionId };
266
198
  }
267
- // design/114 Phase3: `requireExisting` ⇒ FAIL LOUD on a genuinely missing session (no session_meta row),
268
- // never the silent create-on-miss. core's prepare-task maps this `not_found` to `resume.session_not_found`.
269
199
  if (requireExisting)
270
200
  throw new SessionError("not_found", `session ${sessionId} not found`);
271
201
  await this.register(sessionId, null, opts?.placement);
272
202
  storage = await PgSessionStorage.wake(this.pool, sessionId);
273
- // 刚 register 完却唤不醒 = 并发 purge 抢在两步之间(TiDB 孪生同款注:旧形的 `storage!` 断言会炸成
274
- // 一条无信息的 TypeError)。
275
203
  if (!storage) {
276
204
  throw new SessionError("storage", `session ${sessionId} vanished between register and wake (a concurrent purge won the race) — retry`);
277
205
  }
278
- // #292 镜像方向(pre-claim squat):回读缺失/异组 ⇒ 响亮拒(TiDB 孪生同义)。
279
206
  assertPlacementAdmission(sessionId, (await storage.getMetadata()).placement, opts);
280
207
  return { session: new StoredSession(storage), sessionId };
281
208
  }
282
- /** Idempotently create a session_meta row with an owner. Existing owner is never overwritten.
283
- * #292:`placement` 与行同一条 INSERT;`DO NOTHING` 臂 ⇒ 已存在行的元组永不被重写(first-write 不可变)。 */
284
209
  async register(sessionId, owner, placement) {
285
210
  const now = new Date();
286
211
  await this.pool.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, " +
287
212
  "placement_kind, placement_scope, placement_parent_session_id, placement_root_session_id, placement_handle, placed_at) " +
288
213
  "VALUES ($1,$2,$3,$4,NULL,0,$5,$6,$7,$8,$9,$10) ON CONFLICT (session_id) DO NOTHING", [sessionId, owner, now, now, ...placementInsertValues(placement, now)]);
289
214
  }
290
- /** #292 义务 6 的廉价探针(TiDB 孪生 {@link TiDBSessionStore.placementOf} 同义)。 */
291
215
  async placementOf(sessionId) {
292
216
  const res = await this.pool.query(`SELECT ${PLACEMENT_SELECT_COLUMNS} FROM session_meta WHERE session_id = $1`, [sessionId]);
293
217
  return placementFromRow(res.rows[0]);
294
218
  }
295
- /** #292 义务 5 —— placed 分区枚举(年龄锚/上界/tupleIncomplete 三条读法与 TiDB 孪生逐字同义,
296
- * 见 {@link TiDBSessionStore.listPlaced})。 */
297
219
  async listPlaced(kind, opts) {
298
220
  const params = [kind];
299
221
  const ph = (v) => {
@@ -309,7 +231,6 @@ export class PgSessionStore {
309
231
  const res = await this.pool.query(sql, params);
310
232
  return res.rows.map((r) => placedRowFromRow(String(r.session_id), r, r.updated_at instanceof Date ? r.updated_at.getTime() : undefined));
311
233
  }
312
- /** Owner principal of a session, `null` if registered ownerless, `undefined` if no such session. */
313
234
  async ownerOf(sessionId) {
314
235
  const res = await this.pool.query("SELECT owner FROM session_meta WHERE session_id = $1", [sessionId]);
315
236
  if (res.rows.length === 0)
@@ -322,34 +243,26 @@ export class PgSessionStore {
322
243
  sessionId,
323
244
  ]);
324
245
  }
325
- /** E18 — the session's current leaf SessionTreeEntry.id (session_meta.leaf_id). @see TiDBSessionStore.getLeafId
326
- * (single SELECT, no acquire/lock, cache-bypassing — safe to read mid-run for the resume-at turn-capture). */
327
246
  async getLeafId(sessionId) {
328
247
  const r = await this.pool.query("SELECT leaf_id FROM session_meta WHERE session_id = $1", [sessionId]);
329
248
  return r.rows[0] ? (r.rows[0].leaf_id ?? null) : null;
330
249
  }
331
- /** [1196] 六轮复审:owner+leafId 单行原子快照(@see TiDBSessionStore.getHead——混世代组合防)。 */
332
250
  async getHead(sessionId) {
333
251
  const r = await this.pool.query("SELECT owner, leaf_id FROM session_meta WHERE session_id = $1", [sessionId]);
334
252
  if (r.rows.length === 0)
335
253
  return undefined;
336
254
  return { owner: r.rows[0].owner ?? null, leafId: r.rows[0].leaf_id ?? null };
337
255
  }
338
- /** Durable history is retained; just drop any in-flight cache entry.
339
- * 🔴 #292 义务 4:**placed 会话例外 —— release 是真删除**(理由/ABA 条件删/失败上浮三条读法逐字见
340
- * {@link TiDBSessionStore.release},本腿是它的 PG 孪生)。 */
341
256
  async release(sessionId) {
342
257
  this.pending.delete(sessionId);
343
258
  const placed = await this.placementOf(sessionId);
344
259
  if (placed === undefined)
345
- return; // 普通会话:语义逐字节不变(义务 3)
260
+ return;
346
261
  await this.deletePlacedSession(sessionId);
347
262
  }
348
- /** @see TiDBSessionStore.forget — 丢引用不动 durable 史(preflight 纪律的落点;PG 孪生)。 */
349
263
  forget(sessionId) {
350
264
  this.pending.delete(sessionId);
351
265
  }
352
- /** @see TiDBSessionStore.deletePlacedSession — PG 孪生(条件删门 + meta 先删 + 失败上浮同义)。 */
353
266
  async deletePlacedSession(sessionId) {
354
267
  const conn = await this.pool.connect();
355
268
  try {
@@ -364,7 +277,6 @@ export class PgSessionStore {
364
277
  await conn.query("ROLLBACK");
365
278
  }
366
279
  catch {
367
- /* ignore */
368
280
  }
369
281
  throw err instanceof SessionError
370
282
  ? err
@@ -374,29 +286,21 @@ export class PgSessionStore {
374
286
  conn.release();
375
287
  }
376
288
  }
377
- /** #292:placed id 拒绝 session-sync 的整段换装(TiDB 孪生 {@link TiDBSessionStore.assertNotPlaced} 同义)。 */
378
289
  async assertNotPlaced(sessionId, what) {
379
290
  const placed = await this.placementOf(sessionId);
380
291
  if (placed !== undefined) {
381
292
  throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placed.kind} placement partition — a session-sync ${what} cannot overwrite a delegation transcript`);
382
293
  }
383
294
  }
384
- // ───────────────────────── SessionRepo seam (§0.5) — PG twin of TiDBSessionStore ─────────────────────────
385
- // Behaviourally identical to {@link TiDBSessionStore.listSessions/fork/deleteSession}; only the SQL dialect
386
- // differs ($n placeholders, jsonb, rowCount, BEGIN/COMMIT handle). Kept in lock-step by the shared real-DB suite.
387
- /** @see TiDBSessionStore.listSessions — session identity from session_meta (owner-filtered), task_run LEFT JOIN for preview. */
388
295
  async listSessions(opts) {
389
296
  const params = [];
390
297
  const ph = (v) => {
391
298
  params.push(v);
392
299
  return `$${params.length}`;
393
300
  };
394
- // 🔴 #292 义务 2:placed 会话不上宿主枚举面(TiDB 孪生逐字同义,理由见那里)。
395
301
  const where = ["sm.placement_kind IS NULL"];
396
- // [4198] 窄互认第二臂的第三条件(TiDB 孪生逐字同义):`includeUnowned` ⇒「本人的 ∪ 无主的」。
397
302
  if (opts.owner)
398
303
  where.push(opts.includeUnowned ? `(sm.owner = ${ph(opts.owner)} OR sm.owner IS NULL)` : `sm.owner = ${ph(opts.owner)}`);
399
- // `?q=` — the PG twin of the TiDB EXISTS predicate (LOWER-LIKE + ESCAPE, dialect-mirrored).
400
304
  if (opts.q)
401
305
  where.push(`EXISTS (SELECT 1 FROM task_run tr WHERE tr.session_id = sm.session_id AND LOWER(tr.objective_preview) LIKE LOWER(${ph(`%${escapeLike(opts.q)}%`)}) ESCAPE '\\')`);
402
306
  if (opts.cursor) {
@@ -409,7 +313,7 @@ export class PgSessionStore {
409
313
  " COALESCE(r.run_count, 0) AS run_count, latest.objective_preview, latest.status, latest.task_id AS last_run_id " +
410
314
  "FROM session_meta sm " +
411
315
  "LEFT JOIN (SELECT session_id, COUNT(*) AS run_count FROM task_run GROUP BY session_id) r ON r.session_id = sm.session_id " +
412
- "LEFT JOIN (SELECT DISTINCT ON (session_id) session_id, objective_preview, status, task_id FROM task_run " + // K-5c: latest run's task_id
316
+ "LEFT JOIN (SELECT DISTINCT ON (session_id) session_id, objective_preview, status, task_id FROM task_run " +
413
317
  " ORDER BY session_id, created_at DESC, task_id DESC) latest ON latest.session_id = sm.session_id " +
414
318
  (where.length ? "WHERE " + where.join(" AND ") + " " : "") +
415
319
  `ORDER BY sm.updated_at DESC, sm.session_id DESC LIMIT ${ph(opts.limit)}`;
@@ -422,11 +326,10 @@ export class PgSessionStore {
422
326
  runCount: Number(r.run_count),
423
327
  objectivePreview: r.objective_preview ?? null,
424
328
  lastStatus: r.status == null ? "" : String(r.status),
425
- lastRunId: r.last_run_id ?? null, // K-5c: latest run id = the live-tail re-attach anchor (null if no runs)
329
+ lastRunId: r.last_run_id ?? null,
426
330
  title: r.title ?? null,
427
331
  }));
428
332
  }
429
- /** @see TiDBSessionStore.probeTitle — the PG twin. */
430
333
  async probeTitle(sessionId) {
431
334
  const res = await this.pool.query("SELECT title FROM session_meta WHERE session_id = $1", [sessionId]);
432
335
  const row = res.rows[0];
@@ -434,19 +337,15 @@ export class PgSessionStore {
434
337
  return "none";
435
338
  return row.title == null ? "untitled" : "titled";
436
339
  }
437
- /** @see TiDBSessionStore.setTitleIfNull — the PG twin (write-once auto-title). */
438
340
  async setTitleIfNull(sessionId, title) {
439
341
  const res = await this.pool.query("UPDATE session_meta SET title = $1 WHERE session_id = $2 AND title IS NULL", [title, sessionId]);
440
342
  return (res.rowCount ?? 0) > 0;
441
343
  }
442
- /** @see TiDBSessionStore.fork — copy the WHOLE durable history (full session_event log, compaction floor bypassed,
443
- * NOT wake's bounded tail) to a NEW uuidv7 session, re-seq dense from 0, ids/parents verbatim, owner=caller. */
444
344
  async fork(sourceId, owner) {
445
345
  const metaRes = await this.pool.query("SELECT leaf_id FROM session_meta WHERE session_id = $1", [sourceId]);
446
346
  if (!metaRes.rows[0])
447
347
  return null;
448
348
  const leafId = metaRes.rows[0].leaf_id ?? null;
449
- // FULL durable history (compaction floor bypassed) — every entry, oldest first. node-pg parses jsonb → object.
450
349
  const eventRes = await this.pool.query("SELECT entry_id, parent_id, type, payload, ts FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [sourceId]);
451
350
  const rows = eventRes.rows;
452
351
  const newId = uuidv7();
@@ -456,8 +355,6 @@ export class PgSessionStore {
456
355
  await conn.query("BEGIN");
457
356
  for (let seq = 0; seq < rows.length; seq++) {
458
357
  const r = rows[seq];
459
- // Copy the payload blob VERBATIM; re-stringify only an object so $6::jsonb binds a JSON string (node-pg
460
- // returns jsonb already parsed, so a string here means it was stored stringly — rare; pass either through).
461
358
  const payload = typeof r.payload === "string" ? r.payload : pgSafeJsonStringify(r.payload);
462
359
  await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [newId, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
463
360
  }
@@ -470,7 +367,6 @@ export class PgSessionStore {
470
367
  await conn.query("ROLLBACK");
471
368
  }
472
369
  catch {
473
- /* ignore */
474
370
  }
475
371
  throw err instanceof SessionError ? err : new SessionError("storage", `fork of session ${sourceId} failed`, err instanceof Error ? err : undefined);
476
372
  }
@@ -478,60 +374,44 @@ export class PgSessionStore {
478
374
  conn.release();
479
375
  }
480
376
  }
481
- /** @see TiDBSessionStore.exportEntries — 2c session-sync EXPORT: the FULL durable session_event log (compaction floor
482
- * bypassed, NOT wake's tail) as `SessionTreeEntry[]`, oldest-first, verbatim. null if the source is unknown. */
483
377
  async exportEntries(sessionId) {
484
378
  const metaRes = await this.pool.query("SELECT 1 FROM session_meta WHERE session_id = $1", [sessionId]);
485
379
  if (metaRes.rows.length === 0)
486
380
  return null;
487
381
  const eventRes = await this.pool.query("SELECT payload FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [sessionId]);
488
- // node-pg parses jsonb → object already (parseEntry passes objects through; a string would be rare/legacy).
489
382
  return eventRes.rows.map((r) => parseEntry(r.payload));
490
383
  }
491
- /** @see TiDBSessionStore.listEntryIds — 2c P1d-α (PULL streaming): the IDS-ONLY projection of the full durable log,
492
- * oldest-first (`SELECT entry_id … ORDER BY seq ASC`), no payloads. null if the source is unknown. */
493
384
  async listEntryIds(sessionId) {
494
385
  const metaRes = await this.pool.query("SELECT 1 FROM session_meta WHERE session_id = $1", [sessionId]);
495
386
  if (metaRes.rows.length === 0)
496
- return null; // no such session (mirror exportEntries' null)
387
+ return null;
497
388
  const res = await this.pool.query("SELECT entry_id FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [sessionId]);
498
389
  return res.rows.map((r) => String(r.entry_id));
499
390
  }
500
- /** @see TiDBSessionStore.exportEntriesStream — 2c P1d-α (PULL streaming): KEYSET-paged async generator over the full
501
- * durable log (node-pg also buffers a plain SELECT → page it). `… WHERE seq > cursor ORDER BY seq ASC LIMIT K`
502
- * (K = batchSize ?? 500), advancing the cursor to the last seq each batch, ONE pooled `pool.query` per batch (no
503
- * held connection across the stream → a slow consumer can't pin one). Start cursor = afterSeq ?? -1 (0-based dense).
504
- * Resolves to null (NOT an empty iterable) for a missing session — eager `session_meta` probe (BIGINT seq comes
505
- * back as a STRING in node-pg → always Number(...) the cursor advance). */
506
391
  async exportEntriesStream(sessionId, opts) {
507
392
  const metaRes = await this.pool.query("SELECT 1 FROM session_meta WHERE session_id = $1", [sessionId]);
508
393
  if (metaRes.rows.length === 0)
509
- return null; // no such session — NOT an empty iterable
394
+ return null;
510
395
  const pool = this.pool;
511
396
  const batchSize = opts?.batchSize ?? 500;
512
397
  async function* gen() {
513
- let cursor = opts?.afterSeq ?? -1; // seq is 0-based dense → > -1 starts at seq 0
398
+ let cursor = opts?.afterSeq ?? -1;
514
399
  for (;;) {
515
400
  const res = await pool.query("SELECT seq, payload FROM session_event WHERE session_id = $1 AND seq > $2 ORDER BY seq ASC LIMIT $3", [sessionId, cursor, batchSize]);
516
401
  for (const r of res.rows)
517
402
  yield parseEntry(r.payload);
518
403
  if (res.rows.length < batchSize)
519
- return; // short page = end of log
520
- cursor = Number(res.rows[res.rows.length - 1].seq); // BIGINT seq → STRING in node-pg → Number(...)
404
+ return;
405
+ cursor = Number(res.rows[res.rows.length - 1].seq);
521
406
  }
522
407
  }
523
408
  return gen();
524
409
  }
525
- /** @see TiDBSessionStore.importEntries — 2c session-sync IMPORT: core `validateEntriesForImport` gate FIRST (fail-closed),
526
- * then persist its verbatim output (re-seq dense from 0, ids/parents/payload/ts unchanged) in ONE txn with
527
- * session_meta LAST as the commit point; owner = the RE-STAMPED authenticated principal (never the bundle's). */
528
410
  async importEntries(sessionId, owner, entries) {
529
- const validated = validateEntriesForImport(entries); // fail-closed invariant gate BEFORE any write
411
+ const validated = validateEntriesForImport(entries);
530
412
  const conn = await this.pool.connect();
531
413
  try {
532
414
  await conn.query("BEGIN");
533
- // Shared insert body (entries re-seq dense from 0 + session_meta LAST = commit point). Plain INSERT (fresh-only);
534
- // replaceEntries is the idempotent-over-existing path (DELETEs first inside the same txn). @see TiDB twin.
535
415
  await this.insertValidatedLog(conn, sessionId, owner, validated);
536
416
  await conn.query("COMMIT");
537
417
  }
@@ -540,37 +420,28 @@ export class PgSessionStore {
540
420
  await conn.query("ROLLBACK");
541
421
  }
542
422
  catch {
543
- /* ignore */
544
423
  }
545
424
  if (err instanceof SessionError)
546
- throw err; // invalid_session (gate) / conflict — pass through
425
+ throw err;
547
426
  throw new SessionError("storage", `import of session ${sessionId} failed`, err instanceof Error ? err : undefined);
548
427
  }
549
428
  finally {
550
429
  conn.release();
551
430
  }
552
431
  }
553
- /** @see TiDBSessionStore.replaceEntries — 2c (§7/§8) IDEMPOTENT REPLACE: core `validateEntriesForImport` gate FIRST
554
- * (fail-closed), then DELETE events + the owner-guarded meta delete (`owner IS NOT DISTINCT FROM $n` = the PG `<=>`,
555
- * defense-in-depth), then re-insert via the SHARED body (entries + session_meta LAST as the commit point), ALL in
556
- * ONE txn. Safe to run OVER an existing session (fast-forward / §8 retry) — unlike importEntries' fresh-only INSERT. */
557
432
  async replaceEntries(sessionId, owner, entries) {
558
- await this.assertNotPlaced(sessionId, "replace"); // 快路(事务外,零工拒);真门在事务内(TiDB 孪生同款注)
559
- const validated = validateEntriesForImport(entries); // fail-closed gate BEFORE any write (mirrors importEntries)
433
+ await this.assertNotPlaced(sessionId, "replace");
434
+ const validated = validateEntriesForImport(entries);
560
435
  const conn = await this.pool.connect();
561
436
  try {
562
437
  await conn.query("BEGIN");
563
- // #292(codex R1-[high] 同族窗):事务内锁读 + DELETE 的 `placement_kind IS NULL` 硬条件
564
- // (TiDB 孪生逐字同义 —— placed 行 owner 恒 NULL,而 replace(owner=null) 的属主门恰好匹配它)。
565
438
  const placedRes = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = $1 FOR UPDATE", [sessionId]);
566
439
  if (placedRes.rows[0]?.placement_kind != null) {
567
440
  await conn.query("ROLLBACK");
568
441
  throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placedRes.rows[0].placement_kind} placement partition — a session-sync replace cannot overwrite a delegation transcript`);
569
442
  }
570
- // Purge first (idempotent: a missing session deletes 0 rows). Events, then the owner-guarded meta delete.
571
443
  await conn.query("DELETE FROM session_event WHERE session_id = $1", [sessionId]);
572
444
  await conn.query("DELETE FROM session_meta WHERE session_id = $1 AND owner IS NOT DISTINCT FROM $2 AND placement_kind IS NULL", [sessionId, owner]);
573
- // …then re-insert the validated log (entries + meta-as-commit-point) via the SHARED body.
574
445
  await this.insertValidatedLog(conn, sessionId, owner, validated);
575
446
  await conn.query("COMMIT");
576
447
  }
@@ -579,23 +450,19 @@ export class PgSessionStore {
579
450
  await conn.query("ROLLBACK");
580
451
  }
581
452
  catch {
582
- /* ignore */
583
453
  }
584
454
  if (err instanceof SessionError)
585
- throw err; // invalid_session (gate) / conflict — pass through
455
+ throw err;
586
456
  throw new SessionError("storage", `replace of session ${sessionId} failed`, err instanceof Error ? err : undefined);
587
457
  }
588
458
  finally {
589
459
  conn.release();
590
460
  }
591
461
  }
592
- /** @see TiDBSessionStore.insertValidatedLog — shared insert half of importEntries / replaceEntries (ONE source, no
593
- * drift): entries re-seq dense from 0 (ids/parents/payload/ts verbatim), THEN session_meta LAST as the commit point.
594
- * Runs inside the caller's open txn (`conn` already in BEGIN); the caller owns COMMIT/ROLLBACK + the validate gate. */
595
462
  async insertValidatedLog(conn, sessionId, owner, validated) {
596
463
  let leafId = null;
597
464
  for (const e of validated)
598
- leafId = leafIdAfterEntry(e); // replayed leaf — core's exported helper (no service copy)
465
+ leafId = leafIdAfterEntry(e);
599
466
  const now = new Date();
600
467
  for (let seq = 0; seq < validated.length; seq++) {
601
468
  const e = validated[seq];
@@ -603,21 +470,13 @@ export class PgSessionStore {
603
470
  }
604
471
  await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES ($1,$2,$3,$4,$5,$6)", [sessionId, owner, now, now, leafId, validated.length]);
605
472
  }
606
- /** @see TiDBSessionStore.beginImportStaging — 2c P1d-β PUSH staging: stream batches into the shadow id
607
- * `${realSessionId}#stg-${token}` (route-minted token), then commit (atomic swap) / abort. */
608
473
  beginImportStaging(realSessionId, token) {
609
474
  return new PgStagingHandle(this.pool, realSessionId, stagingIdFor(realSessionId, token));
610
475
  }
611
- /** @see TiDBSessionStore.readStagedEntries — 2c P1d-β staged-row inspection: the staged rows under `stagingId`, oldest-
612
- * first, no session_meta probe (a staging id has no meta row). No longer the import gate (the per-line
613
- * StreamingImportValidator validates during the stream); a tests/diagnostics seam. Returns [] for an empty/unknown id. */
614
476
  async readStagedEntries(stagingId) {
615
477
  const res = await this.pool.query("SELECT payload FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [stagingId]);
616
478
  return res.rows.map((r) => parseEntry(r.payload));
617
479
  }
618
- /** @see TiDBSessionStore.sweepStagingSessions — reaper for abandoned staging sessions: DELETE session_event rows
619
- * under a `%#stg-%` id with NO session_meta whose oldest row is older than the grace window (in-flight stays fresh).
620
- * Returns the affected row count. Driven by main.ts's reaper. */
621
480
  async sweepStagingSessions() {
622
481
  const cutoff = new Date(Date.now() - STAGING_GC_GRACE_MS);
623
482
  const cands = await this.pool.query("SELECT se.session_id FROM session_event se LEFT JOIN session_meta sm ON sm.session_id = se.session_id " +
@@ -630,9 +489,6 @@ export class PgSessionStore {
630
489
  }
631
490
  return removed;
632
491
  }
633
- /** @see TiDBSessionStore.deleteSession — purge session_meta + session_event; idempotent (false if absent).
634
- * Owner-guarded on session_meta.owner (`IS NOT DISTINCT FROM` = the PG `<=>`); the event purge is gated on the
635
- * owner-matched meta delete (session_event has no owner column). */
636
492
  async deleteSession(sessionId, owner) {
637
493
  const conn = await this.pool.connect();
638
494
  try {
@@ -648,7 +504,6 @@ export class PgSessionStore {
648
504
  await conn.query("ROLLBACK");
649
505
  }
650
506
  catch {
651
- /* ignore */
652
507
  }
653
508
  throw err;
654
509
  }
@@ -663,28 +518,18 @@ export class PgSessionStore {
663
518
  this.pending.clear();
664
519
  }
665
520
  }
666
- /**
667
- * 2c session-sync P1d-β — the PG twin of {@link TiDBStagingHandle} (behaviourally identical; only the SQL dialect
668
- * differs: `$n` placeholders, `$n::jsonb`, `ON CONFLICT (session_id, entry_id) DO NOTHING`, rowCount, FOR UPDATE,
669
- * BEGIN/COMMIT handle). Streams entry batches into a shadow session_event partition, then atomically RENAMEs them
670
- * onto the real session id in ONE txn with session_meta LAST = the commit point.
671
- */
672
521
  class PgStagingHandle {
673
522
  pool;
674
523
  realSessionId;
675
524
  stagingId;
676
525
  nextSeq = 0;
677
- seen = new Set(); // staged ids (resume dedup → dense seqs); @see TiDBStagingHandle
526
+ seen = new Set();
678
527
  constructor(pool, realSessionId, stagingId) {
679
528
  this.pool = pool;
680
529
  this.realSessionId = realSessionId;
681
530
  this.stagingId = stagingId;
682
531
  }
683
- /** @see TiDBStagingHandle.appendBatch — DENSE re-seq (skip already-staged ids), idempotent ON CONFLICT (session_id,
684
- * entry_id) DO NOTHING, one txn/batch. */
685
532
  async appendBatch(entries) {
686
- // ONE multi-row INSERT per batch (bounded) — @see TiDBStagingHandle.appendBatch. Build $-placeholders for the NEW
687
- // rows (skip already-staged ids → dense seqs), ON CONFLICT (session_id, entry_id) DO NOTHING as the durable backstop.
688
533
  const params = [];
689
534
  const tuples = [];
690
535
  for (const e of entries) {
@@ -692,7 +537,7 @@ class PgStagingHandle {
692
537
  continue;
693
538
  const b = params.length;
694
539
  tuples.push(`($${b + 1},$${b + 2},$${b + 3},$${b + 4},$${b + 5},$${b + 6}::jsonb,$${b + 7})`);
695
- params.push(this.stagingId, this.nextSeq, e.id, e.parentId ?? null, e.type, pgSafeJsonStringify(e), new Date() /* A-1: row-insert time, not source-history — reaper liveness; payload keeps e.timestamp */);
540
+ params.push(this.stagingId, this.nextSeq, e.id, e.parentId ?? null, e.type, pgSafeJsonStringify(e), new Date());
696
541
  this.seen.add(e.id);
697
542
  this.nextSeq++;
698
543
  }
@@ -706,15 +551,12 @@ class PgStagingHandle {
706
551
  throw err instanceof SessionError ? err : new SessionError("storage", `staged append for ${this.realSessionId} failed`, err instanceof Error ? err : undefined);
707
552
  }
708
553
  }
709
- /** @see TiDBStagingHandle.commit — the atomic swap: in-commit re-classify (FOR UPDATE) → purge dst → rename
710
- * staging→real → session_meta LAST. Throws SyncConflictError on an unconsented fork/stale flip. */
711
554
  async commit(owner, opts) {
712
555
  const conn = await this.pool.connect();
713
556
  try {
714
557
  await conn.query("BEGIN");
715
558
  const staged = await conn.query("SELECT entry_id FROM session_event WHERE session_id = $1 ORDER BY seq ASC FOR UPDATE", [this.stagingId]);
716
559
  const dst = await conn.query("SELECT entry_id FROM session_event WHERE session_id = $1 ORDER BY seq ASC FOR UPDATE", [this.realSessionId]);
717
- // `placement_kind` 捎带在存在性探针上(#292:placed 目的地拒整段换装;TiDB 孪生同形)。
718
560
  const dstMeta = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = $1", [this.realSessionId]);
719
561
  if (dstMeta.rows[0]?.placement_kind != null) {
720
562
  await conn.query("ROLLBACK");
@@ -727,8 +569,6 @@ class PgStagingHandle {
727
569
  await conn.query("ROLLBACK");
728
570
  throw new SyncConflictError(this.realSessionId, rel);
729
571
  }
730
- // 🔴 [2557] A5/F2(2026-08-05):fast_forward 的**载荷**判据(tidb twin 同注)——共享段分叉 ⇒ 409,
731
- // 除非显式 overwrite-dst;载荷只在这条臂上读,行已被 FOR UPDATE 锁住,同 txn 重读是一致读。
732
572
  if (rel.relation === "fast_forward" && opts?.resolution !== "overwrite-dst" && dstIds !== null && dstIds.length > 0) {
733
573
  const dstFull = await conn.query("SELECT payload FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [this.realSessionId]);
734
574
  const stagedFull = await conn.query("SELECT entry_id, payload FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [this.stagingId]);
@@ -750,9 +590,6 @@ class PgStagingHandle {
750
590
  const now = new Date();
751
591
  await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES ($1,$2,$3,$4,$5,$6) " +
752
592
  "ON CONFLICT (session_id) DO UPDATE SET owner = EXCLUDED.owner, updated_at = EXCLUDED.updated_at, leaf_id = EXCLUDED.leaf_id, leaf_seq = EXCLUDED.leaf_seq", [this.realSessionId, owner, now, now, leafId, srcIds.length]);
753
- // 🔴 #292(codex R1-[high]):**提交前的最后一道门** —— upsert 之后这一行必被本事务锁住,故这次
754
- // 复读是权威的;它关的是「探针时行还不存在、并发方随后把同 id 铸成 placed」那半个窗(PG 的
755
- // FOR UPDATE 对缺席行不锁任何东西)。理由与失败经济学逐字见 TiDB 孪生同处的注。
756
593
  const afterMeta = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = $1", [this.realSessionId]);
757
594
  if (afterMeta.rows[0]?.placement_kind != null) {
758
595
  await conn.query("ROLLBACK");
@@ -765,7 +602,7 @@ class PgStagingHandle {
765
602
  try {
766
603
  await conn.query("ROLLBACK");
767
604
  }
768
- catch { /* ignore */ }
605
+ catch { }
769
606
  if (err instanceof SyncConflictError || err instanceof SessionError)
770
607
  throw err;
771
608
  throw new SessionError("storage", `staged commit for ${this.realSessionId} failed`, err instanceof Error ? err : undefined);
@@ -774,7 +611,6 @@ class PgStagingHandle {
774
611
  conn.release();
775
612
  }
776
613
  }
777
- /** @see TiDBStagingHandle.abort — DELETE the staging rows. Idempotent. */
778
614
  async abort() {
779
615
  await this.pool.query("DELETE FROM session_event WHERE session_id = $1", [this.stagingId]);
780
616
  }