@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,97 +1,40 @@
1
- /** The staging-id format: a real session id with a `#stg-${token}` suffix. The `#` is illegal in a uuidv7, so a
2
- * staging id can never collide with a real session id (the orphan invariant) and is recognizable for the reaper. */
3
1
  export function stagingIdFor(realSessionId, token) {
4
2
  return `${realSessionId}#stg-${token}`;
5
3
  }
6
- /** The substring marker every staging id carries (the reaper's `LIKE '%#stg-%'` selector). */
7
4
  export const STAGING_ID_MARKER = "#stg-";
8
- /** Recover the real session id from a staging id (everything before the `#stg-` marker), or null if not a staging id. */
9
5
  export function realIdOfStaging(stagingId) {
10
6
  const i = stagingId.indexOf(STAGING_ID_MARKER);
11
7
  return i < 0 ? null : stagingId.slice(0, i);
12
8
  }
13
- /**
14
- * Staging-row reaper grace window — a staging session's rows are GC'able only if it has NO `session_meta` (never
15
- * committed) AND its OLDEST row is older than this (an in-flight stream keeps appending fresh rows → safe).
16
- * Mirrors the file-snapshot store's `BLOB_GC_GRACE_MS` discipline (an in-flight import must never be reaped
17
- * mid-stream).
18
- *
19
- * 🔴 住在 kernel 里而不是任一个店里(2026-08-08,#176 二轮扫描 finding):TiDB 与 PG 两个孪生店原先**各
20
- * 抄一份**同值常量,PG 那份的注释甚至自称「twin of the TiDB store's STAGING_GC_GRACE_MS」——明知孪生却
21
- * 不共享。单边改值的后果是两个后端对「同一个中断的导入还能不能收」判定分家,而两边都按自己的常量静默
22
- * 正确工作。这里是两个店**本就都 import** 的那个叶子模块(店契约纪律:适配器不许 import 自己的孪生,
23
- * 共享常量必须落在中立处),同族先例见 `checkpoint-store-sql.ts` 的 `export const TERMINAL_GRACE_MS`。
24
- */
25
9
  export const STAGING_GC_GRACE_MS = 3_600_000;
26
- /**
27
- * §7 — classify how importing `srcEntries` into a destination holding `dstEntries` would relate the two logs. PURE
28
- * (no I/O); the §7 fork detector. The comparison is over the ENTRY-ID SETS, not leaf_id:
29
- *
30
- * Entry ids are copied VERBATIM across backends (fork/import re-seq only `seq`; id/parentId/payload are byte-identical
31
- * — see tidb-session-store fork/importEntries), so a set comparison of ids is the SOUND divergence test §7 demands. It
32
- * fixes v1's leaf_id-only check, which was unsound BOTH ways:
33
- * - FALSE NEGATIVE (the dangerous one): a cross-end rewind moves `LeafEntry.targetId` back onto a shared OLD entry id,
34
- * so two genuinely-forked logs report the SAME leaf_id → v1 would silently overwrite = data loss (exactly the 2c
35
- * use case). Set comparison sees each side's exclusive ids and reports `fork`.
36
- * - FALSE POSITIVE: a clean fast-forward ends on a NEW leaf id ≠ the dst's leaf → v1 flags a (non-)conflict. Set
37
- * comparison sees dst ⊊ src and reports `fast_forward` (safe).
38
- *
39
- * `commonAncestor` for a fork = the entry id present in BOTH logs that appears LAST in `srcEntries` oldest-first order
40
- * (the deepest shared appended entry = the divergence point), or `null` if the logs share no entry at all.
41
- */
42
10
  export function classifySyncRelationship(srcEntries, dstEntries) {
43
- // Map entries → their ids (the only thing the comparison reads) and delegate to the id-set classifier — ONE source
44
- // of the §7 relation logic. (P1d's HTTP plan route, where one peer is REMOTE and sends only an entry-id LIST, calls
45
- // classifySyncRelationshipByIds DIRECTLY; this entries-typed entry point is the same logic with a nicer call shape.)
46
11
  return classifySyncRelationshipByIds(srcEntries.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
47
12
  }
48
- /**
49
- * §7 — the SAME relation classifier as {@link classifySyncRelationship}, over entry-id LISTS (oldest-first) instead of
50
- * full entries. This is the canonical implementation the entries-typed wrapper delegates to. It exists in its own right
51
- * for the P1d HTTP `/sync/plan` route: the two peers are different PROCESSES (the local shell + the cloud), so the
52
- * planning peer holds only the OTHER end's entry-id list (not its `SessionTreeEntry[]`) — the relation is decidable from
53
- * the ids alone (ids are copied VERBATIM across backends, so a set comparison of ids is the sound divergence test §7
54
- * demands; see the wrapper's doc-comment for why leaf_id was unsound). `srcIds`/`dstIds` are OLDEST-FIRST so the
55
- * fork `commonAncestor` (the deepest shared id) and the ordered exclusive/new lists are derived correctly.
56
- */
57
13
  export function classifySyncRelationshipByIds(srcIds, dstIds) {
58
- // dst absent OR empty ⇒ nothing at the destination to relate to / lose → an unconditional fresh import.
59
14
  if (dstIds === null || dstIds.length === 0)
60
15
  return { relation: "fresh" };
61
16
  const srcSet = new Set(srcIds);
62
17
  const dstSet = new Set(dstIds);
63
- // identical: same cardinality AND every src id is in dst (⟹ the sets are equal) → already present & equal (no-op).
64
18
  if (srcSet.size === dstSet.size && [...srcSet].every((id) => dstSet.has(id)))
65
19
  return { relation: "identical" };
66
- const allDstInSrc = [...dstSet].every((id) => srcSet.has(id)); // dst ⊆ src
67
- const allSrcInDst = [...srcSet].every((id) => dstSet.has(id)); // src ⊆ dst
68
- // fast_forward: dst ⊊ src (src strictly ahead — a clean append) → safe to apply. newEntryIds = the src ids the dst
69
- // lacks, in SRC oldest-first order (the appended tail), so the caller can surface "what would be added".
20
+ const allDstInSrc = [...dstSet].every((id) => srcSet.has(id));
21
+ const allSrcInDst = [...srcSet].every((id) => dstSet.has(id));
70
22
  if (allDstInSrc && !allSrcInDst) {
71
23
  const newEntryIds = srcIds.filter((id) => !dstSet.has(id));
72
24
  return { relation: "fast_forward", newEntryIds };
73
25
  }
74
- // stale: src ⊊ dst (src strictly behind) → applying would DROP the dst entries src never saw = data loss. dstAheadBy
75
- // = the dst ids src lacks (dst order). Refused as a conflict unless the caller explicitly resolves overwrite-dst.
76
26
  if (allSrcInDst && !allDstInSrc) {
77
27
  const dstAheadBy = dstIds.filter((id) => !srcSet.has(id));
78
28
  return { relation: "stale", dstAheadBy };
79
29
  }
80
- // fork: neither set contains the other ⇒ each side has ≥1 exclusive entry (true divergence). The common ancestor is
81
- // the deepest shared entry = the LAST id in src oldest-first order that the dst also holds (null if no shared id).
82
30
  const srcExclusive = srcIds.filter((id) => !dstSet.has(id));
83
31
  const dstExclusive = dstIds.filter((id) => !srcSet.has(id));
84
32
  let commonAncestor = null;
85
33
  for (const id of srcIds)
86
34
  if (dstSet.has(id))
87
- commonAncestor = id; // last shared id in src oldest-first order
35
+ commonAncestor = id;
88
36
  return { relation: "fork", commonAncestor, srcExclusive, dstExclusive };
89
37
  }
90
- /**
91
- * §7 — a typed conflict the route maps to HTTP 409: an import that WOULD lose destination history (a `fork` or a
92
- * `stale` source) and the caller did NOT pass an explicit `{ resolution: "overwrite-dst" }`. Carries the classified
93
- * relation so the route/shell can surface the exclusive entry sets to the user for a keep-local / keep-cloud decision.
94
- */
95
38
  export class SyncConflictError extends Error {
96
39
  sessionId;
97
40
  relation;
@@ -1,56 +1,32 @@
1
1
  import { classifySyncRelationship } from "./session-sync-kernel.js";
2
2
  import { normalizeRules, stripRev } from "@sema-agent/core";
3
- /** Single-point capability probe (design/158 R7): does `fs` carry the optional `importManifest` snapshot-import
4
- * seam? Returns the bound method (never a naked function reference — `this` inside `importManifest` must resolve
5
- * to `fs`, exactly like `asServiceDeps`'s other capability-probe-then-bind authorities) or `undefined` when the
6
- * seam is absent (an older/in-memory dev store). Callers gate on the RETURN VALUE, not a separate `typeof` check,
7
- * so there is exactly one place that knows the shape of the probe. */
8
3
  export function fileSnapshotImportFace(fs) {
9
4
  const candidate = fs?.importManifest;
10
5
  if (typeof candidate !== "function")
11
6
  return undefined;
12
7
  return { importManifest: candidate.bind(fs) };
13
8
  }
14
- /** Narrow `backend.session()` to the optional ownership/export seam (the concrete TiDB/PG/Local stores carry it;
15
- * the bare in-memory dev store doesn't → the methods are optional, exactly like the fork route narrows). */
16
9
  function ownerAware(backend) {
17
10
  return backend.session();
18
11
  }
19
- /**
20
- * P1d-α (PULL streaming, §15) — EXPORT a session's portable state as a {@link SessionManifest} (entries LIFTED OFF
21
- * the wire), so it does NOT load entry payloads: it reads the
22
- * IDS-only projection via `listEntryIds` (`null` ⇒ no such session ⇒ this returns `null`), counts them, reads the
23
- * current `leafId`, and builds snapshots/policy/anchors. The paired NDJSON
24
- * `GET /sync/entries` route streams the entry payloads separately (keyset-paged → bounded memory), so a 1M-entry
25
- * session's manifest stays small (ids + counts + the snapshot/policy/anchor metadata).
26
- *
27
- * The OWNER is NOT exported (import re-stamps it, §9). The owner-scope GATE is the CALLER's (route P1d): this returns
28
- * `null` ONLY for "session does not exist". (Named exportSessionManifest, NOT exportManifest, to avoid the collision
29
- * with `FileSnapshotStore.exportManifest`.)
30
- */
31
12
  export async function exportSessionManifest(sessionId, srcBackend) {
32
13
  const session = ownerAware(srcBackend);
33
- // entry IDS FIRST — null ⇒ no such session ⇒ no manifest (the route maps this to 404). A backend whose session store
34
- // lacks the seam can't be a sync source (older/in-memory dev store) → treat as nothing to export.
35
14
  const listEntryIds = session.listEntryIds?.bind(session);
36
15
  if (!listEntryIds)
37
16
  return null;
38
17
  const entryIds = await listEntryIds(sessionId);
39
18
  if (entryIds === null)
40
- return null; // session does not exist
41
- // leafId — the session's current leaf (cache-bypassing single read). Absent seam / no leaf ⇒ null.
19
+ return null;
42
20
  const getLeafId = session.getLeafId?.bind(session);
43
21
  const leafId = getLeafId ? await getLeafId(sessionId) : null;
44
- // snapshots / policy / anchors — no entry payloads loaded anywhere here.
45
22
  const fs = srcBackend.fileSnapshot();
46
23
  const snapshots = [];
47
- // [2373]B-5b:core 接口 listKeys 必填(恒真半支删);exportManifest? 可选——探测只留真可选的那半。
48
24
  if (typeof fs.exportManifest === "function") {
49
25
  const keys = await fs.listKeys(sessionId);
50
26
  for (const key of keys) {
51
27
  const manifest = await fs.exportManifest(sessionId, key);
52
28
  if (manifest === null)
53
- continue; // raced reap between listKeys and exportManifest — skip (never an empty snapshot)
29
+ continue;
54
30
  snapshots.push({ key, manifest: [...manifest] });
55
31
  }
56
32
  }
@@ -59,10 +35,7 @@ export async function exportSessionManifest(sessionId, srcBackend) {
59
35
  ? await policyStore.listBySession(sessionId)
60
36
  : [];
61
37
  const anchorStore = srcBackend.resumeAnchor();
62
- // [2373]B-5a:ResumeAnchorStore 是三实现的闭合联合、listBySession 全必填——feature-detect 恒真,
63
- // `: []` 臂不可达([2195] deleteBySession 同判据,那轮漏了这对双胞胎)。直调。
64
38
  const anchors = await anchorStore.listBySession(sessionId);
65
- // getBlob — bound to the SOURCE snapshot store (content-addressed, lazy).
66
39
  const getBlobFn = fs.getBlob?.bind(fs);
67
40
  const getBlob = getBlobFn ? (hash) => getBlobFn(hash) : async () => undefined;
68
41
  return {
@@ -70,37 +43,19 @@ export async function exportSessionManifest(sessionId, srcBackend) {
70
43
  getBlob,
71
44
  };
72
45
  }
73
- /**
74
- * §7 DRY-RUN — classify what importing `sessionId` from `srcBackend` into `dstBackend` WOULD do, WITHOUT writing
75
- * anything (the shell can present a keep-local / keep-cloud / fork-new choice up front).
76
- *
77
- * Returns `null` when the SOURCE session does not exist (no bundle to plan); otherwise the {@link SyncRelation}
78
- * between the source's full durable log and the destination's current log (a `null` dst log ⇒ `fresh`). No writes.
79
- */
80
46
  export async function planSync(sessionId, srcBackend, dstBackend) {
81
47
  const srcEntries = (await ownerAware(srcBackend).exportEntries?.(sessionId)) ?? null;
82
48
  if (srcEntries === null)
83
- return null; // no such source session — nothing to plan (the route maps this to 404)
49
+ return null;
84
50
  const dstEntries = (await ownerAware(dstBackend).exportEntries?.(sessionId)) ?? null;
85
51
  return classifySyncRelationship(srcEntries, dstEntries);
86
52
  }
87
- // ═══ #137 —— 死码 importSession 独有保护的活路由移植(910c524 顶注 TOMBSTONE 两缺口的修复面)═══
88
- // 本段是「测红复现(fanout-characterization #137 L1/L2)→ 修绿」的绿半场;顶注 TOMBSTONE 的
89
- // 「NO live-route equivalent」声明随本批作废(保留原文供考古,状态以本段+路由接线为准)。
90
- /** #137 ①(死码 A6 修的移植):同内容判据 —— 比较「归一化、去 rev」后的字节形。putRules 每次
91
- * 无条件写都 rev+1,而 rev 是 operator 乐观锁的 CAS 令牌(`PutRulesOptions.expectedRev`),
92
- * 幂等重试/无害重复同步不该把在途 CAS 打失败。归一化用 core 的 `normalizeRules`(store 落盘
93
- * 前走同一函数 ⇒ 两侧同坐标);`stripRev` 去掉记录内嵌的 rev(`listBySession` 的 rules 含 rev,
94
- * 它不是内容)。 */
95
53
  export function sameRulesContent(stored, incoming) {
96
54
  if (stored === null)
97
55
  return false;
98
56
  const norm = (r) => JSON.stringify(normalizeRules(stripRev(r)));
99
57
  return norm(stored) === norm(incoming);
100
58
  }
101
- /** #137 ① 的重放腿共享实现:PUSH 路由的两条 policy 重放腿(Phase A identical 短路支 / Phase B
102
- * commit 后支)都从这里走 —— 等内容跳过(rev 零漂移),真变更照常落地(E6 tighten-only 门在
103
- * putRules 内部,此处不重述)。 */
104
59
  export async function replayPolicyRecords(store, sessionId, records, operatorOk) {
105
60
  for (const rec of records) {
106
61
  const cur = await store.getRules(sessionId, rec.principal);
@@ -109,27 +64,9 @@ export async function replayPolicyRecords(store, sessionId, records, operatorOk)
109
64
  await store.putRules(sessionId, rec.principal, rec.rules, { operator: operatorOk });
110
65
  }
111
66
  }
112
- /** #137 ② 能力预检(fail-loud 方向,[2195] 判据:保护型能力缺席=fail-closed):consented
113
- * overwrite-dst 需要 policy 店的 `deleteBySession` purge seam 才能不留混合态;缺席=true,路由在
114
- * commit **之前**用它干净中止(此时零写入落地),wire 拒因字面量归路由所有(api-error-text-freeze
115
- * 锚字面站点)。anchor 店的 deleteBySession 是类型必填、快照店有 core 必填的 `reap` 兜底,都不构成
116
- * 缺席面;唯一可缺的就是 policy 这条(core 接口上是可选方法)。 */
117
67
  export function overwriteWipeIncapable(policyStore) {
118
68
  return policyStore !== undefined && typeof policyStore.deleteBySession !== "function";
119
69
  }
120
- /** #137 ②(死码 ⓪ 擦除步的移植):consented overwrite-dst = keep-source,结果必须是**源端那
121
- * 一份**,不得混成「源端对话 + 目的端遗留 policy/anchor/快照」(混合态比丢历史更危险:悬空
122
- * anchor 指向已 purge 的 entry、被放弃分支的 policy 行继续 gating、共享 key 快照因
123
- * importManifest create-once 永远留着目的端字节)。调用时点=commit(原子 swap)成功**之后**、
124
- * 附随物重放**之前**:死码在任何写入前擦(其 entries 写本身非原子);活路由有真 commit point,
125
- * 擦在 commit 前会在「commit 失败」时留下反向混合态(目的端对话完好、附随物已毁)——方向更坏。
126
- * 诚实残余:进程在 commit 与本函数之间崩溃时,重试落 Phase A identical 支(无 wipe)⇒ 遗留行
127
- * 可存活;该窗口窄于修前世界(修前=遗留行**恒**存活),与 §8「快照重放失败 ⇒ 422 重试自愈」
128
- * 同款接受,记录于此不装没有。
129
- * 各店姿势(死码 verbatim 语义):快照=deleteBySession(SQL 孪生:manifest 即删、blob 走异步
130
- * 孤儿 GC)缺席则 core 必填 `reap(scope, [])`(内容寻址,他 scope 仍引用的 blob 存活);
131
- * anchor=直调(类型必填);policy=deleteBySession(路由已用 {@link overwriteWipeIncapable}
132
- * 预检过,此处缺席=类型外的 JS 层残缺实现,直调抛 TypeError 即 fail-loud)。 */
133
70
  export async function wipeSessionAttendants(stores, sessionId) {
134
71
  const fs = stores.fileSnapshotStore;
135
72
  if (fs !== undefined) {
@@ -1,30 +1,10 @@
1
- /**
2
- * Auto-generated session titles.
3
- *
4
- * SHAPE: fire-and-forget at FIRST task submit for a session (the objective is known then — no need to wait
5
- * for the run to finish, and the web Recents gets the title sooner). Write-once (`setTitleIfNull`, store-side
6
- * `WHERE title IS NULL`) — the USER-facing rename lives BFF-side as a label overlay (user label wins
7
- * over engine title, two layers never fight). Generation = one cheap-model one-liner via the deployment's
8
- * hookLlm face (summarize role → MODEL_CHEAP_ID when configured, else the main model; bounded + timed out).
9
- *
10
- * FAILURE POSTURE: everything is best-effort — a generation/store failure logs and leaves title null (the
11
- * list falls back to objectivePreview). An in-process pending set dedupes concurrent submits on the same
12
- * session; the store's IS NULL guard closes the cross-replica race (double generation costs one extra cheap
13
- * call at worst, and exactly one title lands).
14
- */
15
- export const TITLE_MAX_CHARS = 80; // display budget (column holds 120; leave headroom for future prefixing)
16
- const OBJECTIVE_SNIPPET_CHARS = 600; // prompt input bound — a title needs the gist, not the whole objective
1
+ export const TITLE_MAX_CHARS = 80;
2
+ const OBJECTIVE_SNIPPET_CHARS = 600;
17
3
  const LLM_TIMEOUT_MS = 15_000;
18
- const NO_ROW_RETRY_MS = 20_000; // session_meta register lag grace (MED3) — one in-process retry before releasing
19
- /** 失败日志 per-cause 去重的归一窗口:取 error 文案前 N 字符当 cause 键(错误文案尾部常带
20
- * 会话/请求级变量,前缀足以区分「凭据缺失」vs「超时」这类配置性差异)。 */
4
+ const NO_ROW_RETRY_MS = 20_000;
21
5
  const FAIL_CAUSE_KEY_CHARS = 80;
22
- /** cause 键集合上限——错误文案前缀本身含变量时防无界;满了整体清空,代价只是偶发多一条 warn。 */
23
6
  const FAIL_CAUSE_CAP = 100;
24
- /** Sessions already handled (titled, failed, or in flight) this process — one attempt per session per process.
25
- * Bounded: evict-oldest past the cap (a worker sees a bounded set of NEW sessions between restarts anyway). */
26
7
  const SEEN_CAP = 10_000;
27
- /** Single line, control chars stripped, hard cap; surrounding quotes (a common LLM tic) trimmed. */
28
8
  export function sanitizeTitle(raw) {
29
9
  const oneLine = raw.replace(/[\r\n]+/g, " ").replace(/[\u0000-\u001f\u007f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "").trim();
30
10
  const unquoted = oneLine.replace(/^["'「『]+/, "").replace(/["'」』]+$/, "").trim();
@@ -32,10 +12,6 @@ export function sanitizeTitle(raw) {
32
12
  }
33
13
  export function buildTitlePrompt(objective) {
34
14
  const snippet = objective.length > OBJECTIVE_SNIPPET_CHARS ? `${objective.slice(0, OBJECTIVE_SNIPPET_CHARS)}…` : objective;
35
- // The task text is DATA here, not instructions — fence it (with the closing tag DEFUSED inside the data so
36
- // the objective cannot terminate the fence early) and pin the output contract. A prompt-injection attempt
37
- // can at worst produce a weird title, which the sanitizer bounds to one line; titles are display-only and
38
- // never re-enter any model context.
39
15
  const fenced = snippet.replace(/<(\/?)request>/gi, "<\u200b$1request>");
40
16
  return ("Write a short title (at most 8 words) summarizing the user request below. " +
41
17
  "Reply with ONLY the title on a single line — no quotes, no preamble. " +
@@ -45,12 +21,6 @@ export function buildTitlePrompt(objective) {
45
21
  }
46
22
  export function createSessionTitler(deps) {
47
23
  const seen = new Set();
48
- // [854]⑤ 失败日志锐化:titler 失败本是 fail-soft(标题留空、列表回退 objectivePreview,零用户面),
49
- // 但同一部署级配置性失败(凭据缺失/模型面不可达)会随每次 trial/每个新会话反复触发——per-session
50
- // warn 在评测跑批时是纯噪音。取舍=「首条 warn + 后续同 cause 全 debug」:完全静默会让运维发现不了
51
- // 配置问题(违背 fail-loud),每条都 warn 又淹日志;同 cause 每进程恰好 warn 一次是两全。观测面不靠
52
- // 日志靠 metrics(session_title_total outcome 族照常逐次记账)。cause=error 文案前缀归一(见常量注释);
53
- // 集合挂在 titler 实例上,部署里每进程恰建一只 titler ⇒ 等价 per-process。
54
24
  const warnedCauses = new Set();
55
25
  const logFailure = (sessionId, error) => {
56
26
  const cause = error.slice(0, FAIL_CAUSE_KEY_CHARS);
@@ -69,7 +39,7 @@ export function createSessionTitler(deps) {
69
39
  if (!sessionId || !objective.trim())
70
40
  return;
71
41
  if (seen.has(sessionId))
72
- return; // one attempt per session per process (resubmits/turns don't re-fire)
42
+ return;
73
43
  if (seen.size >= SEEN_CAP) {
74
44
  const oldest = seen.values().next().value;
75
45
  if (oldest !== undefined)
@@ -77,14 +47,12 @@ export function createSessionTitler(deps) {
77
47
  }
78
48
  seen.add(sessionId);
79
49
  void (async () => {
80
- // pre-LLM probe: a point-read is orders cheaper than an LLM call (MED2 restarts/evicted seen/other
81
- // replicas must not re-pay), and it detects the register-lag "no row yet" window (MED3).
82
- let state = await deps.probeTitle(sessionId).catch(() => "untitled"); // probe error ⇒ optimistic (IS NULL still guards)
50
+ let state = await deps.probeTitle(sessionId).catch(() => "untitled");
83
51
  if (state === "none") {
84
52
  await new Promise((r) => setTimeout(r, NO_ROW_RETRY_MS));
85
53
  state = await deps.probeTitle(sessionId).catch(() => "untitled");
86
54
  if (state === "none") {
87
- seen.delete(sessionId); // release: the NEXT submit on this session retries (no permanent miss)
55
+ seen.delete(sessionId);
88
56
  deps.metrics?.inc("session_title_total", { outcome: "row_not_yet" });
89
57
  return;
90
58
  }
@@ -106,8 +74,6 @@ export function createSessionTitler(deps) {
106
74
  }
107
75
  const landed = await deps.setTitleIfNull(sessionId, title);
108
76
  if (!landed) {
109
- // distinguish "another writer titled it" (fine) from "row STILL absent" (UPDATE-0 on a missing row —
110
- // release the seen slot so a later submit retries; never leave the session permanently untitled).
111
77
  const after = await deps.probeTitle(sessionId).catch(() => "titled");
112
78
  if (after === "none")
113
79
  seen.delete(sessionId);
@@ -115,7 +81,7 @@ export function createSessionTitler(deps) {
115
81
  return;
116
82
  }
117
83
  deps.metrics?.inc("session_title_total", { outcome: "set" });
118
- deps.logger?.info?.("session_titled", { sessionId, titleChars: title.length }); // content stays OUT of logs (R10 posture) — it's user-derived data
84
+ deps.logger?.info?.("session_titled", { sessionId, titleChars: title.length });
119
85
  })().catch((e) => {
120
86
  deps.metrics?.inc("session_title_total", { outcome: "error" });
121
87
  logFailure(sessionId, String(e));
@@ -1,28 +1,3 @@
1
- /**
2
- * Session-watch registry — the server half of the session-level SSE subscription
3
- * (design: sema-internal `server/docs/SESSION-SSE-DESIGN.md`, 黑板 [1164]①/[1196]).
4
- *
5
- * One entry per watched session; N subscribers SHARE one probe loop (this is the whole point:
6
- * it converts web-side per-client polling into one per-replica probe). Two change channels:
7
- * - fast path: `notifyLocal(sessionId, leafId)` — an append that lands on THIS replica fans out
8
- * synchronously (zero latency on single-replica deployments, the common TOC/local shape);
9
- * - slow path: the probe loop reads leafId through the SAME storage face the /head probe uses.
10
- *
11
- * codex 复审(1.230 一轮)后的三条硬语义:
12
- * - **revision 守卫**:notifyLocal 每次推进 bump `rev`;探针起读前抓拍 rev,读回后 rev 变=结果作废
13
- * (陈旧慢读不得把 B 倒退回 A——latest-wins 的机制保证,不靠时序运气)。
14
- * - **replay-on-subscribe**:已有条目携带 `last` 时,新订阅者同步补发一帧(「join 与基线原子化」的
15
- * registry 半场;连接层再配 lastSent 去重,乱序/重复帧对下游不可见)。
16
- * - **冷启动 warm**:新条目 lastChangeAt=0(没观测到变化就不算热)——首拍一次 hotMs 快确认后即落
17
- * warm 拍距,直到真观测到跳变才进热窗(codex:重连风暴下 256 会话×200ms=对存储的自造尖峰)。
18
- *
19
- * Deliberately backend-agnostic (tidb|pg|local all go through getLeafId — the seamless-local-cloud
20
- * invariant): no PG LISTEN/NOTIFY specialization, no shared bus. Latest-wins semantics: subscribers
21
- * converge on the newest leafId; intermediate hops may be dropped (content still comes from tail faces).
22
- */
23
- /** [1196] 二轮复审:SSE 资源旋钮的 env 解析——有界正整数,坏形(NaN/0/负/超界)一律回缺省并可观测
24
- * (坏 `SESSION_EVENTS_MAX_PROBES=foo` 若直进 Number() 会把帽谓词变恒假=无帽,拍距 NaN 进 setTimeout
25
- * =0ms 紧循环打爆存储)。 */
26
1
  export function posIntEnv(raw, fallback, max = 3_600_000) {
27
2
  if (raw === undefined || raw === "")
28
3
  return fallback;
@@ -36,33 +11,19 @@ export class SessionWatchRegistry {
36
11
  warmMs;
37
12
  hotWindowMs;
38
13
  maxWatched;
39
- constructor(
40
- /** 五轮复审:owner 与 leaf **同拍原子读**——探针每拍先核 owner(被删=undefined/换主=≠条目 owner
41
- * ⇒ dropSession 断全部订阅者),匹配才应用 leaf。跨副本 reclaim 的披露窗从 25s 心跳收紧到
42
- * ≤一个探针拍(hot 200ms/warm 2s),且旧流在新主推进**之前**先死(owner 检查先于 leaf 应用)。 */
43
- getHead, opts) {
14
+ constructor(getHead, opts) {
44
15
  this.getHead = getHead;
45
16
  this.hotMs = opts?.hotMs ?? 200;
46
17
  this.warmMs = opts?.warmMs ?? 2000;
47
18
  this.hotWindowMs = opts?.hotWindowMs ?? 60_000;
48
19
  this.maxWatched = opts?.maxWatchedSessions ?? 512;
49
20
  }
50
- /** Number of sessions currently probed (observability + the cap predicate). */
51
21
  watchedCount() {
52
22
  return this.entries.size;
53
23
  }
54
- /**
55
- * Subscribe to head changes. Returns undefined when the per-replica probe cap is reached
56
- * (caller answers 503 + Retry-After — the client falls back to /head polling).
57
- * If the shared entry already knows a head (`last`), it is REPLAYED synchronously into
58
- * `onHead` before returning — a subscriber joining an advanced entry can never miss the
59
- * newest value (connection-level dedupe absorbs any overlap with the caller's own read).
60
- */
61
24
  subscribe(sessionId, owner, onHead, onEnd) {
62
25
  let e = this.entries.get(sessionId);
63
26
  if (e && e.owner !== owner) {
64
- // 四轮复审跨副本围栏:同键不同 owner=本副本条目属被删/被 reclaim 的旧世代——整体作废
65
- // (旧订阅者 onEnd 断流),新 owner 拿全新条目,旧租户缓存 head 绝不 replay。
66
27
  this.dropSession(sessionId);
67
28
  e = undefined;
68
29
  }
@@ -71,7 +32,7 @@ export class SessionWatchRegistry {
71
32
  return undefined;
72
33
  e = { subs: new Set(), ends: new Map(), owner, last: undefined, rev: 0, lastChangeAt: 0, probing: false };
73
34
  this.entries.set(sessionId, e);
74
- this.arm(sessionId, e, this.hotMs); // one fast confirmation tick, then warm until a REAL change
35
+ this.arm(sessionId, e, this.hotMs);
75
36
  }
76
37
  e.subs.add(onHead);
77
38
  e.ends.set(onHead, onEnd);
@@ -80,24 +41,21 @@ export class SessionWatchRegistry {
80
41
  onHead(e.last);
81
42
  }
82
43
  catch {
83
- /* subscriber callback must not break subscribe */
84
44
  }
85
45
  }
86
46
  return () => {
87
47
  const cur = this.entries.get(sessionId);
88
48
  if (cur !== e)
89
- return; // entry already replaced/removed
49
+ return;
90
50
  e.subs.delete(onHead);
91
51
  e.ends.delete(onHead);
92
52
  if (e.subs.size === 0) {
93
53
  if (e.timer)
94
54
  clearTimeout(e.timer);
95
- this.entries.delete(sessionId); // cold: last subscriber gone → probe stops, entry dies
55
+ this.entries.delete(sessionId);
96
56
  }
97
57
  };
98
58
  }
99
- /** [1196] 三轮复审(租户轮转围栏,同副本半场):session 删除即终结条目——通知全部订阅者(onEnd,
100
- * 路由据此断流)+ 停探针 + 弃缓存 head(新租户同键重订阅=全新世代,旧 replay 不可能渗入)。 */
101
59
  dropSession(sessionId) {
102
60
  const e = this.entries.get(sessionId);
103
61
  if (!e)
@@ -110,27 +68,17 @@ export class SessionWatchRegistry {
110
68
  end?.();
111
69
  }
112
70
  catch {
113
- /* 单个订阅者收尾失败不挡其余 */
114
71
  }
115
72
  }
116
73
  }
117
- /** S2 fast path: an append that landed on THIS replica. Safe to call for unwatched sessions (no-op).
118
- * `owner` 给了就 owner-限定(五轮复审):不符=条目属旧世代 ⇒ drop(断旧流),值不投——S2 接线时
119
- * append 落点必然知道 session owner,应当传。 */
120
74
  notifyLocal(sessionId, leafId, owner, leafSeq) {
121
75
  const e = this.entries.get(sessionId);
122
76
  if (!e)
123
77
  return;
124
78
  if (owner !== undefined && owner !== e.owner) {
125
- // codex-4(S2 批):owner 不符的帧**绝不凭帧行刑**——leaf_seq 随行删建归零,跨世代序不可比:
126
- // 迟到的旧租户高序帧会假装「新于」新租户的 seq 1(凭帧 drop=误杀新流);反向新租户低序真帧也
127
- // 会被旧条目的高水位吞掉。裁决交给**立即权威探针**(owner 原子 getHead,本就是围栏权威):
128
- // 真 reclaim=探针 dropSession(零等待,不再候 warm 拍),误报=条目安然。值不投。
129
79
  this.probeNow(sessionId, e);
130
80
  return;
131
81
  }
132
- // codex-3:同 owner 内的乱序守卫——post-commit continuation 不保序,迟到低序=忽略不倒退。
133
- // (同 owner 的 delete/recreate 归零序被高水位吞=良性新鲜度问题,探针自纠——条目头注既有口径。)
134
82
  if (leafSeq !== undefined && e.lastLocalSeq !== undefined && leafSeq <= e.lastLocalSeq)
135
83
  return;
136
84
  if (leafSeq !== undefined)
@@ -139,12 +87,8 @@ export class SessionWatchRegistry {
139
87
  return;
140
88
  this.applyAdvance(e, leafId);
141
89
  }
142
- /** 立即压缩下一拍到 0ms(owner 疑云的权威核验)。探针自身 probing 串行 + 世代守卫照旧;urgent 旗
143
- * 保证「疑云到达时已有读在飞(其快照摄于 reclaim 前=陈旧)」也必有一次**疑云后开始**的权威读。 */
144
90
  probeNow(sessionId, e) {
145
91
  e.urgent = true;
146
- // codex-6:读在飞时**只置旗不排拍**——0ms 拍撞 probing 会跳读后又按旗补 0ms=事件环忙转直到慢读落定
147
- // (存储降级时整副本被搅)。在飞读的拍尾独占排拍权,旗在那里兑现。
148
92
  if (e.probing)
149
93
  return;
150
94
  if (e.timer)
@@ -157,47 +101,40 @@ export class SessionWatchRegistry {
157
101
  }
158
102
  async probe(sessionId, e) {
159
103
  if (this.entries.get(sessionId) !== e)
160
- return; // entry died while the timer was pending
104
+ return;
161
105
  if (e.probing)
162
- return; // 撞上在飞读:不读不排拍(其拍尾独占排拍权,urgent 旗在那兑现——codex-6)
106
+ return;
163
107
  {
164
108
  e.probing = true;
165
- e.urgent = false; // 本读始于旗后=消费旗(读期间新疑云会重新置旗)
109
+ e.urgent = false;
166
110
  const revAtStart = e.rev;
167
111
  try {
168
112
  const head = await this.getHead(sessionId);
169
- // 世代守卫(二轮复审):读期间末订阅者退出+重连=同键**新条目**——陈旧读绝不许经按键查找
170
- // 渗入新世代;只对抓拍的旧条目本体应用(旧条目已无订阅者=自然无害)。
171
113
  if (this.entries.get(sessionId) !== e)
172
114
  return;
173
- // 五轮复审跨副本围栏(探针拍级):session 被删(owner undefined)或被 reclaim(owner 变)
174
- // ⇒ 条目作废+订阅者断流,leaf **不应用不 fan-out**——新租户的 leafId 绝不流向旧 owner 的流。
175
115
  if (head.owner === undefined || head.owner !== e.owner) {
176
116
  this.dropSession(sessionId);
177
117
  return;
178
118
  }
179
119
  const leafId = head.leafId ?? null;
180
- // revision 守卫:读期间 notifyLocal 推进过(rev 变)=本读陈旧,丢弃——绝不把新值倒退回旧值。
181
120
  if (e.rev === revAtStart && leafId !== e.last)
182
121
  this.applyAdvance(e, leafId);
183
122
  }
184
123
  catch {
185
- /* transient storage error: keep probing — the SSE stream stays up */
186
124
  }
187
125
  finally {
188
126
  e.probing = false;
189
127
  }
190
128
  }
191
129
  if (this.entries.get(sessionId) !== e)
192
- return; // last subscriber left during the read
130
+ return;
193
131
  if (e.urgent) {
194
- this.arm(sessionId, e, 0); // codex-5:in-flight 吞掉的急拍在拍尾补上——欠的权威读绝不滑到 warm 拍
132
+ this.arm(sessionId, e, 0);
195
133
  return;
196
134
  }
197
135
  const hot = e.lastChangeAt !== 0 && Date.now() - e.lastChangeAt < this.hotWindowMs;
198
136
  this.arm(sessionId, e, hot ? this.hotMs : this.warmMs);
199
137
  }
200
- /** 对**给定世代**应用一次推进(fan-out 恒按 rev 单调序)。 */
201
138
  applyAdvance(e, leafId) {
202
139
  const wasKnown = e.last !== undefined;
203
140
  e.last = leafId;
@@ -209,7 +146,6 @@ export class SessionWatchRegistry {
209
146
  fn(leafId);
210
147
  }
211
148
  catch {
212
- /* one broken subscriber must not break the fan-out */
213
149
  }
214
150
  }
215
151
  }
@@ -1,25 +1,7 @@
1
- /**
2
- * 活对象(闭包持有目录与取值口)⇒ `create*`(CLAUDE.md 工厂命名律)。
3
- *
4
- * **代价与它的边界**(codex 对抗复审 R1-[medium] 的一半采纳,一半具名保留):取值口按调用现算,
5
- * 意味着每次 `resolve` 都要扫一遍 `config.projects`(O(项目数 × 每项 scope 数)的纯 CPU)。
6
- * · **已采纳**:principal 缺席那支不再二次建 Set —— 生产取值口(`collectDeploymentOrgScopes`)本来就用
7
- * Set 建、交回来已去重,这里再包一层是纯浪费。**去重责任因此上移到取值口**(契约见下面那个字段的注),
8
- * principal 在场那支的合并 Set 保留(它要与目录 scope 求并,那一次是真需要的)。
9
- * · **具名保留**(不做 generation 缓存):`config.projects` 由 `mutateInPlace` **就地**改,对象身份恒定,
10
- * 没有可用的失效信号;而 config-center 的 `appliedGeneration` 是那个模块的私有 WeakMap,不在公面上。
11
- * 要缓存就得先给 config-center 开一只「已提交世代」读口 —— 那是别人模块的接缝件,且**缓存失效写错
12
- * 一次,就恰好把本条 finding 刚关掉的那个陈旧窗原样放回来**(还多一层代码)。规模面亦不支持先做:
13
- * projects 是运维手写的登记簿(量级十几~百),而这条腿的**同一次**调用里,principal 在场那支还要
14
- * `await directory.lookup()`(网络/缓存),扫表在它旁边不构成瓶颈。⇒ 若哪天登记簿真长到会阻塞事件循环,
15
- * 正解是**先给 projects/defaultScopes 定尺**(发布期拒收超尺,与本仓其它 wire 上限同族),再谈缓存。
16
- */
17
1
  export function createSharedMemoryScopeAuthorizer(opts) {
18
2
  const deploymentScopesNow = () => opts.deploymentScopes?.() ?? [];
19
3
  return {
20
4
  async resolve(principal) {
21
- // principal 缺席 = 单用户部署(多租户下 core 与 HTTP 门都先要求身份)。此时唯一的授权事实就是
22
- // operator 自证的那一组;目录无从查起,但这不是「不可用」——它是一个**已知的**答案。
23
5
  if (principal === undefined)
24
6
  return { kind: "granted", scopes: deploymentScopesNow() };
25
7
  const lookup = await opts.directory.lookup(principal);
@@ -1,24 +1,15 @@
1
- /** Returns the SIGHUP handler. Call it on every SIGHUP; it arms the idle watch on the first signal and
2
- * escalates on the second. The interval is deliberately NOT unref'd — in the orphan case it IS the
3
- * shutdown driver (same posture as the drain tick). */
4
1
  export function createSighupIdleHandler(deps) {
5
2
  const tickMs = deps.tickMs ?? 1_000;
6
- const now = deps.now ?? (() => performance.now()); // 单调时钟(修3)——绝不用 Date.now()
3
+ const now = deps.now ?? (() => performance.now());
7
4
  let watch;
8
5
  let idleSince = null;
9
6
  let armedAt = 0;
10
7
  return () => {
11
8
  if (deps.isStopped()) {
12
- // A drain/shutdown owner is already active. Do NOT escalate: escalating during a graceful SIGTERM
13
- // drain would hard-kill every in-flight leg because one terminal window closed — the exact harm B3
14
- // exists to prevent. The operator's hard ladder stays SIGTERM×2 / SIGINT (复审 M5b).
15
9
  deps.log("sighup_ignored_shutdown_in_progress", {});
16
10
  return;
17
11
  }
18
12
  if (watch !== undefined) {
19
- // Debounce (复审 M5a): a window close can deliver SIGHUP twice within milliseconds (kernel pty hangup
20
- // + shell forwarding) — treating the echo as the operator's deliberate second signal would flip
21
- // draining and 503 the peers B3 protects. A human double-press is comfortably > 1s.
22
13
  if (now() - armedAt < 1_000) {
23
14
  deps.log("sighup_duplicate_ignored", {});
24
15
  return;
@@ -48,8 +39,6 @@ export function createSighupIdleHandler(deps) {
48
39
  idleSince = now();
49
40
  return;
50
41
  }
51
- // Activity clock: a request between ticks (or one still in its pre-registration phase) resets the
52
- // window even though inflight sampled 0 — both clocks must be quiet for a full grace period.
53
42
  const lastActivity = deps.lastActivityAt?.() ?? 0;
54
43
  if (now() - lastActivity < deps.idleGraceMs)
55
44
  return;