@sema-agent/server 7.150.0 → 7.151.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 (341) hide show
  1. package/MIGRATION.md +185 -0
  2. package/USAGE.md +53 -25
  3. package/deploy/sema-up/README.md +7 -1
  4. package/deploy/sema-up/chart/Chart.yaml +1 -1
  5. package/deploy/sema-up/chart/templates/NOTES.txt +5 -0
  6. package/deploy/sema-up/chart/templates/_helpers.tpl +15 -16
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -3
  8. package/deploy/sema-up/chart/templates/server.yaml +5 -3
  9. package/deploy/sema-up/chart/values-full-single.yaml +3 -1
  10. package/deploy/sema-up/chart/values.yaml +8 -1
  11. package/deploy/sema-up/cluster-up.sh +1 -0
  12. package/deploy/sema-up/kube-up.sh +1 -1
  13. package/deploy/sema-up/lib/flagshape.sh +1 -1
  14. package/deploy/sema-up/lib/helm-values-cluster.py +2 -0
  15. package/deploy/sema-up/lib/helm-values-kube.py +2 -0
  16. package/dist/adoption/import.js +3 -3
  17. package/dist/approval-ask-machine.d.ts +1 -1
  18. package/dist/approval-card.d.ts +2 -2
  19. package/dist/approval-gate-currency.d.ts +1 -1
  20. package/dist/auth-bridge.d.ts +20 -0
  21. package/dist/auth-bridge.js +9 -0
  22. package/dist/background-session-reap.d.ts +1 -1
  23. package/dist/bake-runner/main.d.ts +4 -2
  24. package/dist/bind-host.d.ts +2 -3
  25. package/dist/bind-host.js +2 -4
  26. package/dist/boot/budget-tracing.js +1 -1
  27. package/dist/boot/engine-lease.d.ts +5 -0
  28. package/dist/boot/engine-lease.js +1 -0
  29. package/dist/boot/execution-env.d.ts +8 -14
  30. package/dist/boot/execution-env.js +54 -17
  31. package/dist/boot/execution-lane.js +32 -5
  32. package/dist/boot/late-binds.d.ts +8 -4
  33. package/dist/boot/late-binds.js +7 -1
  34. package/dist/boot/memory-consolidation.d.ts +3 -2
  35. package/dist/boot/memory-consolidation.js +6 -6
  36. package/dist/boot/reapers.d.ts +5 -0
  37. package/dist/boot/reapers.js +12 -1
  38. package/dist/boot/resolve-spec.d.ts +3 -3
  39. package/dist/boot/resolve-spec.js +31 -22
  40. package/dist/boot/runner-deps.d.ts +9 -2
  41. package/dist/boot/runner-deps.js +3 -3
  42. package/dist/boot/session-age-out.d.ts +36 -0
  43. package/dist/boot/session-age-out.js +101 -0
  44. package/dist/boot/session-faces.d.ts +5 -14
  45. package/dist/boot/session-faces.js +9 -5
  46. package/dist/boot/stage-01-config.js +2 -0
  47. package/dist/boot/stage-05-execution-env.d.ts +5 -3
  48. package/dist/boot/stage-05-execution-env.js +2 -2
  49. package/dist/boot/stage-06-runners.d.ts +7 -4
  50. package/dist/boot/stage-06-runners.js +15 -6
  51. package/dist/boot/stage-07-capability-layer.d.ts +7 -4
  52. package/dist/boot/stage-07-capability-layer.js +2 -1
  53. package/dist/boot/stage-08-reapers.d.ts +7 -4
  54. package/dist/boot/stage-08-reapers.js +4 -2
  55. package/dist/boot/stage-09-leader.d.ts +11 -5
  56. package/dist/boot/stage-09-leader.js +12 -3
  57. package/dist/boot/stage-10-http-server.d.ts +11 -5
  58. package/dist/boot/stage-10-http-server.js +3 -6
  59. package/dist/boot/web-admin.js +3 -1
  60. package/dist/boot/workflow-orchestration.d.ts +7 -2
  61. package/dist/boot/workflow-orchestration.js +24 -7
  62. package/dist/bounded-rotation.d.ts +29 -0
  63. package/dist/bounded-rotation.js +24 -0
  64. package/dist/capability-facts.d.ts +22 -13
  65. package/dist/capability-facts.js +28 -18
  66. package/dist/center-credential.d.ts +25 -6
  67. package/dist/center-credential.js +25 -5
  68. package/dist/config-catalog.d.ts +26 -7
  69. package/dist/config-catalog.js +14 -11
  70. package/dist/config-invariants.d.ts +2 -1
  71. package/dist/config-invariants.js +4 -4
  72. package/dist/config-types.d.ts +10 -4
  73. package/dist/config.d.ts +51 -2
  74. package/dist/config.js +35 -24
  75. package/dist/deployment-shape.d.ts +9 -1
  76. package/dist/deployment-shape.js +2 -1
  77. package/dist/device-protocol.d.ts +5 -4
  78. package/dist/device-protocol.js +1 -1
  79. package/dist/device-ws-hub.js +3 -2
  80. package/dist/device-ws-protocol.d.ts +63 -10
  81. package/dist/device-ws-protocol.js +56 -25
  82. package/dist/engine-identity.d.ts +19 -7
  83. package/dist/engine-identity.js +10 -2
  84. package/dist/entry-mode-clamp-notice.d.ts +2 -1
  85. package/dist/execution-env-refusal.d.ts +23 -0
  86. package/dist/execution-env-refusal.js +5 -0
  87. package/dist/execution-lane-caps.d.ts +3 -1
  88. package/dist/field-conflict.d.ts +1 -1
  89. package/dist/file-history-capture.d.ts +11 -4
  90. package/dist/file-history-capture.js +9 -6
  91. package/dist/file-history-disclosure.d.ts +7 -5
  92. package/dist/file-history-disclosure.js +7 -5
  93. package/dist/fleet/drain-entry.d.ts +4 -0
  94. package/dist/hooks/hook-runner.js +2 -2
  95. package/dist/host-lsp-manager.d.ts +2 -2
  96. package/dist/http/admission.js +17 -2
  97. package/dist/http/approval-sweeps.js +5 -5
  98. package/dist/http/checkpoint-ctx.d.ts +2 -0
  99. package/dist/http/checkpoint-ctx.js +1 -0
  100. package/dist/http/dispatch.d.ts +2 -1
  101. package/dist/http/dispatch.js +34 -27
  102. package/dist/http/durable-run-leg.js +16 -16
  103. package/dist/http/health-self-report.d.ts +9 -1
  104. package/dist/http/health-self-report.js +2 -0
  105. package/dist/http/owner-access.d.ts +31 -11
  106. package/dist/http/owner-access.js +37 -6
  107. package/dist/http/resume-ctx-read.js +25 -3
  108. package/dist/http/resume-legs.d.ts +2 -1
  109. package/dist/http/resume-legs.js +259 -79
  110. package/dist/http/route-ctx.d.ts +18 -8
  111. package/dist/http/route-table.d.ts +1 -1
  112. package/dist/http/routes/admin-memory-optout.d.ts +1 -1
  113. package/dist/http/routes/agents-roster.js +2 -3
  114. package/dist/http/routes/approvals-assistant.js +4 -3
  115. package/dist/http/routes/background.d.ts +3 -3
  116. package/dist/http/routes/capabilities.js +8 -5
  117. package/dist/http/routes/fleet.js +10 -8
  118. package/dist/http/routes/projects.d.ts +3 -2
  119. package/dist/http/routes/projects.js +4 -5
  120. package/dist/http/routes/runs.js +102 -36
  121. package/dist/http/routes/session-sync.d.ts +4 -3
  122. package/dist/http/routes/session-sync.js +172 -77
  123. package/dist/http/routes/sessions-list.js +13 -7
  124. package/dist/http/routes/sessions.js +10 -6
  125. package/dist/http/routes/tasks.js +30 -16
  126. package/dist/http/routes/trace-usage.js +2 -2
  127. package/dist/http/server.d.ts +21 -27
  128. package/dist/http/server.js +2 -2
  129. package/dist/http/submit-ctx.js +5 -4
  130. package/dist/http/wire-types.d.ts +14 -4
  131. package/dist/identity-source.d.ts +20 -3
  132. package/dist/identity-source.js +14 -0
  133. package/dist/index.d.ts +4 -0
  134. package/dist/lane-home-probe.d.ts +3 -7
  135. package/dist/lane-home-probe.js +0 -1
  136. package/dist/leader/leader.d.ts +2 -1
  137. package/dist/leg-facts.d.ts +13 -5
  138. package/dist/leg-facts.js +2 -1
  139. package/dist/memory-scope.d.ts +5 -4
  140. package/dist/memory-scope.js +4 -4
  141. package/dist/memory-sync-client.d.ts +30 -2
  142. package/dist/memory-sync-client.js +16 -9
  143. package/dist/observability/err-text.d.ts +7 -0
  144. package/dist/observability/err-text.js +5 -0
  145. package/dist/observability/fail-open.d.ts +7 -3
  146. package/dist/observability/fail-open.js +7 -3
  147. package/dist/observability/metrics.js +1 -0
  148. package/dist/orchestration/workflow-notify-journal.d.ts +18 -1
  149. package/dist/orchestration/workflow-notify-journal.js +12 -1
  150. package/dist/orchestration/workflow-park-decision.d.ts +70 -18
  151. package/dist/orchestration/workflow-park-decision.js +49 -10
  152. package/dist/orchestration/workflow-park-holder.d.ts +65 -0
  153. package/dist/orchestration/workflow-park-holder.js +98 -0
  154. package/dist/orchestration/workflow-record-fence.d.ts +36 -0
  155. package/dist/orchestration/workflow-record-fence.js +42 -0
  156. package/dist/owner-faces.d.ts +1 -1
  157. package/dist/parked-decide.d.ts +104 -53
  158. package/dist/parked-decide.js +153 -117
  159. package/dist/peer-directory.d.ts +2 -2
  160. package/dist/peer-inbound-face.d.ts +2 -6
  161. package/dist/peer-inbound-face.js +0 -3
  162. package/dist/permission-mode-admission.d.ts +41 -5
  163. package/dist/permission-mode-admission.js +29 -3
  164. package/dist/plugins/adoption-log-sql.d.ts +3 -1
  165. package/dist/plugins/adoption-log-sql.js +7 -1
  166. package/dist/plugins/approval-ask-store-file.d.ts +10 -2
  167. package/dist/plugins/approval-ask-store-file.js +8 -0
  168. package/dist/plugins/approval-ask-store-memory.d.ts +6 -1
  169. package/dist/plugins/approval-ask-store-memory.js +28 -9
  170. package/dist/plugins/approval-ask-store-sql.d.ts +43 -2
  171. package/dist/plugins/approval-ask-store-sql.js +34 -5
  172. package/dist/plugins/approval-exemption-store.js +3 -0
  173. package/dist/plugins/approval-nonce-store.js +2 -0
  174. package/dist/plugins/breaker-state-sql.js +2 -0
  175. package/dist/plugins/caching-session-store.d.ts +9 -15
  176. package/dist/plugins/caching-session-store.js +6 -0
  177. package/dist/plugins/device-store-sql.js +6 -0
  178. package/dist/plugins/error-origin.d.ts +18 -0
  179. package/dist/plugins/error-origin.js +10 -0
  180. package/dist/plugins/execution-placement-sql.js +6 -6
  181. package/dist/plugins/image-bake-store-sql.js +6 -0
  182. package/dist/plugins/image-index-sql.js +4 -0
  183. package/dist/plugins/landed-attendants.d.ts +212 -0
  184. package/dist/plugins/landed-attendants.js +161 -0
  185. package/dist/plugins/leader-run-store-sql.js +4 -0
  186. package/dist/plugins/local-session-live-reference.d.ts +40 -0
  187. package/dist/plugins/local-session-live-reference.js +39 -0
  188. package/dist/plugins/local-session-store.d.ts +71 -33
  189. package/dist/plugins/local-session-store.js +129 -27
  190. package/dist/plugins/memory-engine-pg.js +4 -0
  191. package/dist/plugins/memory-engine-sql-apply.js +5 -2
  192. package/dist/plugins/memory-engine-tidb.js +4 -0
  193. package/dist/plugins/memory-optout-grant-store-sql.js +4 -0
  194. package/dist/plugins/memory-sync-store-pg.js +5 -0
  195. package/dist/plugins/memory-sync-store-tidb.js +5 -0
  196. package/dist/plugins/one-time-migrations.d.ts +57 -63
  197. package/dist/plugins/one-time-migrations.js +162 -90
  198. package/dist/plugins/org-delivery-quota-store.js +4 -1
  199. package/dist/plugins/outcome-ledger-sql.js +2 -0
  200. package/dist/plugins/permission-rule-store-sql.js +11 -4
  201. package/dist/plugins/pg-cost-quota.js +4 -0
  202. package/dist/plugins/pg-pool.js +24 -1
  203. package/dist/plugins/pg-rate-limiter.js +2 -0
  204. package/dist/plugins/posix-shell-fs.d.ts +6 -1
  205. package/dist/plugins/posix-shell-fs.js +4 -1
  206. package/dist/plugins/remote-env-adb.d.ts +19 -15
  207. package/dist/plugins/remote-env-adb.js +41 -15
  208. package/dist/plugins/remote-env-connect-seat.d.ts +105 -0
  209. package/dist/plugins/remote-env-connect-seat.js +72 -0
  210. package/dist/plugins/remote-env-device.d.ts +22 -5
  211. package/dist/plugins/remote-env-device.js +106 -8
  212. package/dist/plugins/remote-env-e2b.d.ts +15 -6
  213. package/dist/plugins/remote-env-e2b.js +42 -32
  214. package/dist/plugins/remote-env-file-error.js +6 -2
  215. package/dist/plugins/remote-env-host.d.ts +17 -0
  216. package/dist/plugins/remote-env-host.js +30 -38
  217. package/dist/plugins/remote-env-k8s.d.ts +41 -39
  218. package/dist/plugins/remote-env-k8s.js +53 -30
  219. package/dist/plugins/remote-env-local-docker.d.ts +13 -8
  220. package/dist/plugins/remote-env-local-docker.js +34 -41
  221. package/dist/plugins/remote-env-ssh.d.ts +18 -12
  222. package/dist/plugins/remote-env-ssh.js +60 -59
  223. package/dist/plugins/remote-shell.d.ts +98 -1
  224. package/dist/plugins/remote-shell.js +218 -0
  225. package/dist/plugins/resume-anchor-store-sql.d.ts +11 -2
  226. package/dist/plugins/resume-anchor-store-sql.js +12 -5
  227. package/dist/plugins/retention-lane-store-sql.js +4 -0
  228. package/dist/plugins/retention-sql.d.ts +20 -2
  229. package/dist/plugins/retention-sql.js +11 -0
  230. package/dist/plugins/run-store-sql.d.ts +21 -2
  231. package/dist/plugins/run-store-sql.js +31 -7
  232. package/dist/plugins/scenario-prefix-switch-m42.d.ts +1 -1
  233. package/dist/plugins/scenario-prefix-switch-m42.js +2 -2
  234. package/dist/plugins/schema-consistency.d.ts +4 -2
  235. package/dist/plugins/schema-consistency.js +19 -2
  236. package/dist/plugins/send-file-ledger.js +3 -0
  237. package/dist/plugins/serving-requirement.d.ts +26 -7
  238. package/dist/plugins/serving-requirement.js +11 -4
  239. package/dist/plugins/session-birth.d.ts +6 -2
  240. package/dist/plugins/session-capture-record-store-sql.js +2 -0
  241. package/dist/plugins/session-ext-table.d.ts +14 -1
  242. package/dist/plugins/session-ext-table.js +5 -1
  243. package/dist/plugins/session-row-sql.d.ts +49 -2
  244. package/dist/plugins/session-row-sql.js +56 -13
  245. package/dist/plugins/session-store-sql.d.ts +34 -18
  246. package/dist/plugins/session-store-sql.js +98 -17
  247. package/dist/plugins/shared-memory-store-sql.d.ts +1 -0
  248. package/dist/plugins/shared-memory-store-sql.js +6 -2
  249. package/dist/plugins/sql-driver.d.ts +2 -1
  250. package/dist/plugins/sql-driver.js +18 -3
  251. package/dist/plugins/sql-errors.d.ts +11 -6
  252. package/dist/plugins/sql-errors.js +5 -6
  253. package/dist/plugins/stop-then-start-sentence.d.ts +10 -4
  254. package/dist/plugins/stop-then-start-sentence.js +14 -10
  255. package/dist/plugins/stop-then-start.d.ts +109 -0
  256. package/dist/plugins/stop-then-start.js +104 -0
  257. package/dist/plugins/store-backend.d.ts +40 -13
  258. package/dist/plugins/store-backend.js +169 -27
  259. package/dist/plugins/store-boundary.d.ts +57 -0
  260. package/dist/plugins/store-boundary.js +139 -0
  261. package/dist/plugins/store-contracts.d.ts +17 -0
  262. package/dist/plugins/store-fault-sentence-m53.d.ts +7 -0
  263. package/dist/plugins/store-fault-sentence-m53.js +25 -0
  264. package/dist/plugins/store-fault.d.ts +26 -7
  265. package/dist/plugins/store-fault.js +18 -2
  266. package/dist/plugins/task-attachment-store.js +9 -0
  267. package/dist/plugins/task-list-store-sql.d.ts +2 -2
  268. package/dist/plugins/task-list-store-sql.js +10 -8
  269. package/dist/plugins/tidb-cost-quota.js +4 -0
  270. package/dist/plugins/tidb-pool.js +2 -1
  271. package/dist/plugins/tidb-rate-limiter.js +4 -0
  272. package/dist/plugins/user-input-ledger-sql.js +4 -0
  273. package/dist/plugins/workflow-run-store-sql.d.ts +28 -10
  274. package/dist/plugins/workflow-run-store-sql.js +64 -4
  275. package/dist/plugins/workspace-fill.d.ts +4 -7
  276. package/dist/plugins/workspace-fill.js +2 -1
  277. package/dist/plugins/write-behind-counter.js +5 -0
  278. package/dist/project-context.d.ts +0 -2
  279. package/dist/project-context.js +4 -8
  280. package/dist/project-registry.d.ts +46 -0
  281. package/dist/project-registry.js +44 -0
  282. package/dist/request-key-closure.js +1 -1
  283. package/dist/request-value-closure.d.ts +4 -0
  284. package/dist/request-value-closure.js +10 -0
  285. package/dist/run-acceptance.d.ts +3 -2
  286. package/dist/run-acceptance.js +4 -6
  287. package/dist/run-local.js +1 -0
  288. package/dist/runs.d.ts +37 -8
  289. package/dist/runs.js +53 -12
  290. package/dist/runtime-governance.d.ts +5 -4
  291. package/dist/security.d.ts +30 -32
  292. package/dist/security.js +41 -7
  293. package/dist/server-secret-env.d.ts +12 -10
  294. package/dist/server-secret-env.js +24 -15
  295. package/dist/session-bindings.d.ts +82 -0
  296. package/dist/session-bindings.js +18 -0
  297. package/dist/session-children-retire.d.ts +2 -2
  298. package/dist/session-children-retire.js +2 -6
  299. package/dist/session-deletion.d.ts +16 -1
  300. package/dist/session-error-wire.d.ts +3 -3
  301. package/dist/session-error-wire.js +3 -3
  302. package/dist/session-incarnation.d.ts +10 -0
  303. package/dist/session-incarnation.js +10 -1
  304. package/dist/session-kind.d.ts +57 -0
  305. package/dist/session-kind.js +22 -0
  306. package/dist/session-row.d.ts +21 -1
  307. package/dist/session-row.js +7 -1
  308. package/dist/session-sync-kernel.d.ts +24 -14
  309. package/dist/session-sync-kernel.js +3 -6
  310. package/dist/session-sync.d.ts +4 -98
  311. package/dist/session-sync.js +1 -71
  312. package/dist/session-titler.d.ts +10 -3
  313. package/dist/session-titler.js +19 -6
  314. package/dist/ssh-host-key.d.ts +5 -0
  315. package/dist/ssh-host-key.js +3 -0
  316. package/dist/stores-sql/file-history-store-sql.d.ts +52 -2
  317. package/dist/stores-sql/file-history-store-sql.js +108 -70
  318. package/dist/stores-sql/pg-session-storage.d.ts +34 -7
  319. package/dist/stores-sql/pg-session-storage.js +37 -13
  320. package/dist/stores-sql/retention-store-sql.d.ts +1 -9
  321. package/dist/stores-sql/retention-store-sql.js +2 -10
  322. package/dist/stores-sql/session-policy-store-sql.d.ts +30 -7
  323. package/dist/stores-sql/session-policy-store-sql.js +61 -44
  324. package/dist/stores-sql/session-write-hooks.d.ts +11 -0
  325. package/dist/stores-sql/tidb-session-store.d.ts +58 -7
  326. package/dist/stores-sql/tidb-session-store.js +37 -13
  327. package/dist/submission-exit.d.ts +3 -2
  328. package/dist/submission-exit.js +9 -1
  329. package/dist/task-cwd.d.ts +2 -2
  330. package/dist/task-settings.d.ts +6 -5
  331. package/dist/terminal.d.ts +33 -2
  332. package/dist/terminal.js +139 -9
  333. package/dist/tool-approval.d.ts +140 -6
  334. package/dist/tool-approval.js +165 -72
  335. package/dist/trace/self-minted-wire.d.ts +290 -27
  336. package/dist/trace/self-minted-wire.js +101 -13
  337. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +78 -27
  338. package/package.json +2 -2
  339. package/wire-changes/server@7.151.0.json +762 -0
  340. package/dist/http/session-inherit-env.d.ts +0 -36
  341. package/dist/http/session-inherit-env.js +0 -6
@@ -0,0 +1,22 @@
1
+ export const SESSION_KINDS = ["sidechain"];
2
+ export function isSessionKind(v) {
3
+ return typeof v === "string" && SESSION_KINDS.includes(v);
4
+ }
5
+ export function sessionKindListed(rowKind, view) {
6
+ return rowKind === view;
7
+ }
8
+ export function sessionKindFaceOf(store) {
9
+ const s = store;
10
+ return typeof s?.sessionKindOf === "function" ? { sessionKindOf: s.sessionKindOf.bind(s) } : undefined;
11
+ }
12
+ export function sessionKindFromCell(sessionId, cell) {
13
+ if (cell === null || cell === undefined)
14
+ return undefined;
15
+ if (isSessionKind(cell))
16
+ return cell;
17
+ throw new Error(`session ${sessionId} carries a session kind this build does not know (${JSON.stringify(String(cell).slice(0, 32))}) — refusing to read it as a top-level session; a newer build wrote it, or the row was edited by hand`);
18
+ }
19
+ export function idleSessionAfter(row, cursor) {
20
+ return cursor === undefined || row.lastActivityAt > cursor.lastActivityAt || (row.lastActivityAt === cursor.lastActivityAt && row.sessionId > cursor.sessionId);
21
+ }
22
+ //# sourceMappingURL=session-kind.js.map
@@ -1,4 +1,5 @@
1
1
  import { type SessionLogMark, type SessionRowExpectation } from "@sema-agent/core";
2
+ import type { SessionFace } from "./session-incarnation.js";
2
3
  /** `title` 列的宽度(字符)= 用户标题的上界。两方言 DDL 内插它。 */
3
4
  export declare const SESSION_TITLE_MAX_CHARS = 120;
4
5
  /** 会话行的可写位(体闭集;两键至少一键)。会话店唯一行格写口 `writeSessionRowCells` 的格参。`title: null` = 清回自动标题(列写 NULL)。 */
@@ -81,6 +82,9 @@ export interface LockedSessionRow {
81
82
  readonly incarnation: string | undefined;
82
83
  /** 委派分区行(SQL `placement_kind` 非空;local 没有)—— 不在列表面上,不写。 */
83
84
  readonly placed: boolean;
85
+ /** 这只会话在删(会话三态 活 → 在删 → 没了,`src/session-deletion.ts`):SQL = 锁读的旁表在删令牌非空;local 的锁内读法({@link LockedSessionRow} 的 local 半)
86
+ * 只交活格,在删格在那里已读作「没有行」⇒ 恒 `false`。判官把它读作行不在(三态律:删除门之外的一切写者把在删读作行不在)。 */
87
+ readonly deleting: boolean;
84
88
  /** 执行格是 `mirror`(写者在别处,本处只读)。 */
85
89
  readonly mirror: boolean;
86
90
  readonly cells: SessionRowCells;
@@ -89,7 +93,7 @@ export interface LockedSessionRow {
89
93
  export type SessionRowVerdict = "write" | "changed" | "mirror" | "absent";
90
94
  /**
91
95
  * 写原语的**唯一判官**(头注「写原语」):三店在各自的锁下读出那一行、交这一只,判序因此逐字同形 ——
92
- * ① **行期望**:core 的 `sessionRowMatches(how.row, 锁住的行, logNow)`;不中、没有行(`undefined`)、委派分区行 ⇒ `absent`;
96
+ * ① **行期望**:core 的 `sessionRowMatches(how.row, 锁住的行, logNow)`;不中、没有行(`undefined`)、在删行、委派分区行 ⇒ `absent`(在删 = 行不在,三态律);
93
97
  * ② **写者身份**:`client` ∧ 执行格为 `mirror` ⇒ `mirror`(镜像在本处只读;`replay` 是镜像的名写者,照写);
94
98
  * ③ **标签**:`client` 带 `rowTag` ∧ 锁住的行算出的标签({@link sessionRowTagOf})≠ 它 ⇒ `changed`(行上没有化身 ⇒ 算不出标签 ⇒ 任何标签都不中);
95
99
  * ④ `write`。
@@ -97,6 +101,22 @@ export type SessionRowVerdict = "write" | "changed" | "mirror" | "absent";
97
101
  * `logNow` 只在行期望带 `log` 时被调(调用方在同一把锁下先读好那只 mark)。
98
102
  */
99
103
  export declare function judgeSessionRowWrite(sessionId: string, how: SessionRowWrite, locked: LockedSessionRow | undefined, logNow: () => SessionLogMark): SessionRowVerdict;
104
+ /**
105
+ * **落定段**的结局(SQL `withLandedRow` · local `LocalSessionStore.withLanded` 两只实现同一形):`landed: true` = 段首锁住的那一行仍是落定的那一位住客
106
+ * ({@link judgeLandedRow} 答 `write`),段体在同一事务 / 同一把会话写锁里跑完、交回它的值;`landed: false` = 那一位已不在(行不在 / 在删 / 换了住客 / 属主换代 /
107
+ * 委派分区行),段体一个字节都没跑。会话的附随物(文件历史图 · 会话规则行 · 回退锚 · 自动标题)只经这一段写 —— 律在 `plugins/landed-attendants.ts` 头注。
108
+ */
109
+ export type LandedOutcome<T> = {
110
+ readonly landed: true;
111
+ readonly value: T;
112
+ } | {
113
+ readonly landed: false;
114
+ };
115
+ /**
116
+ * 落定段的判:锁住的那一行是不是 `landed` 那一位住客 —— **就是**行格写原语的判官 {@link judgeSessionRowWrite} 以 `replay` 写者问(行期望 = `{ row: landed }`;
117
+ * 行在 ∧ 非在删 ∧ 非委派分区 ∧ 属主与化身整对同,core `sessionRowMatches`)。一只判官,没有第二份比较。
118
+ */
119
+ export declare function judgeLandedRow(sessionId: string, landed: SessionFace, locked: LockedSessionRow | undefined): boolean;
100
120
  /** 应答行:提交 / 回滚之后另读的那一行 + 读到它时行上的住客(属主 + 化身那一格)。 */
101
121
  export interface SessionRowAnswer<Row> {
102
122
  readonly row: Row;
@@ -13,7 +13,7 @@ export function sessionRowTagKey(row) {
13
13
  return row.incarnation === undefined ? {} : { rowTag: sessionRowTagOf({ sessionId: row.sessionId, owner: row.owner, incarnation: row.incarnation, cells: row.cells }) };
14
14
  }
15
15
  export function judgeSessionRowWrite(sessionId, how, locked, logNow) {
16
- if (locked === undefined || !sessionRowMatches(how.row, { owner: locked.owner, incarnation: locked.incarnation }, logNow) || locked.placed)
16
+ if (locked === undefined || locked.deleting || !sessionRowMatches(how.row, { owner: locked.owner, incarnation: locked.incarnation }, logNow) || locked.placed)
17
17
  return "absent";
18
18
  switch (how.writer) {
19
19
  case "replay":
@@ -32,6 +32,12 @@ export function judgeSessionRowWrite(sessionId, how, locked, logNow) {
32
32
  }
33
33
  }
34
34
  }
35
+ export function judgeLandedRow(sessionId, landed, locked) {
36
+ return judgeSessionRowWrite(sessionId, { writer: "replay", row: { row: landed } }, locked, landedCarriesNoLogMark) === "write";
37
+ }
38
+ function landedCarriesNoLogMark() {
39
+ throw new Error("judgeLandedRow: a landed-section expectation carries no log mark, yet the judge asked for one");
40
+ }
35
41
  export function settleSessionRowWrite(how, verdict, answer) {
36
42
  const judged = how.row.row;
37
43
  if (judged === null || answer === undefined)
@@ -28,11 +28,11 @@ export interface StagingHandle {
28
28
  /** Append a bounded batch of entries to the staging id, re-seq'd dense from a running counter held in the handle.
29
29
  * Idempotent (ON DUPLICATE KEY / ON CONFLICT DO NOTHING) — a re-streamed batch is a no-op. One txn per batch. */
30
30
  appendBatch(entries: SessionTreeEntry[]): Promise<void>;
31
- /** ATOMICALLY swap the staged rows into the real session id (ONE txn): owner CAS ({@link StagedCommitOptions.expectedOwner},
31
+ /** ATOMICALLY swap the staged rows into the real session id (ONE txn): occupant CAS ({@link StagedCommitOptions.expectedOccupant},
32
32
  * before anything about the row is classified or reported, so a refused commit reveals nothing about a row it did not judge — no
33
33
  * placement, no relation, no entry ids) → in-txn re-classify
34
34
  * (TOCTOU close) → purge dst → row-key rename staging→real → session_meta settled; COMMIT is the commit point. `owner` is re-stamped (§9).
35
- * Throws {@link StagedCommitOwnerChangedError} when the owner cell is not the one the import's gate judged, and
35
+ * Throws {@link StagedCommitOccupantChangedError} when the row is not the occupant the import's gate judged, and
36
36
  * {@link SyncConflictError} on an unconsented fork/stale flip — both after rolling back. Returns the committed relation **and the settled session
37
37
  * face** — the occupant (owner + incarnation) the commit left in the real session id, read INSIDE the same txn (the local
38
38
  * store: the same hold of the session's write lock), never re-read afterwards. The post-commit replays conditioned on the
@@ -47,29 +47,39 @@ export interface StagingHandle {
47
47
  abort(): Promise<void>;
48
48
  }
49
49
  /**
50
- * What a staged commit is conditioned on ([ref] — one gate predicate, two phases). `expectedOwner` = the owner cell the import's gate
51
- * judged when it opened the staging, in the head-read reading (`headOf(id)?.owner`): `undefined` = no such session (a fresh push), `null` = ownerless, a string =
52
- * that owner. The route judges `mayTouch` on that cell at Phase A and again on the session's current cell right before Phase B commits; the
53
- * commit itself lands only on that same cell — each store compares it inside the commit txn (local: inside the session's write lock) and, when
54
- * the cell differs, rolls back and throws {@link StagedCommitOwnerChangedError} without writing a byte. The compare is on the owner cell, not
55
- * on the incarnation: the gate's predicate reads only the owner (plus placement, which the commit gate refuses on its own), so a same-owner
56
- * re-create is judged the same and is not a reason to refuse.
50
+ * What a staged commit is conditioned on ([ref] — one gate predicate, two phases). `expectedOccupant` = the occupant the import's gate judged when it
51
+ * opened the staging — the live row's owner **and** incarnation, one head read (`undefined` = no live session: a fresh push, which claims the id).
52
+ * The route judges `mayTouch` at Phase A and again on the session's current row right before Phase B commits; the commit itself lands only on that
53
+ * same occupant — each store compares owner ∧ incarnation inside the commit txn (SQL `owner <=> ? AND incarnation = ?`; local: the same judge as every
54
+ * occupant check, `sameOccupant`, inside the session's write lock) and, when it differs, rolls back and throws {@link StagedCommitOccupantChangedError}
55
+ * without writing a byte. So a session deleted and re-created while the entries streamed — by anyone, the same owner included — is not the session
56
+ * phase one classified, and the commit does not land on it (the push restarts from phase one).
57
57
  */
58
+ /**
59
+ * 拉取流的选项:`afterSeq` 续传游标(后端原生 seq)· `batchSize` 页长 · `recheck` = 调用方的住客复判 —— **每页读后、交出那一页的任何一条之前**调一次
60
+ * (local 孪生整读之后、交出第一条之前调一次);它抛 ⇒ 流在那里结束(调用方读作截断)。必填:流不会在没有复判的情况下交出条目。
61
+ */
62
+ export interface EntriesStreamOptions {
63
+ readonly afterSeq?: number;
64
+ readonly batchSize?: number;
65
+ readonly recheck: () => Promise<void>;
66
+ }
58
67
  export interface StagedCommitOptions {
59
- readonly expectedOwner: string | null | undefined;
68
+ readonly expectedOccupant: {
69
+ readonly owner: string | null;
70
+ readonly incarnation: string;
71
+ } | undefined;
60
72
  readonly resolution?: "overwrite-dst";
61
73
  }
62
74
  /**
63
- * The staged commit's owner CAS did not hold: the session's owner cell at commit time is not the one the import's gate judged (another replica
75
+ * The staged commit's occupant CAS did not hold: the session row at commit time is not the occupant the import's gate judged (another replica
64
76
  * deleted / re-created / claimed / created the session while the import streamed). Code = core's closed-set `conflict` (wire: the import face's
65
77
  * row in `session-error-wire.ts`, 409); thrown after the commit txn rolled back — the destination is untouched. The three stores throw this one
66
78
  * class (one sentence, minted here).
67
79
  */
68
- export declare class StagedCommitOwnerChangedError extends SessionError {
80
+ export declare class StagedCommitOccupantChangedError extends SessionError {
69
81
  constructor(sessionId: string);
70
82
  }
71
- /** The owner-cell comparison of {@link StagedCommitOptions.expectedOwner} against a store's own owner read (`headOf(id)?.owner`; `undefined` = no row). */
72
- export declare function ownerCellIs(expectedOwner: string | null | undefined, actual: string | null | undefined): boolean;
73
83
  /** A session store that can stage a streamed import (P1d-β). The 3 backends implement it; the in-memory dev store
74
84
  * does not → the method is optional (the route 501s when absent, parity with the other §0.5 optional seams). */
75
85
  export type ImportStagingStore = {
@@ -1,14 +1,11 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { SessionError, StreamingImportValidator } from "@sema-agent/core";
3
- export class StagedCommitOwnerChangedError extends SessionError {
3
+ export class StagedCommitOccupantChangedError extends SessionError {
4
4
  constructor(sessionId) {
5
- super("conflict", `session ${sessionId}: owner changed while the import streamed — the staged commit was rolled back`);
6
- this.name = "StagedCommitOwnerChangedError";
5
+ super("conflict", `session ${sessionId}: the session changed hands while the import streamed (deleted, re-created or re-owned) — the staged commit was rolled back`);
6
+ this.name = "StagedCommitOccupantChangedError";
7
7
  }
8
8
  }
9
- export function ownerCellIs(expectedOwner, actual) {
10
- return expectedOwner === undefined ? actual === undefined : actual !== undefined && (actual ?? null) === expectedOwner;
11
- }
12
9
  export function stagingIdFor(realSessionId, token) {
13
10
  return `${realSessionId}#stg-${token}`;
14
11
  }
@@ -36,6 +36,8 @@
36
36
  * Neither gap is fixed by this car (mechanical removal, no behavior changes) — they are reported here + in the
37
37
  * removal's final report for separate triage (port the fix to the live route, or explicitly accept the regression).
38
38
  *
39
+ * ✅ **STATUS UPDATE(会话附随物绑住客)**:附随物的写半(擦除 / 文件历史重放 / 规则重放与等内容判据 / 锚)整体搬到落定席位 `plugins/landed-attendants.ts`
40
+ * —— 每一笔写都在锁住会话根行、判过住客的落定段里;本模块只剩读半(manifest 导出、文件历史导出、blob 取值口)与干跑。
39
41
  * ✅ **STATUS UPDATE (2026-08-06 同日,[ref] 红先修绿)**:上面两条「NO live-route equivalent」已作废——
40
42
  * 两个保护都已移植到活路由(本文件尾部 [ref] 段:`sameRulesContent`/`replayPolicyRecords`/
41
43
  * `overwriteWipeIncapable`/`wipeSessionAttendants`;路由接线=src/http/routes/session-sync.ts 的两条重放腿
@@ -45,8 +47,8 @@
45
47
  */
46
48
  import type { StoreBackend } from "./plugins/store-backend.js";
47
49
  import { type SyncRelation } from "./session-sync-kernel.js";
48
- import { type SessionTreeEntry, type SessionRulesRecord, type FileHistoryError, type FileHistoryExport, type FileHistoryResult, type SessionPermissionRules, type StoredSessionRules, type SessionPolicyStore } from "@sema-agent/core";
49
- import type { ServiceBlobExchange, ServiceFileHistoryStore, ServiceSessionPolicyStore, ResumeAnchorStore } from "./plugins/store-backend.js";
50
+ import type { SessionTreeEntry, SessionRulesRecord, FileHistoryError, FileHistoryExport } from "@sema-agent/core";
51
+ import type { ServiceBlobExchange, ServiceFileHistoryStore } from "./plugins/store-backend.js";
50
52
  /**
51
53
  * core 7.55.0 [ref] T2([ref]):一只会话的文件历史**读不全**(撕裂 / 读不出的边界、读不出的跟踪集 —— 店的 `exportHistory` 答 `{ ok: false }`,`adopt_failed`
52
54
  * 带店判的 `permanence`)。它**不是**「没有历史」:读成 `null` 的同步会把这只会话的全部 rewind 历史丢在路上、还答成功。会话同步的两只拉取口
@@ -77,55 +79,6 @@ export declare function blobExchangeOf(store: ServiceFileHistoryStore): ServiceB
77
79
  /** 一只文件历史店自己的 blob 取值口({@link BlobGetter}):从 {@link blobExchangeOf} 派生 —— 有交换组 ⇒ 组的 `get`;没有(core 的 local 文件店)⇒ 恒答 `undefined`(「这里取不到」)。
78
80
  * 拉取面(manifest 的 `getBlob`)、identical 自愈臂、提交臂三处读同一只,不各写一份「无组则 undefined」。 */
79
81
  export declare function blobGetterOf(store: ServiceFileHistoryStore): BlobGetter;
80
- /** The history-import capability face of a `FileHistoryStore` (`importHistory` is OPTIONAL on the core
81
- * contract — a store without it cannot be a 2c history sink). */
82
- export interface FileHistoryImportFace {
83
- importHistory(scope: string, data: FileHistoryExport, srcGetBlob: BlobGetter): Promise<FileHistoryResult>;
84
- }
85
- /** Single-point capability probe ([ref] R7): does `fs` carry the optional `importHistory` seam?
86
- * Returns the bound method (never a naked function reference — `this` inside must resolve to `fs`) or
87
- * `undefined` when absent. Callers gate on the RETURN VALUE, not a separate `typeof` check, so there is
88
- * exactly one place that knows the shape of the probe. */
89
- export declare function fileHistoryImportFace(fs: unknown): FileHistoryImportFace | undefined;
90
- /**
91
- * {@link replayFileHistory} 的结果 = 店的 {@link FileHistoryResult};失败臂可再带 `missingBlob` —— 这一次尝试里**重放自己交给店的 getter 亲口答过 `undefined`** 的那枚 hash
92
- * (「这枚 blob 不在」是 getter 观察到的事实,不是从店码里猜的:同一个 `adopt_failed` 也可能是取值抛了 / 发布步撞上库的故障,那两形 `missingBlob` 缺席)。
93
- * 下发口({@link sendFileHistoryReplayFailure} 在路由里)据它铸 wire `missing_blob`,否则原样送店的码。
94
- */
95
- export type FileHistoryReplayResult = {
96
- readonly ok: true;
97
- } | {
98
- readonly ok: false;
99
- readonly error: FileHistoryError;
100
- readonly missingBlob?: string;
101
- };
102
- export type FileHistoryReplayFailure = Extract<FileHistoryReplayResult, {
103
- ok: false;
104
- }>;
105
- /**
106
- * The 2c PUSH replay of a session's file-history graph — keep-SOURCE convergence over core's
107
- * non-destructive-target law. `importHistory` refuses a target that already holds a DIFFERENT graph
108
- * (typed `conflict`; the identical envelope re-publishes idempotently). On that arm, when the store has
109
- * the purge seam, the sync's documented semantic (the destination converges to the source's state, same
110
- * posture as the policy/anchor replays) is applied: wipe the scope, then import once more. Every bundled
111
- * store carries the verb (the SQL twins, and core's local File store from core 7.64.0), so the three backends
112
- * read one rule; a store WITHOUT `deleteBySession` (an embedder's) surfaces the typed conflict unchanged —
113
- * an honest capability limit, never a silent half-state. The wipe throws on a store fault (and core's stores
114
- * refuse it while they run a write of the scope): the throw propagates, nothing is re-imported.
115
- *
116
- * Honest residual (recorded, not hidden): if the RE-import after the wipe fails (a blob corrupted in
117
- * transit past the Phase-A presence check), the destination's old graph is already gone and the target
118
- * is left EMPTY — worse than the moment before this call, but strictly self-healing: the client re-PUTs
119
- * the blob and re-POSTs, and the retry lands on an empty target (importHistory's first-import arm).
120
- * The alternative (keeping the old graph) would wedge every retry on the same typed conflict forever.
121
- *
122
- * 缺 blob 的事实只在这里读一次:每次尝试给店的 getter 都包一层观察,店取到 `undefined` 就记下那枚 hash(按次计,替换臂的第二次尝试从空白起),
123
- * 失败时随结果交出({@link FileHistoryReplayResult.missingBlob})。预检({@link sessionSyncFileHistory} 事实 + 交换组的 `has`)管「这台部署根本收不到 / 索引里没有」,
124
- * 这里管「预检过了而这一枚取不到」(对象存储里已丢 / 校验和不符按缺席读 / 预检与重放之间被孤儿回收清走)—— 同一只不变量的两端,wire 上同一个词。
125
- */
126
- export declare function replayFileHistory(store: ServiceFileHistoryStore, scope: string, envelope: FileHistoryExport, srcGetBlob: BlobGetter, opts: {
127
- allowReplace: boolean;
128
- }): Promise<FileHistoryReplayResult>;
129
82
  /**
130
83
  * The portable state of ONE session, ready to replay into another backend. Blobs are deliberately NOT inlined
131
84
  * (§5: a turn's working-tree blobs can be tens of MiB → OOM/413 if buffered); each snapshot carries only its
@@ -208,51 +161,4 @@ historyStore: ServiceFileHistoryStore): Promise<{
208
161
  * between the source's full durable log and the destination's current log (a `null` dst log ⇒ `fresh`). No writes.
209
162
  */
210
163
  export declare function planSync(sessionId: string, srcBackend: StoreBackend, dstBackend: StoreBackend): Promise<SyncRelation | null>;
211
- /** [ref] ①(死码 A6 修的移植):同内容判据 —— 比较「归一化、去 rev」后的字节形。putRules 每次
212
- * 无条件写都 rev+1,而 rev 是 operator 乐观锁的 CAS 令牌(`PutRulesOptions.expectedRev`),
213
- * 幂等重试/无害重复同步不该把在途 CAS 打失败。归一化用 core 的 `normalizeRules`(store 落盘
214
- * 前走同一函数 ⇒ 两侧同坐标);它略过店的戳 `rev` / `gen`(`listBySession` 的 rules 含 rev,
215
- * 它不是内容;core 7.51.0 [ref] 起 `normalizeRules` 自己略过,修前的 `stripRev` 包裹随之退役)。
216
- * 两侧过的是同一只形读法(core 7.51.0 `readSessionRulesShape`):`incoming` 由写口读者(引擎的写口判官)交出,`stored` 由店的读回交出 ——
217
- * 店读回读不出形的行答 `corrupt`、根本到不了这里,所以 `normalizeRules` 对两侧都不抛(修前本仓的读回比引擎宽,另有一只判官先挡一次)。
218
- * 🔴 字节同 ≠ 意思同(core 7.51.2 行自带文法戳;codex r1 [high] 验真):`normalizeRules` 把店的戳 `rev` / `gen` / `grammar` 都略过,而同一串桶字节在两种文法里是两句话 ——
219
- * 目的行无戳(文法 0:`toolAllow` 里的 `Bash(rm:*)` 什么都不放行)、导入记录带 `grammar: 1`(放行 rm),只比字节会判「同内容」、零次 putRules、同步答 200 而目的行仍是旧意思。
220
- * 所以字节同之外还问引擎的收紧比对 `loosenReasons` **双向**(两侧各在自己的文法里读:`stored` 带 `rev`,`incoming` 带 `grammar`):两个方向都「没放宽」才是同一句话;
221
- * 意思不同就交给 `putRules` 的判官(非 operator ⇒ `loosen_forbidden`;operator ⇒ 改写成当前文法、盖戳)。本仓仍零处读 / 比戳值;意思由引擎说。
222
- * 🔴 core 7.55.0 [ref]:行还有一格 **operator 部分**(`StoredSessionRules.operatorRules`:非 operator 的写只能收回非 operator 写加上去的东西;缺席 = 整行归 operator)。
223
- * 桶同、意思同,这一次写仍可能挪动它 —— operator 重放把整行变成 operator 的(引擎写口判官的答:operator 写的值整只归 operator)。只比桶会判「同内容」⇒ 零次 putRules ⇒
224
- * 属主盖过戳的目的行保持「属主收得回」,源端 operator 断言过的条目在目的端被属主一条非 operator 的写收回(安全轴的静默放宽)。所以第三问:这一次写之后的部分
225
- * = 引擎写口判官对(目的行, 记录, 这次重放的态度)的答,与目的行现有的部分按同一读法(缺席 = 整行)比;不同就交给 `putRules`。判官拒 ⇒ 原样上抛(`putRules` 会拒同一句)。
226
- * `operator` 必填:同一份记录在 operator 与非 operator 的重放下是两种写(漏传是编译错,不是一条静默的读法)。 */
227
- export declare function sameRulesContent(stored: StoredSessionRules | null, incoming: SessionPermissionRules, operator: boolean): boolean;
228
- /** [ref] ① 的重放腿共享实现:PUSH 路由的两条 policy 重放腿(Phase A identical 短路支 / Phase B
229
- * commit 后支)都从这里走 —— 等内容跳过(rev 零漂移),真变更照常落地(E6 tighten-only 门在
230
- * putRules 内部,此处不重述)。 */
231
- export declare function replayPolicyRecords(store: Pick<SessionPolicyStore, "getRules" | "putRules">, sessionId: string, records: readonly SessionRulesRecord[], operatorOk: boolean): Promise<void>;
232
- /** [ref] ② 能力预检(fail-loud 方向,[ref] 判据:保护型能力缺席=fail-closed):consented
233
- * overwrite-dst 需要 policy 店的 `deleteBySession` purge seam 才能不留混合态;缺席=true,路由在
234
- * commit **之前**用它干净中止(此时零写入落地),wire 拒因字面量归路由所有(api-error-text-freeze
235
- * 锚字面站点)。anchor 店的 deleteBySession 是类型必填、history 店有 core 必填的 `reap` 兜底(+
236
- * {@link replayFileHistory} 的 conflict 收敛臂),都不构成缺席面;唯一可缺的就是 policy 这条
237
- * (core 接口上是可选方法)。 */
238
- export declare function overwriteWipeIncapable(policyStore: ServiceSessionPolicyStore | undefined): boolean;
239
- /** [ref] ②(死码 ⓪ 擦除步的移植):consented overwrite-dst = keep-source,结果必须是**源端那
240
- * 一份**,不得混成「源端对话 + 目的端遗留 policy/anchor/历史」(混合态比丢历史更危险:悬空
241
- * anchor 指向已 purge 的 entry、被放弃分支的 policy 行继续 gating、目的端旧历史图挡住源端图的
242
- * importHistory 空目标法则)。调用时点=commit(原子 swap)成功**之后**、附随物重放**之前**:
243
- * 死码在任何写入前擦(其 entries 写本身非原子);活路由有真 commit point,擦在 commit 前会在
244
- * 「commit 失败」时留下反向混合态(目的端对话完好、附随物已毁)——方向更坏。
245
- * 诚实残余:进程在 commit 与本函数之间崩溃时,重试落 Phase A identical 支(无 wipe)⇒ 遗留行
246
- * 可存活;该窗口窄于修前世界(修前=遗留行**恒**存活),与 §8「历史重放失败 ⇒ 422 重试自愈」
247
- * 同款接受,记录于此不装没有。
248
- * 各店姿势:file history=deleteBySession(整 scope 清空;blob 走异步孤儿 GC)缺席则 core 必填
249
- * `reap(scope, [])`(丢全部 boundary;v1 基线按 core 契约留存 —— 随后 {@link replayFileHistory}
250
- * 的 conflict 臂在有 purge 缝的店上收敛,无缝的店如实 422);anchor=直调(类型必填);
251
- * policy=deleteBySession(路由已用 {@link overwriteWipeIncapable} 预检过,此处缺席=类型外的
252
- * JS 层残缺实现,直调抛 TypeError 即 fail-loud)。 */
253
- export declare function wipeSessionAttendants(stores: {
254
- fileHistoryStore?: ServiceFileHistoryStore | undefined;
255
- sessionPolicyStore?: ServiceSessionPolicyStore | undefined;
256
- resumeAnchorStore?: Pick<ResumeAnchorStore, "deleteBySession"> | undefined;
257
- }, sessionId: string): Promise<void>;
258
164
  //# sourceMappingURL=session-sync.d.ts.map
@@ -1,8 +1,7 @@
1
1
  import { anchorEntryDeclared, classifySyncRelationship } from "./session-sync-kernel.js";
2
2
  import { recordFailOpen } from "./observability/fail-open.js";
3
- import { loosenReasons, normalizeRules } from "@sema-agent/core";
4
3
  import { sessionFactsReadOf } from "./http/owner-access.js";
5
- import { fileHistoryBlobExchangeOf, judgeSessionRulesWrite } from "@sema-agent/core";
4
+ import { fileHistoryBlobExchangeOf } from "@sema-agent/core";
6
5
  export class FileHistoryUnreadableError extends Error {
7
6
  sessionId;
8
7
  failure;
@@ -29,35 +28,6 @@ export function blobGetterOf(store) {
29
28
  const exchange = blobExchangeOf(store);
30
29
  return exchange ? exchange.get : async () => undefined;
31
30
  }
32
- export function fileHistoryImportFace(fs) {
33
- const candidate = fs?.importHistory;
34
- if (typeof candidate !== "function")
35
- return undefined;
36
- return { importHistory: candidate.bind(fs) };
37
- }
38
- export async function replayFileHistory(store, scope, envelope, srcGetBlob, opts) {
39
- const face = fileHistoryImportFace(store);
40
- if (face === undefined) {
41
- return { ok: false, error: { code: "unsupported_format", message: "this deployment's file-history store has no importHistory seam" } };
42
- }
43
- const attempt = async () => {
44
- let missing;
45
- const observed = async (hash) => {
46
- const bytes = await srcGetBlob(hash);
47
- if (bytes === undefined && missing === undefined)
48
- missing = hash;
49
- return bytes;
50
- };
51
- const r = await face.importHistory(scope, envelope, observed);
52
- return r.ok || missing === undefined ? r : { ok: false, error: r.error, missingBlob: missing };
53
- };
54
- let r = await attempt();
55
- if (!r.ok && r.error.code === "conflict" && opts.allowReplace && typeof store.deleteBySession === "function") {
56
- await store.deleteBySession(scope);
57
- r = await attempt();
58
- }
59
- return r;
60
- }
61
31
  function ownerAware(backend) {
62
32
  return backend.session();
63
33
  }
@@ -107,44 +77,4 @@ export async function planSync(sessionId, srcBackend, dstBackend) {
107
77
  const dstEntries = (await ownerAware(dstBackend).exportEntries?.(sessionId)) ?? null;
108
78
  return classifySyncRelationship(srcEntries, dstEntries);
109
79
  }
110
- export function sameRulesContent(stored, incoming, operator) {
111
- if (stored === null)
112
- return false;
113
- const norm = (r) => JSON.stringify(normalizeRules(r));
114
- if (norm(stored) !== norm(incoming))
115
- return false;
116
- if (loosenReasons(stored, incoming).length !== 0 || loosenReasons(incoming, stored).length !== 0)
117
- return false;
118
- return operatorPartUnmoved(stored, incoming, operator);
119
- }
120
- function operatorPartUnmoved(stored, incoming, operator) {
121
- const next = judgeSessionRulesWrite(stored, incoming, { operator });
122
- const partOf = (row) => JSON.stringify(normalizeRules(row.operatorRules ?? row));
123
- return partOf(next) === partOf(stored);
124
- }
125
- export async function replayPolicyRecords(store, sessionId, records, operatorOk) {
126
- for (const rec of records) {
127
- const cur = await store.getRules(sessionId, rec.principal);
128
- if (sameRulesContent(cur, rec.rules, operatorOk))
129
- continue;
130
- await store.putRules(sessionId, rec.principal, rec.rules, { operator: operatorOk });
131
- }
132
- }
133
- export function overwriteWipeIncapable(policyStore) {
134
- return policyStore !== undefined && typeof policyStore.deleteBySession !== "function";
135
- }
136
- export async function wipeSessionAttendants(stores, sessionId) {
137
- const fs = stores.fileHistoryStore;
138
- if (fs !== undefined) {
139
- if (typeof fs.deleteBySession === "function")
140
- await fs.deleteBySession(sessionId);
141
- else
142
- await fs.reap(sessionId, []);
143
- }
144
- if (stores.resumeAnchorStore !== undefined)
145
- await stores.resumeAnchorStore.deleteBySession(sessionId);
146
- const policy = stores.sessionPolicyStore;
147
- if (policy !== undefined)
148
- await policy.deleteBySession(sessionId);
149
- }
150
80
  //# sourceMappingURL=session-sync.js.map
@@ -28,9 +28,15 @@
28
28
  */
29
29
  import type { HookLlmCall } from "./hooks/hook-runner.js";
30
30
  import { type SessionTitledFrame } from "./session-titled-frame.js";
31
+ import type { SessionFace } from "./session-incarnation.js";
31
32
  export interface SessionTitlerDeps {
32
- /** Write-once store leg (SQL twins / local sidecar). Resolves false when a title already existed. */
33
- setTitleIfNull(sessionId: string, title: string): Promise<boolean>;
33
+ /** Write-once store leg (SQL twins / local sidecar). Resolves false when a title already existed — or when the session is no longer `landed`
34
+ * (the occupant read at acceptance: the store judges it under the session's row lock with the row-write judge, so a title generated for one
35
+ * occupant never lands on a session deleted and re-created under the same id meanwhile, nor on a freed id). */
36
+ setTitleIfNull(sessionId: string, title: string, landed: SessionFace): Promise<boolean>;
37
+ /** The session's occupant now (the owner accessor's `sessionOccupantReadOf`) — read once, at acceptance, while the accepting leg waits on it
38
+ * (so the run is live and the occupant is that run's); `undefined` ⇒ no row to title. */
39
+ occupantOf(sessionId: string): Promise<SessionFace | undefined>;
34
40
  /** Cheap pre-LLM probe: "titled" = skip (a restart/evicted-seen/another replica
35
41
  * already paid — don't re-spend an LLM call); "untitled" = generate; "none" = the session_meta row hasn't
36
42
  * been REGISTERED (or was deleted meanwhile) — retry after a grace window,
@@ -54,7 +60,8 @@ export interface SessionTitler {
54
60
  /** `model`([ref]②)= 本 turn 真实驱动的模型——带上则标题 hook 同链路,不再裸落 env 缺省。 */
55
61
  /** `onTitled` = 这一次调用**自己**把标题写成之后(`setTitleIfNull` 落行)交出的活通知(`session_titled` 帧,契约 §4a-ttl):只在落行的那一支调用恰一次 ——
56
62
  * 已有标题 / 竞态输了 / 模型失败 / 空标题 / 这个会话已有一次在飞的调用,都不调。回调同步执行、**不许抛**;抛了也不影响已落行的标题(见下 `frame_failed`)。 */
57
- maybeTitle(sessionId: string, objective: string, model?: string, onTitled?: (frame: SessionTitledFrame) => void): void;
63
+ /** Resolves once the occupant to title is pinned (bounded; the accepting leg awaits it), the model call and the write then run in the background. */
64
+ maybeTitle(sessionId: string, objective: string, model?: string, onTitled?: (frame: SessionTitledFrame) => void): Promise<void>;
58
65
  }
59
66
  /** 标题的显示预算(码元,含截断记号)—— 界的属主是出口座的 `session-title` 行,这里读它;落在 `title` 列宽 `SESSION_TITLE_MAX_CHARS`(session-row.ts)之内。 */
60
67
  export declare const TITLE_MAX_CHARS: number;
@@ -2,6 +2,7 @@ import { recordFailOpen } from "./observability/fail-open.js";
2
2
  import { seatMaxChars, seatText } from "./trace/exit-seat.js";
3
3
  import { userAuthoredText } from "./user-authored-text.js";
4
4
  import { buildSessionTitledFrame } from "./session-titled-frame.js";
5
+ import { withinIoDeadline } from "./instruction-source-notice.js";
5
6
  export const TITLE_MAX_CHARS = seatMaxChars("session-title");
6
7
  const OBJECTIVE_SNIPPET_CHARS = 600;
7
8
  const LLM_TIMEOUT_MS = 15_000;
@@ -52,7 +53,7 @@ export function createSessionTitler(deps) {
52
53
  .then((state) => ({ state, unreadable: false }))
53
54
  .catch(() => ({ state: continueWord, unreadable: true }));
54
55
  return {
55
- maybeTitle(sessionId, objective, model, onTitled) {
56
+ async maybeTitle(sessionId, objective, model, onTitled) {
56
57
  if (!sessionId || !userAuthoredText(objective))
57
58
  return;
58
59
  if (seen.has(sessionId))
@@ -63,6 +64,19 @@ export function createSessionTitler(deps) {
63
64
  seen.delete(oldest);
64
65
  }
65
66
  seen.add(sessionId);
67
+ let landed;
68
+ try {
69
+ landed = await withinIoDeadline(() => deps.occupantOf(sessionId));
70
+ }
71
+ catch {
72
+ landed = undefined;
73
+ }
74
+ if (landed === undefined) {
75
+ seen.delete(sessionId);
76
+ deps.metrics?.inc("session_title_total", { outcome: "row_not_yet" });
77
+ return;
78
+ }
79
+ const occupant = landed;
66
80
  void (async () => {
67
81
  let state = (await probeOrContinue(sessionId, "untitled")).state;
68
82
  if (state === "none") {
@@ -90,12 +104,11 @@ export function createSessionTitler(deps) {
90
104
  deps.metrics?.inc("session_title_total", { outcome: "empty" });
91
105
  return;
92
106
  }
93
- const landed = await deps.setTitleIfNull(sessionId, title);
94
- if (!landed) {
107
+ const wrote = await deps.setTitleIfNull(sessionId, title, occupant);
108
+ if (!wrote) {
95
109
  const after = await probeOrContinue(sessionId, "none");
96
- if (after.state !== "untitled")
97
- seen.delete(sessionId);
98
- deps.metrics?.inc("session_title_total", { outcome: after.unreadable ? "probe_failed" : after.state === "none" ? "row_not_yet" : "already_titled" });
110
+ seen.delete(sessionId);
111
+ deps.metrics?.inc("session_title_total", { outcome: after.unreadable ? "probe_failed" : after.state === "none" ? "row_not_yet" : after.state === "untitled" ? "occupant_changed" : "already_titled" });
99
112
  return;
100
113
  }
101
114
  seen.delete(sessionId);
@@ -36,12 +36,17 @@ export interface SshHostKeyPolicy {
36
36
  knownHostsPath?: string;
37
37
  }
38
38
  /** {@link createSshHostKeyVerifier} 的判词。`verified:false` = 两旋钮都缺席那一格(照连,但响亮)。 */
39
+ /**
40
+ * 判官的一次判。拒的那一臂带 `standing`:对面出示的密钥被钉判了不过(指纹不符 / 条目换了钥 / `@revoked` / 没有可用条目)⇒ `true` —— 同一配置同一主机每次同答;
41
+ * 钉的文件此刻读不出 ⇒ `false`(这一次判不了,文件可能正被换写,下一次连接重判)。
42
+ */
39
43
  export type SshHostKeyVerdict = {
40
44
  ok: true;
41
45
  verified: boolean;
42
46
  } | {
43
47
  ok: false;
44
48
  reason: string;
49
+ standing: boolean;
45
50
  };
46
51
  /** SHA256 base64 指纹的**归一**:`SHA256:` 前缀可选、`=` 填充可选;形不合 ⇒ **抛**(调用方在 boot 期拒启)。 */
47
52
  export declare function normalizeSshFingerprint(raw: string): string;
@@ -82,6 +82,7 @@ export function createSshHostKeyVerifier(opts) {
82
82
  ok: false,
83
83
  reason: `SSH host key rejected for ${host}:${port} — fingerprint mismatch. presented=${presented} expected=SHA256:${fingerprint} (SSH_HOST_FINGERPRINT). ` +
84
84
  keyscanHint(host, port),
85
+ standing: true,
85
86
  };
86
87
  }
87
88
  if (knownHostsPath !== undefined) {
@@ -95,6 +96,7 @@ export function createSshHostKeyVerifier(opts) {
95
96
  reason: `SSH host key rejected for ${host}:${port} — SSH_KNOWN_HOSTS (${knownHostsPath}) could not be read at connect time: ` +
96
97
  `${e instanceof Error ? e.message : String(e)}. presented=${presented}. ` +
97
98
  keyscanHint(host, port),
99
+ standing: false,
98
100
  };
99
101
  }
100
102
  const verdict = matchKnownHosts(text, host, port, key.toString("base64"));
@@ -108,6 +110,7 @@ export function createSshHostKeyVerifier(opts) {
108
110
  ok: false,
109
111
  reason: `SSH host key rejected for ${host}:${port} — ${why}. presented=${presented} source=SSH_KNOWN_HOSTS:${knownHostsPath}. ` +
110
112
  keyscanHint(host, port),
113
+ standing: true,
111
114
  };
112
115
  }
113
116
  }
@@ -1,15 +1,26 @@
1
1
  import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import { type FileHistoryRetentionPolicy, type ExecutionEnv, type FileHistoryStore, type FileHistoryResult, type FileHistoryTrackResult, type FileHistoryRestoreResult, type FileHistoryExport, type FileHistoryExportResult } from "@sema-agent/core";
3
+ import { type FileHistoryRetentionPolicy, type ExecutionEnv, type FileHistoryStore, type FileHistoryError, type FileHistoryResult, type FileHistoryTrackResult, type FileHistoryRestoreResult, type FileHistoryExport, type FileHistoryExportResult } from "@sema-agent/core";
4
4
  import type { BlobBackend } from "../plugins/blob-backend.js";
5
5
  import { SqlBlobIndex } from "./blob-index-sql.js";
6
6
  import type { ServiceBlobExchange } from "../plugins/store-backend.js";
7
- import { type SqlDriver, type SqlDialect } from "../plugins/sql-driver.js";
7
+ import { type SqlDriver, type SqlTxConn, type SqlDialect } from "../plugins/sql-driver.js";
8
8
  /** The file-history blob domain's SQL table (this store's blob index + the A7 assert). */
9
9
  export declare const FILE_HISTORY_BLOB_TABLE = "file_history_blob";
10
10
  /** MinIO object-key prefix for this domain (snapshot epoch used "blobs/"; a separate prefix keeps the
11
11
  * two domains' objects disjoint so neither sweep can see the other's keys). */
12
12
  export declare const FILE_HISTORY_BLOB_KEY_PREFIX = "file-history/";
13
+ /**
14
+ * {@link SqlFileHistoryStore.prepareImport} 的答:门 / 取值校验的拒(`ok: false`,店码),或这一份信封落进目标要做什么 —— `same`(目标已是这一张图:不写)/
15
+ * `publish`(目标空,或 `replace` 下目标是另一张图:发布步写)。发布步({@link SqlFileHistoryStore.publishImportOn})在调用方的事务里按它做。
16
+ */
17
+ export type FileHistoryImportPlan = {
18
+ readonly ok: false;
19
+ readonly error: FileHistoryError;
20
+ } | {
21
+ readonly ok: true;
22
+ readonly mode: "same" | "publish";
23
+ };
13
24
  /**
14
25
  * [ref] twin 保留条款(core 7.0.0 [ref] §片2 的 SQL 孪生)—— 构造期旋钮,键形与 core 内置后端
15
26
  * `FileFileHistoryStoreOptions.retention` / `FileStorageBackendOptions.fileHistory.retention` **同域同名**。
@@ -89,6 +100,13 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
89
100
  * The lock order itself stays single: sentinels first (sorted), then the scope's rows.
90
101
  */
91
102
  private lockScope;
103
+ /**
104
+ * The ensure-and-lock of {@link lockScope} **without** its restart — the statement source a transaction someone else opened runs (the
105
+ * landed section: its first statements lock the session's root row and judge the occupant, so a restart here would drop that lock and
106
+ * that judgement). A lost arbitration (a deadlock victim, or {@link ScopeSentinelVanishedError}, a `SqlArbitrationLostError`) propagates;
107
+ * the transaction's owner restarts from ITS top. Order: sentinels sorted, then the scope's rows (the one lock order).
108
+ */
109
+ lockScopeOn(conn: SqlTxConn, scopes: readonly string[]): Promise<void>;
92
110
  /** Returns the UPDATE's affected count — 0 = the record is gone (annulled) or was already observed;
93
111
  * the caller disambiguates by re-reading (codex R1-high: answering minted:false after the baseline
94
112
  * vanished would let a later touch mint already-modified bytes as pristine). */
@@ -164,6 +182,12 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
164
182
  * collected by the async grace-window sweep (never a synchronous external-store delete on the purge
165
183
  * path). Also the overwrite-dst session-sync wipe seam (import requires an EMPTY target graph). */
166
184
  deleteBySession(scope: string): Promise<number>;
185
+ /** The whole-scope removal as a statement source on the caller's transaction ({@link lockScopeOn} + the six tables, sentinel last):
186
+ * {@link deleteBySession}'s body, and the landed section's wipe (one transaction with the session's root-row lock). Returns rows removed. */
187
+ deleteScopeOn(conn: SqlTxConn, scope: string): Promise<number>;
188
+ /** Delete one scope's rows from `tables` (caller holds the scope's sentinel lock). 第六表 `file_history_reaped_entry`(spent-entry 台账)随 scope 一起走:
189
+ * E21 是删除权,台账不是例外。 */
190
+ private deleteScopeRowsOn;
167
191
  /** Async byte-GC: blobs referenced by NO version row (any scope), past the created_at grace window.
168
192
  * Domain-isolated by construction (this store's blob table only — [ref] D1). Reaper-driven. */
169
193
  sweepOrphanBlobs(): Promise<number>;
@@ -190,6 +214,32 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
190
214
  */
191
215
  private verifySourceBlobs;
192
216
  importHistory(scope: string, data: FileHistoryExport, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>): Promise<FileHistoryResult>;
217
+ /**
218
+ * 导入接住的一只故障的**唯一**折法(本店的图事务、准备步与落定段的发布步同读):core 7.55.0 —— `unsupported_format` 只说坏信封(或它的形驱动的活),
219
+ * 门之后撞上的故障(库拒了一份好图 / 重来用尽 / 连接断)是 `adopt_failed`,带接住的那只故障判得出的 `permanence`(判不出 ⇒ 不带)。
220
+ */
221
+ importFailureOf(err: unknown): FileHistoryError;
222
+ /**
223
+ * 导入的**事务外半**(`importHistory` 与落定段的重放同经它):门(core 的共享校验 + 本店的 mode 值域)→ 预读目标图(读不铸哨兵)判同图 / 异图 → 取值校验信封引用的每一枚 blob
224
+ * (首推 / 替换:校验过的字节续租进 blob 后端,发布前的孤儿清扫不收走它们;同图:只校验不回写)。`replace`(consented overwrite-dst 的落定重放)⇒ 异图不拒,按首推准备。
225
+ * 任何拒 ⇒ `ok: false`(店码);一个字节没发布。
226
+ */
227
+ prepareImport(scope: string, data: FileHistoryExport, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>, opts: {
228
+ readonly replace: boolean;
229
+ }): Promise<FileHistoryImportPlan>;
230
+ /**
231
+ * 导入的**发布步**,一只语句源跑在调用方的事务里(`importHistory` 自己的图事务 / 落定段):`same` ⇒ 不写;`publish` ⇒ scope 锁({@link lockScopeOn},不自己重开事务)→
232
+ * 事务内空目标复判(非破坏性目标律的 TOCTOU 关)→ 空 ⇒ 整图一步插入;已是这张图 ⇒ 不写;另一张图 ⇒ `replace` 下先删这一 scope 的图行(五表,哨兵留着 —— 本事务持着它)
233
+ * 再插(删与插同一事务:替换不会把目标留成空),否则 `conflict`、一个字节没写。
234
+ */
235
+ publishImportOn(conn: SqlTxConn, scope: string, data: FileHistoryExport, plan: Extract<FileHistoryImportPlan, {
236
+ ok: true;
237
+ }>, opts: {
238
+ readonly replace: boolean;
239
+ }): Promise<FileHistoryResult>;
240
+ /** The publish step's body under a held scope lock ({@link importHistory}: {@link lockScope} in the store's own transaction · {@link publishImportOn}:
241
+ * {@link lockScopeOn} in the landed section). */
242
+ private publishImportLockedOn;
193
243
  }
194
244
  /** MySQL-protocol (TiDB) binding. */
195
245
  export declare class TiDBFileHistoryStore extends SqlFileHistoryStore {