@sema-agent/server 7.139.0 → 7.141.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 (328) hide show
  1. package/MIGRATION.md +471 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +94 -26
  5. package/dist/adoption/runner.js +1 -1
  6. package/dist/adoption/sql.js +1 -1
  7. package/dist/approval-batch-census.d.ts +27 -0
  8. package/dist/approval-batch-census.js +27 -0
  9. package/dist/approval-card.d.ts +37 -22
  10. package/dist/approval-card.js +6 -3
  11. package/dist/approval.d.ts +15 -0
  12. package/dist/approval.js +10 -0
  13. package/dist/audit.d.ts +4 -2
  14. package/dist/audit.js +2 -2
  15. package/dist/bench/s1/live-deps.js +3 -2
  16. package/dist/bench/s1/row.d.ts +5 -2
  17. package/dist/bench/s1/row.js +2 -22
  18. package/dist/boot/adoption.js +1 -1
  19. package/dist/boot/device-lane.d.ts +11 -0
  20. package/dist/boot/device-lane.js +16 -0
  21. package/dist/boot/engine-lease.d.ts +24 -3
  22. package/dist/boot/engine-lease.js +9 -2
  23. package/dist/boot/execution-env.js +18 -12
  24. package/dist/boot/execution-lane.d.ts +8 -0
  25. package/dist/boot/execution-lane.js +6 -1
  26. package/dist/boot/memory-boundary.d.ts +16 -53
  27. package/dist/boot/memory-boundary.js +10 -20
  28. package/dist/boot/memory-consolidation.d.ts +16 -12
  29. package/dist/boot/memory-consolidation.js +24 -13
  30. package/dist/boot/reapers.d.ts +2 -2
  31. package/dist/boot/resolve-spec.d.ts +3 -5
  32. package/dist/boot/resolve-spec.js +9 -8
  33. package/dist/boot/runner-deps.d.ts +3 -3
  34. package/dist/boot/runner-deps.js +1 -0
  35. package/dist/boot/session-faces.d.ts +1 -1
  36. package/dist/boot/session-faces.js +4 -3
  37. package/dist/boot/shutdown.js +5 -0
  38. package/dist/boot/stage-03-store-backend.d.ts +2 -6
  39. package/dist/boot/stage-03-store-backend.js +2 -3
  40. package/dist/boot/stage-04-stores.d.ts +2 -6
  41. package/dist/boot/stage-04-stores.js +5 -5
  42. package/dist/boot/stage-05-execution-env.d.ts +2 -6
  43. package/dist/boot/stage-05-execution-env.js +3 -1
  44. package/dist/boot/stage-06-runners.d.ts +2 -8
  45. package/dist/boot/stage-06-runners.js +3 -3
  46. package/dist/boot/stage-07-capability-layer.d.ts +2 -7
  47. package/dist/boot/stage-07-capability-layer.js +8 -10
  48. package/dist/boot/stage-08-reapers.d.ts +2 -7
  49. package/dist/boot/stage-09-leader.d.ts +2 -7
  50. package/dist/boot/stage-10-http-server.d.ts +2 -7
  51. package/dist/boot/stage-10-http-server.js +6 -4
  52. package/dist/boot/stores.d.ts +3 -6
  53. package/dist/boot/stores.js +37 -44
  54. package/dist/boot/task-list-lane.d.ts +1 -1
  55. package/dist/boot/task-list-lane.js +1 -1
  56. package/dist/boot/web-admin.js +36 -9
  57. package/dist/capabilities/execution-lane.d.ts +2 -2
  58. package/dist/capabilities/scenarios.d.ts +1 -1
  59. package/dist/capability-facts.d.ts +9 -0
  60. package/dist/capability-facts.js +9 -0
  61. package/dist/card-void.d.ts +38 -0
  62. package/dist/card-void.js +23 -0
  63. package/dist/config-catalog.js +12 -8
  64. package/dist/config-provider.d.ts +18 -17
  65. package/dist/config-provider.js +28 -36
  66. package/dist/config-types.d.ts +36 -9
  67. package/dist/config.js +56 -46
  68. package/dist/delegated-session-owner.d.ts +23 -20
  69. package/dist/delegated-session-owner.js +2 -2
  70. package/dist/deployment-shape.d.ts +9 -8
  71. package/dist/deployment-shape.js +4 -4
  72. package/dist/device-store.d.ts +7 -0
  73. package/dist/device-store.js +3 -0
  74. package/dist/execution-config-root.d.ts +22 -0
  75. package/dist/execution-config-root.js +35 -0
  76. package/dist/execution-lane-caps.d.ts +145 -107
  77. package/dist/execution-lane-caps.js +144 -83
  78. package/dist/execution-placement.d.ts +3 -0
  79. package/dist/execution-placement.js +7 -0
  80. package/dist/fleet/drain-entry.d.ts +3 -0
  81. package/dist/hosted-posture.d.ts +8 -0
  82. package/dist/hosted-posture.js +4 -0
  83. package/dist/http/admission.js +3 -3
  84. package/dist/http/caller-trust.d.ts +3 -2
  85. package/dist/http/caller-trust.js +2 -1
  86. package/dist/http/dispatch.js +4 -0
  87. package/dist/http/live-frame.d.ts +24 -0
  88. package/dist/http/live-frame.js +16 -0
  89. package/dist/http/memory-compliance-failure-reply.d.ts +3 -1
  90. package/dist/http/memory-compliance-failure-reply.js +5 -0
  91. package/dist/http/memory-layer-reply.d.ts +30 -0
  92. package/dist/http/memory-layer-reply.js +24 -0
  93. package/dist/http/resume-legs.js +49 -35
  94. package/dist/http/retired-service-deps.d.ts +1 -1
  95. package/dist/http/retired-service-deps.js +1 -1
  96. package/dist/http/route-table.js +2 -0
  97. package/dist/http/routes/adoption.js +1 -1
  98. package/dist/http/routes/approvals-assistant.js +1 -0
  99. package/dist/http/routes/capabilities.js +5 -4
  100. package/dist/http/routes/diagnostics.d.ts +0 -19
  101. package/dist/http/routes/diagnostics.js +16 -2
  102. package/dist/http/routes/memory-bundle.d.ts +2 -2
  103. package/dist/http/routes/memory-bundle.js +4 -1
  104. package/dist/http/routes/memory-compliance.js +9 -2
  105. package/dist/http/routes/memory-consolidation.js +12 -1
  106. package/dist/http/routes/memory-origin.js +14 -2
  107. package/dist/http/routes/memory-policy.js +7 -2
  108. package/dist/http/routes/run-permission-mode.d.ts +75 -0
  109. package/dist/http/routes/run-permission-mode.js +170 -0
  110. package/dist/http/routes/session-sync.d.ts +3 -6
  111. package/dist/http/routes/session-sync.js +7 -2
  112. package/dist/http/routes/sessions.js +33 -15
  113. package/dist/http/routes/tasks.js +67 -68
  114. package/dist/http/run-read-face.js +3 -3
  115. package/dist/http/send.d.ts +1 -1
  116. package/dist/http/send.js +2 -2
  117. package/dist/http/server.d.ts +14 -27
  118. package/dist/http/server.js +2 -2
  119. package/dist/http/sse-log.d.ts +3 -8
  120. package/dist/http/stored-body-replay.d.ts +3 -3
  121. package/dist/http/stored-body-replay.js +3 -1
  122. package/dist/identity-source.d.ts +1 -1
  123. package/dist/identity-source.js +1 -1
  124. package/dist/instruction-source-notice.d.ts +7 -0
  125. package/dist/instruction-source-notice.js +6 -6
  126. package/dist/lane-home-probe.d.ts +68 -42
  127. package/dist/lane-home-probe.js +53 -30
  128. package/dist/leader/wire.js +5 -4
  129. package/dist/mcp-assertion-header.d.ts +4 -0
  130. package/dist/mcp-assertion-header.js +5 -0
  131. package/dist/memory-bundle-engine.d.ts +16 -9
  132. package/dist/memory-bundle-engine.js +29 -6
  133. package/dist/memory-operator-faces.d.ts +85 -30
  134. package/dist/memory-operator-faces.js +136 -30
  135. package/dist/memory-posture.d.ts +1 -1
  136. package/dist/memory-project-layer.d.ts +3 -21
  137. package/dist/memory-project-layer.js +11 -8
  138. package/dist/memory-scope-stores.d.ts +73 -0
  139. package/dist/memory-scope-stores.js +86 -0
  140. package/dist/memory-scope.d.ts +33 -9
  141. package/dist/memory-scope.js +17 -5
  142. package/dist/memory-sync-client.d.ts +34 -9
  143. package/dist/memory-sync-client.js +108 -28
  144. package/dist/observability/fail-open.d.ts +30 -2
  145. package/dist/observability/fail-open.js +30 -2
  146. package/dist/observability/metrics.js +0 -1
  147. package/dist/observability/run-terminal.d.ts +6 -2
  148. package/dist/observability/run-terminal.js +25 -26
  149. package/dist/observability/secret-env-scrub.d.ts +20 -1
  150. package/dist/observability/secret-env-scrub.js +14 -0
  151. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -2
  152. package/dist/orchestration/workflow-completion-inbox.js +13 -5
  153. package/dist/orchestration/workflow-notify-journal.d.ts +5 -3
  154. package/dist/orchestration/workflow-notify-journal.js +2 -1
  155. package/dist/permission-mode-admission.d.ts +68 -0
  156. package/dist/permission-mode-admission.js +23 -0
  157. package/dist/permission-rule-list.d.ts +55 -6
  158. package/dist/permission-rule-list.js +52 -5
  159. package/dist/plugins/adoption-log-sql.js +4 -4
  160. package/dist/plugins/approval-ask-store-sql.js +14 -14
  161. package/dist/plugins/approval-nonce-store.js +4 -4
  162. package/dist/plugins/blob-backend.d.ts +4 -4
  163. package/dist/plugins/blob-backend.js +7 -7
  164. package/dist/plugins/breaker-state-sql.js +2 -2
  165. package/dist/plugins/caching-session-store.d.ts +6 -0
  166. package/dist/plugins/caching-session-store.js +3 -0
  167. package/dist/plugins/checkpoint-index-columns.js +2 -2
  168. package/dist/plugins/checkpoint-store-sql.d.ts +65 -17
  169. package/dist/plugins/checkpoint-store-sql.js +90 -19
  170. package/dist/plugins/device-store-sql.d.ts +5 -2
  171. package/dist/plugins/device-store-sql.js +23 -20
  172. package/dist/plugins/ensure-index.d.ts +1 -2
  173. package/dist/plugins/execution-placement-sql.d.ts +19 -9
  174. package/dist/plugins/execution-placement-sql.js +20 -9
  175. package/dist/plugins/file-history-store-sql.d.ts +2 -2
  176. package/dist/plugins/file-history-store-sql.js +4 -4
  177. package/dist/plugins/file-run-store.d.ts +2 -2
  178. package/dist/plugins/file-run-store.js +1 -1
  179. package/dist/plugins/fork-routing-session-store.d.ts +10 -32
  180. package/dist/plugins/fork-routing-session-store.js +2 -61
  181. package/dist/plugins/image-bake-store-sql.js +9 -9
  182. package/dist/plugins/image-index-sql.js +11 -11
  183. package/dist/plugins/k8s-exec-protocol.d.ts +4 -1
  184. package/dist/plugins/k8s-exec-protocol.js +6 -1
  185. package/dist/plugins/leader-run-store-sql.js +4 -4
  186. package/dist/plugins/local-checkpoint-store.d.ts +10 -1
  187. package/dist/plugins/local-checkpoint-store.js +22 -0
  188. package/dist/plugins/local-session-store.d.ts +227 -35
  189. package/dist/plugins/local-session-store.js +518 -37
  190. package/dist/plugins/local-session-window.d.ts +17 -3
  191. package/dist/plugins/local-session-window.js +7 -13
  192. package/dist/plugins/mailbox-store-sql.d.ts +2 -1
  193. package/dist/plugins/mailbox-store-sql.js +4 -4
  194. package/dist/plugins/memory-engine-tidb.js +5 -5
  195. package/dist/plugins/memory-optout-grant-store-sql.js +1 -1
  196. package/dist/plugins/memory-run-store.d.ts +2 -2
  197. package/dist/plugins/memory-run-store.js +1 -1
  198. package/dist/plugins/memory-sync-store-tidb.js +3 -3
  199. package/dist/plugins/one-time-migrations.d.ts +52 -11
  200. package/dist/plugins/one-time-migrations.js +101 -39
  201. package/dist/plugins/org-delivery-quota-store.js +2 -2
  202. package/dist/plugins/outcome-ledger-sql.js +2 -2
  203. package/dist/plugins/permission-rule-store-sql.d.ts +4 -4
  204. package/dist/plugins/permission-rule-store-sql.js +16 -16
  205. package/dist/plugins/pg-pool.js +3 -2
  206. package/dist/plugins/pg-session-storage.d.ts +2 -0
  207. package/dist/plugins/pg-session-storage.js +3 -2
  208. package/dist/plugins/remote-env-adb.d.ts +7 -24
  209. package/dist/plugins/remote-env-adb.js +22 -5
  210. package/dist/plugins/remote-env-device.d.ts +2 -28
  211. package/dist/plugins/remote-env-device.js +2 -1
  212. package/dist/plugins/remote-env-e2b.d.ts +40 -27
  213. package/dist/plugins/remote-env-e2b.js +180 -20
  214. package/dist/plugins/remote-env-file-error.d.ts +1 -1
  215. package/dist/plugins/remote-env-file-error.js +8 -2
  216. package/dist/plugins/remote-env-host.d.ts +19 -35
  217. package/dist/plugins/remote-env-host.js +26 -3
  218. package/dist/plugins/remote-env-k8s.d.ts +22 -14
  219. package/dist/plugins/remote-env-k8s.js +150 -26
  220. package/dist/plugins/remote-env-local-docker.d.ts +27 -24
  221. package/dist/plugins/remote-env-local-docker.js +91 -24
  222. package/dist/plugins/remote-env-ssh.d.ts +4 -24
  223. package/dist/plugins/remote-env-ssh.js +4 -3
  224. package/dist/plugins/resume-anchor-store-sql.js +2 -2
  225. package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
  226. package/dist/plugins/retention-lane-store-sql.js +2 -2
  227. package/dist/plugins/retention-store-sql.d.ts +1 -1
  228. package/dist/plugins/retention-store-sql.js +6 -6
  229. package/dist/plugins/run-store-sql.d.ts +3 -3
  230. package/dist/plugins/run-store-sql.js +19 -18
  231. package/dist/plugins/sandbox-read-floor.d.ts +273 -0
  232. package/dist/plugins/sandbox-read-floor.js +608 -0
  233. package/dist/plugins/schema-consistency.d.ts +6 -6
  234. package/dist/plugins/schema-consistency.js +1 -1
  235. package/dist/plugins/schema-ensure-legs.d.ts +2 -3
  236. package/dist/plugins/schema-ensure-legs.js +2 -2
  237. package/dist/plugins/schema-live-columns.d.ts +1 -1
  238. package/dist/plugins/session-capture-record-store-sql.js +1 -1
  239. package/dist/plugins/session-ownership.d.ts +2 -1
  240. package/dist/plugins/session-placement.d.ts +17 -4
  241. package/dist/plugins/session-placement.js +2 -1
  242. package/dist/plugins/session-rules-column.d.ts +1 -1
  243. package/dist/plugins/session-rules-column.js +1 -1
  244. package/dist/plugins/session-store.d.ts +18 -20
  245. package/dist/plugins/session-store.js +4 -12
  246. package/dist/plugins/session-workspace-cell.d.ts +1 -1
  247. package/dist/plugins/session-workspace-cell.js +1 -1
  248. package/dist/plugins/shared-memory-store-sql.js +2 -2
  249. package/dist/plugins/sql-driver.d.ts +10 -5
  250. package/dist/plugins/sql-driver.js +6 -6
  251. package/dist/plugins/sql-errors.js +1 -1
  252. package/dist/plugins/store-backend.d.ts +35 -6
  253. package/dist/plugins/store-backend.js +27 -10
  254. package/dist/plugins/store-contracts.d.ts +9 -2
  255. package/dist/plugins/task-list-store-sql.js +11 -11
  256. package/dist/plugins/tidb-pool.js +7 -6
  257. package/dist/plugins/tidb-session-storage.js +4 -4
  258. package/dist/plugins/tidb-session-store.d.ts +4 -1
  259. package/dist/plugins/tidb-session-store.js +16 -15
  260. package/dist/plugins/tool-result-store-sql.d.ts +2 -2
  261. package/dist/plugins/tool-result-store-sql.js +3 -3
  262. package/dist/plugins/user-input-ledger-sql.js +2 -2
  263. package/dist/plugins/workflow-journal-store-sql.js +5 -5
  264. package/dist/plugins/workflow-run-store-sql.js +19 -19
  265. package/dist/plugins/write-behind-counter.d.ts +2 -3
  266. package/dist/plugins/write-behind-counter.js +5 -5
  267. package/dist/resource-suspend.d.ts +14 -12
  268. package/dist/resource-suspend.js +1 -1
  269. package/dist/resource-window.d.ts +4 -3
  270. package/dist/run-cancel-context.d.ts +34 -0
  271. package/dist/run-cancel-context.js +7 -0
  272. package/dist/run-local.js +14 -19
  273. package/dist/runs.d.ts +47 -16
  274. package/dist/runs.js +112 -113
  275. package/dist/security.d.ts +2 -2
  276. package/dist/server-secret-env.d.ts +2 -1
  277. package/dist/server-secret-env.js +2 -4
  278. package/dist/session-error-wire.d.ts +22 -4
  279. package/dist/session-error-wire.js +16 -0
  280. package/dist/session-incarnation.d.ts +10 -4
  281. package/dist/session-incarnation.js +1 -1
  282. package/dist/session-policy-wire.d.ts +1 -1
  283. package/dist/session-sync-kernel.d.ts +42 -1
  284. package/dist/session-sync-kernel.js +17 -0
  285. package/dist/session-sync.js +16 -4
  286. package/dist/sql-engine-coordinates.d.ts +49 -0
  287. package/dist/sql-engine-coordinates.js +27 -0
  288. package/dist/sql-engine-posture.d.ts +1 -1
  289. package/dist/sql-engine-posture.js +1 -1
  290. package/dist/standing-answer-grant.d.ts +31 -2
  291. package/dist/standing-answer-grant.js +14 -2
  292. package/dist/store-posture.d.ts +1 -1
  293. package/dist/{plugins → stores-sql}/background-agent-store-sql.d.ts +2 -2
  294. package/dist/{plugins → stores-sql}/background-agent-store-sql.js +8 -8
  295. package/dist/{plugins → stores-sql}/session-policy-store-sql.d.ts +7 -5
  296. package/dist/{plugins → stores-sql}/session-policy-store-sql.js +12 -12
  297. package/dist/{plugins → stores-sql}/usage-window-store-sql.d.ts +1 -1
  298. package/dist/{plugins → stores-sql}/usage-window-store-sql.js +3 -3
  299. package/dist/task-a2a.d.ts +1 -1
  300. package/dist/task-a2a.js +5 -6
  301. package/dist/task-mcp.d.ts +21 -8
  302. package/dist/task-mcp.js +17 -9
  303. package/dist/task-settings.d.ts +52 -25
  304. package/dist/task-settings.js +91 -28
  305. package/dist/terminal.d.ts +103 -19
  306. package/dist/terminal.js +72 -20
  307. package/dist/trace/core-keyset-guard.d.ts +79 -2
  308. package/dist/trace/exit-seat.d.ts +7 -0
  309. package/dist/trace/exit-seat.js +1 -0
  310. package/dist/trace/model-segment.d.ts +2 -2
  311. package/dist/trace/redact.d.ts +3 -1
  312. package/dist/trace/redact.js +395 -110
  313. package/dist/trace/self-minted-wire.d.ts +226 -2
  314. package/dist/trace/self-minted-wire.js +79 -0
  315. package/dist/verifier-pause-card.d.ts +8 -0
  316. package/dist/verifier-pause-card.js +19 -0
  317. package/dist/web-admin-client/apply-effective.d.ts +5 -7
  318. package/dist/web-admin-client/apply-effective.js +2 -2
  319. package/dist/web-admin-client/candidate-gate.d.ts +11 -7
  320. package/dist/web-admin-client/candidate-gate.js +1 -1
  321. package/dist/web-admin-client/http-client.d.ts +5 -4
  322. package/dist/web-admin-client/http-client.js +2 -2
  323. package/dist/web-admin-client/read-warnings.d.ts +7 -17
  324. package/dist/web-admin-client/read-warnings.js +5 -9
  325. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -2
  326. package/package.json +2 -2
  327. package/wire-changes/server@7.140.0.json +184 -0
  328. package/wire-changes/server@7.141.0.json +367 -0
@@ -1,13 +1,21 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
2
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { dirname, join, resolve } from "node:path";
3
6
  import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
4
- import { FileSessionRepo, SessionError, StoredSession, sessionLogMarkOf, settledForkOptions, validateEntriesForImport } from "@sema-agent/core";
7
+ import { StampedSessionArchiveError } from "../session-error-wire.js";
8
+ import { FileSessionRepo, SessionError, StoredSession, admitSessionPlacement, atomicWriteFile, sessionLogMarkOf, settledForkOptions, uuidv7, validateEntriesForImport } from "@sema-agent/core";
9
+ import { createPlacedSessionRow, sourceFacesVia } from "../delegated-session-owner.js";
10
+ import { placementCapabilityOf } from "./session-placement.js";
5
11
  import { z } from "zod";
6
12
  import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
13
+ import { StoreBootRefusalError } from "./store-contracts.js";
7
14
  import { judgeSessionRowWrite, sessionRowTagKey, settleSessionRowWrite, } from "../session-row.js";
8
15
  import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
9
16
  import { createZodColumnReader, decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
10
17
  import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./one-time-migrations.js";
18
+ import { recordFailOpen } from "../observability/fail-open.js";
11
19
  import { createLogger } from "../observability/logger.js";
12
20
  import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
13
21
  import { UnreadableLeaf, carriedRowCells, headFromRow, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
@@ -73,12 +81,44 @@ const SIDECAR_DEGRADES = { lastActivity: "recency", title: "title", pinned: "pin
73
81
  const readPinnedSidecar = createZodColumnReader(z.record(z.string(), z.literal(true)));
74
82
  const MIRROR_CELL = { role: "mirror" };
75
83
  function readSessionMetaCell(cell) {
76
- return typeof cell === "string" ? { incarnation: cell, owner: undefined, execution: undefined, forkKey: undefined } : { incarnation: cell.incarnation, owner: cell.owner, execution: cell.execution, forkKey: cell.forkKey };
84
+ return typeof cell === "string"
85
+ ? { incarnation: cell, owner: undefined, execution: undefined, forkKey: undefined, createdAt: undefined, forkedFrom: undefined }
86
+ : { incarnation: cell.incarnation, owner: cell.owner, execution: cell.execution, forkKey: cell.forkKey, createdAt: cell.createdAt, forkedFrom: cell.forkedFrom };
77
87
  }
78
88
  const ownerRecorded = (cell) => cell !== undefined && readSessionMetaCell(cell).owner !== undefined;
89
+ const cellObjectOf = (cell) => (typeof cell === "string" ? { incarnation: cell } : cell);
79
90
  function cellWith(before, bits) {
80
- const base = typeof before === "string" ? { incarnation: before } : before;
81
- return { ...base, ...("owner" in bits ? { owner: bits.owner } : {}), ...(bits.execution !== undefined ? { execution: bits.execution } : {}) };
91
+ return { ...cellObjectOf(before), ...("owner" in bits ? { owner: bits.owner } : {}), ...(bits.execution !== undefined ? { execution: bits.execution } : {}) };
92
+ }
93
+ const rowBornNow = () => new Date().toISOString();
94
+ function archivedRowFactsOf(head) {
95
+ const createdAt = head.createdAt;
96
+ const forkedFrom = head.forkedFrom;
97
+ return {
98
+ createdAt: typeof createdAt === "string" && createdAt !== "" ? createdAt : ROW_CREATED_AT_ABSENT,
99
+ ...(typeof forkedFrom === "string" && forkedFrom !== "" ? { forkedFrom } : {}),
100
+ };
101
+ }
102
+ function rowFactsFilled(before, facts) {
103
+ if (before === undefined)
104
+ return { incarnation: mintSessionIncarnation(), createdAt: facts.createdAt, ...(facts.forkedFrom !== undefined ? { forkedFrom: facts.forkedFrom } : {}) };
105
+ const held = cellObjectOf(before);
106
+ const add = {
107
+ ...(held.createdAt === undefined ? { createdAt: facts.createdAt } : {}),
108
+ ...(held.forkedFrom === undefined && facts.forkedFrom !== undefined ? { forkedFrom: facts.forkedFrom } : {}),
109
+ };
110
+ return Object.keys(add).length === 0 ? undefined : { ...held, ...add };
111
+ }
112
+ const ROW_CREATED_AT_ABSENT = new Date(0).toISOString();
113
+ function rowCreatedAtOf(sessionId, meta) {
114
+ if (meta?.createdAt !== undefined)
115
+ return meta.createdAt;
116
+ recordFailOpen("server.local-session-row.created-at-absent", `session=${sessionId}`);
117
+ return ROW_CREATED_AT_ABSENT;
118
+ }
119
+ function rowMetadataOf(sessionId, meta) {
120
+ const face = sessionFaceFromRow(sessionId, meta.owner ?? null, meta.incarnation);
121
+ return { id: sessionId, createdAt: rowCreatedAtOf(sessionId, meta), owner: face.owner, incarnation: face.incarnation, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}) };
82
122
  }
83
123
  const isForkCreatedCell = (meta) => meta !== undefined && meta.forkKey !== undefined && meta.execution === undefined;
84
124
  function readingOfCell(sessionId, meta, forking) {
@@ -88,7 +128,17 @@ const LOCAL_EXECUTION_CELL = z.discriminatedUnion("role", [
88
128
  z.object({ role: z.literal("writer"), lane: z.custom((v) => typeof v === "string" && isExecutionLane(v), { message: "not an execution lane" }) }).strict(),
89
129
  z.object({ role: z.literal("mirror") }).strict(),
90
130
  ]);
91
- const SESSION_META_SIDECAR = z.record(z.string(), z.union([z.string(), z.object({ incarnation: z.string(), owner: z.string().nullable().optional(), execution: LOCAL_EXECUTION_CELL.optional(), forkKey: z.string().optional() }).strict()]));
131
+ export const SESSION_META_CELL_OBJECT = z
132
+ .object({
133
+ incarnation: z.string(),
134
+ owner: z.string().nullable().optional(),
135
+ execution: LOCAL_EXECUTION_CELL.optional(),
136
+ forkKey: z.string().optional(),
137
+ createdAt: z.string().optional(),
138
+ forkedFrom: z.string().optional(),
139
+ })
140
+ .strict();
141
+ const SESSION_META_SIDECAR = z.record(z.string(), z.union([z.string(), SESSION_META_CELL_OBJECT]));
92
142
  const readSessionMetaSidecar = createZodColumnReader(SESSION_META_SIDECAR);
93
143
  function ownerClaimWindowOpen() {
94
144
  const window = ONE_TIME_MIGRATIONS.find((w) => w.id === "M10");
@@ -153,6 +203,12 @@ class SidecarMap {
153
203
  persist() {
154
204
  return this.commit(() => true);
155
205
  }
206
+ syncToDisk() {
207
+ const path = this.path;
208
+ if (!path || !existsSync(path))
209
+ return;
210
+ fsyncPaths(path, dirname(path));
211
+ }
156
212
  deleteKey(key) {
157
213
  return this.commit((draft) => draft.delete(key));
158
214
  }
@@ -189,32 +245,116 @@ class SidecarMap {
189
245
  return link;
190
246
  }
191
247
  }
248
+ const archiveRewriteReadScope = new AsyncLocalStorage();
249
+ function noteReplayInArchiveRewriteRead(info) {
250
+ if (info.kind !== "replay")
251
+ return;
252
+ const scope = archiveRewriteReadScope.getStore();
253
+ if (scope !== undefined)
254
+ scope.dropped += info.dropped;
255
+ }
192
256
  export function createLocalSessionRepo(root) {
193
257
  return new FileSessionRepo(root, {
194
- onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", noteReplayInSessionOpenScope),
258
+ onCorruptRead: createCorruptReadFailOpenSeat("server.session-repo.corrupt-record-skipped", (info) => {
259
+ noteReplayInSessionOpenScope(info);
260
+ noteReplayInArchiveRewriteRead(info);
261
+ }),
195
262
  });
196
263
  }
197
- function occupiedBy(session, incarnation) {
264
+ function occupiedBy(session, row) {
198
265
  const storage = session.getStorage();
199
266
  const view = new Proxy(storage, {
200
267
  get(target, prop) {
201
268
  if (prop === "getMetadata")
202
- return async () => occupantMetadata(await target.getMetadata(), incarnation);
269
+ return async () => ({ ...archiveKeysBesideRowFacts(await target.getMetadata()), ...row });
203
270
  const value = Reflect.get(target, prop, target);
204
271
  return typeof value === "function" ? value.bind(target) : value;
205
272
  },
206
273
  });
207
274
  return new StoredSession(view);
208
275
  }
209
- function occupantMetadata(meta, incarnation) {
210
- const { forkedFromIncarnation: _notThisStoresToken, ...rest } = meta;
211
- return { ...rest, incarnation };
276
+ function archiveKeysBesideRowFacts(meta) {
277
+ const { id: _id, createdAt: _createdAt, owner: _owner, incarnation: _incarnation, forkedFrom: _forkedFrom, forkedFromIncarnation: _notThisStoresToken, ...rest } = meta;
278
+ return rest;
279
+ }
280
+ export const LOCAL_ARCHIVE_SNAPSHOT_DIR = "session-archive-m33";
281
+ export class LocalArchiveSnapshotsPresentError extends StoreBootRefusalError {
282
+ dir;
283
+ names;
284
+ code = "config.local_archive_snapshot_present";
285
+ constructor(dir, names, detail) {
286
+ super(`config.local_archive_snapshot_present: ${names.length > 0 ? `the local data root holds ${names.length} session archive snapshot file(s) in ${dir} (${names.slice(0, 8).join(", ")}${names.length > 8 ? ", …" : ""})` : `the local data root's session archive snapshot directory ${dir} could not be read`}` +
287
+ `${detail !== undefined ? ` — ${detail}` : ""} — refusing to start. Each snapshot is a copy of one session's conversation that an interrupted one-time migration (M33) wrote ` +
288
+ "before rewriting that session's stored archive: the archive may have been lost in that interruption and its content may be only in the snapshot, and nothing else reads or " +
289
+ "deletes these copies (deleting the session would leave them behind). Ways out: start a server version that carries the one-time migration M33 once — it restores or discards " +
290
+ "each snapshot on boot and names any it cannot; or, after checking that every session named here has its archive under the data root's sessions/ directory " +
291
+ "(restore it from the snapshot's entries otherwise), delete the snapshot directory.");
292
+ this.dir = dir;
293
+ this.names = names;
294
+ this.name = "LocalArchiveSnapshotsPresentError";
295
+ }
296
+ }
297
+ function fsyncPaths(...paths) {
298
+ for (const target of paths) {
299
+ const fd = openSync(target, "r");
300
+ try {
301
+ fsyncSync(fd);
302
+ }
303
+ finally {
304
+ closeSync(fd);
305
+ }
306
+ }
307
+ }
308
+ function namesIn(dir) {
309
+ try {
310
+ return readdirSync(dir).sort();
311
+ }
312
+ catch (e) {
313
+ if (e?.code === "ENOENT")
314
+ return [];
315
+ throw e;
316
+ }
317
+ }
318
+ const errnoOf = (e) => {
319
+ const code = e?.code;
320
+ return typeof code === "string" ? code : undefined;
321
+ };
322
+ const unreadableDirDetail = (e) => `reading the directory failed (${e?.code ?? (e instanceof Error ? e.message : String(e))})`;
323
+ export function assertNoArchiveSnapshots(dir) {
324
+ let names;
325
+ try {
326
+ names = namesIn(dir);
327
+ }
328
+ catch (e) {
329
+ throw new LocalArchiveSnapshotsPresentError(dir, [], unreadableDirDetail(e));
330
+ }
331
+ if (names.length > 0)
332
+ throw new LocalArchiveSnapshotsPresentError(dir, names);
333
+ }
334
+ const ARCHIVE_SNAPSHOT_RECORD = z
335
+ .object({
336
+ id: z.string(),
337
+ archiveIncarnation: z.string().nullable(),
338
+ rowFacts: z.object({ createdAt: z.string(), forkedFrom: z.string().optional() }).strict(),
339
+ entries: z.array(z.custom((v) => typeof v === "object" && v !== null && !Array.isArray(v), { message: "not an entry object" })),
340
+ })
341
+ .strict();
342
+ const readArchiveSnapshotRecord = createZodColumnReader(ARCHIVE_SNAPSHOT_RECORD);
343
+ function sameLog(a, b) {
344
+ return isDeepStrictEqual(a, b);
212
345
  }
213
346
  export class LocalSessionStore {
214
347
  repo;
215
348
  runs;
216
349
  retention = UNMANAGED_RETENTION;
350
+ durability;
351
+ placements;
352
+ placementOf;
353
+ listPlaced;
354
+ claimOwnerless;
355
+ recordedPlacementOf;
217
356
  pending = new Map();
357
+ rowsDropped = new Set();
218
358
  lastActivity = new Map();
219
359
  stagings = new Map();
220
360
  loaded;
@@ -233,11 +373,42 @@ export class LocalSessionStore {
233
373
  sessionWriteLocks;
234
374
  executionPlacement;
235
375
  executionPlacementBackfill;
376
+ dataRoot;
377
+ archiveSnapshotsSettled = 0;
236
378
  constructor(repo, runs, dataDir, opts) {
237
379
  this.repo = repo;
238
380
  this.runs = runs;
239
381
  this.sidecarPath = dataDir ? join(dataDir, "session-last-activity.json") : undefined;
240
382
  this.log = opts?.log ?? createLogger();
383
+ const repoPlacementOf = repo.placementOf;
384
+ const repoListPlaced = repo.listPlaced;
385
+ if ((typeof repoPlacementOf === "function") !== (typeof repoListPlaced === "function")) {
386
+ throw new Error("LocalSessionStore: the session repo answers only one of placementOf / listPlaced — a repo that records placements answers both");
387
+ }
388
+ if (typeof repoPlacementOf === "function" && typeof repoListPlaced === "function") {
389
+ this.recordedPlacementOf = (sessionId) => repoPlacementOf.call(repo, sessionId);
390
+ this.placementOf = this.recordedPlacementOf;
391
+ this.listPlaced = (kind, o) => repoListPlaced.call(repo, kind, o);
392
+ this.claimOwnerless = (sessionId, incarnation, owner) => this.claimOwnerlessPlaced(sessionId, incarnation, owner);
393
+ }
394
+ else {
395
+ this.recordedPlacementOf = async () => undefined;
396
+ }
397
+ if (opts?.placements !== undefined) {
398
+ if (this.placementOf === undefined) {
399
+ throw new Error("LocalSessionStore: declares a subagent placement partition over a session repo that cannot record placements (no placementOf / listPlaced) — the declaration would be a promise this store cannot keep");
400
+ }
401
+ if (opts.placements.subagent?.durability === "durable" && dataDir === undefined) {
402
+ throw new Error("LocalSessionStore: declares durable placed transcripts without a data dir — their owner and incarnation cells would not survive a restart");
403
+ }
404
+ this.placements = opts.placements;
405
+ }
406
+ if (opts?.durability !== undefined) {
407
+ if (opts.durability === "durable" && dataDir === undefined) {
408
+ throw new Error("LocalSessionStore: declares durable sessions without a data dir — the owner and incarnation cells would not survive a restart");
409
+ }
410
+ this.durability = opts.durability;
411
+ }
241
412
  const sidecar = (file, which, read, strict = false) => new SidecarMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, read, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which), strict);
242
413
  this.titles = sidecar("session-titles.json", "title", readStringRecordColumn);
243
414
  this.pinned = sidecar("session-pinned.json", "pinned", readPinnedSidecar);
@@ -245,6 +416,7 @@ export class LocalSessionStore {
245
416
  this.workspaces = sidecar("session-workspaces.json", "workspace", readStringRecordColumn);
246
417
  this.meta = sidecar("session-incarnations.json", "session-meta", readSessionMetaSidecar, true);
247
418
  const root = dataDir ? resolve(dataDir) : undefined;
419
+ this.dataRoot = root;
248
420
  if (root !== undefined && !SESSION_WRITE_LOCKS_BY_ROOT.has(root))
249
421
  SESSION_WRITE_LOCKS_BY_ROOT.set(root, new Map());
250
422
  this.sessionWriteLocks = root !== undefined ? SESSION_WRITE_LOCKS_BY_ROOT.get(root) : new Map();
@@ -278,6 +450,11 @@ export class LocalSessionStore {
278
450
  return read;
279
451
  if (read.state === "absent")
280
452
  return undefined;
453
+ const partition = await this.placementProbe(sessionId);
454
+ if (partition.state === "placed")
455
+ return undefined;
456
+ if (partition.state === "unusable" && partition.word === "storage")
457
+ return partition;
281
458
  const forking = await this.forkingRead(sessionId, read.meta);
282
459
  return typeof forking === "boolean" ? readingOfCell(sessionId, read.meta, forking) : forking;
283
460
  }
@@ -308,12 +485,13 @@ export class LocalSessionStore {
308
485
  }
309
486
  async settleExecutionHeld(sessionId, judge) {
310
487
  await this.meta.ensureLoaded();
311
- const forking = await this.forkingOf(sessionId, this.metaCellOf(sessionId));
488
+ const placed = await this.isPlacedRow(sessionId);
489
+ const forking = !placed && (await this.forkingOf(sessionId, this.metaCellOf(sessionId)));
312
490
  let settled;
313
491
  let fault;
314
492
  await this.writeMeta(sessionId, (before) => {
315
493
  try {
316
- settled = judge(before === undefined ? undefined : readingOfCell(sessionId, readSessionMetaCell(before), forking));
494
+ settled = judge(before === undefined || placed ? undefined : readingOfCell(sessionId, readSessionMetaCell(before), forking));
317
495
  }
318
496
  catch (e) {
319
497
  fault = e;
@@ -372,6 +550,34 @@ export class LocalSessionStore {
372
550
  return word === "absent" ? { state: "absent" } : unusableArchiveOf(word, e);
373
551
  }
374
552
  }
553
+ async placementProbe(sessionId) {
554
+ try {
555
+ const record = await this.recordedPlacementOf(sessionId);
556
+ return record === undefined ? { state: "ordinary" } : { state: "placed", record };
557
+ }
558
+ catch (e) {
559
+ const word = e instanceof SessionError ? archiveFaultWord(e.code) : "storage";
560
+ return word === "absent" ? { state: "ordinary" } : unusableArchiveOf(word, e);
561
+ }
562
+ }
563
+ async placedRecordOf(sessionId) {
564
+ const probe = await this.placementProbe(sessionId);
565
+ if (probe.state === "unusable") {
566
+ if (probe.word === "storage")
567
+ throw probe.fault;
568
+ return undefined;
569
+ }
570
+ return probe.state === "placed" ? probe.record : undefined;
571
+ }
572
+ async isPlacedRow(sessionId) {
573
+ return (await this.placedRecordOf(sessionId)) !== undefined;
574
+ }
575
+ async placedRecordOrThrow(sessionId) {
576
+ const probe = await this.placementProbe(sessionId);
577
+ if (probe.state === "unusable")
578
+ throw probe.fault;
579
+ return probe.state === "placed" ? probe.record : undefined;
580
+ }
375
581
  async existsInRepo(sessionId) {
376
582
  const probe = await this.probeArchive(sessionId);
377
583
  if (probe.state === "unusable")
@@ -399,7 +605,7 @@ export class LocalSessionStore {
399
605
  }
400
606
  async cellOfPresent(sessionId) {
401
607
  if (!this.meta.state.entries.has(sessionId))
402
- await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation() } : undefined));
608
+ await this.writeMeta(sessionId, (before) => (before === undefined && !this.rowsDropped.has(sessionId) ? { incarnation: mintSessionIncarnation() } : undefined));
403
609
  const cell = this.meta.state.entries.get(sessionId);
404
610
  return cell === undefined ? undefined : readSessionMetaCell(cell);
405
611
  }
@@ -407,18 +613,25 @@ export class LocalSessionStore {
407
613
  const before = await this.metaOf(sessionId);
408
614
  if (before === undefined)
409
615
  return undefined;
410
- const leaf = await this.repo.headOf(sessionId).then((archive) => archive?.leafId ?? null, (fault) => new UnreadableLeaf(fault));
616
+ const leaf = await this.archiveHeadOf(sessionId).then((archive) => archive?.leafId ?? null, (fault) => new UnreadableLeaf(fault));
411
617
  const cell = this.meta.state.entries.get(sessionId);
412
618
  const after = cell === undefined ? undefined : readSessionMetaCell(cell);
413
619
  if (after === undefined || after.incarnation !== before.incarnation || (after.owner ?? null) !== (before.owner ?? null))
414
620
  return undefined;
415
621
  return headFromRow(sessionId, before.owner ?? null, before.incarnation, leaf);
416
622
  }
623
+ archiveHeadOf(sessionId) {
624
+ return this.repo.headOf(sessionId);
625
+ }
417
626
  async handOut(sessionId, session) {
418
627
  const meta = await this.cellOfPresent(sessionId);
419
628
  if (meta === undefined)
420
629
  throw new SessionError("storage", `LocalSessionStore: session ${sessionId} lost its session-meta cell while it was being handed out`);
421
- return { session: occupiedBy(session, sessionFaceFromRow(sessionId, meta.owner ?? null, meta.incarnation).incarnation), sessionId };
630
+ return { session: occupiedBy(session, rowMetadataOf(sessionId, meta)), sessionId };
631
+ }
632
+ async sessionMetadataOf(sessionId) {
633
+ const meta = await this.metaOf(sessionId);
634
+ return meta === undefined ? undefined : rowMetadataOf(sessionId, meta);
422
635
  }
423
636
  async probeTitle(sessionId) {
424
637
  await this.titles.ensureLoaded();
@@ -618,26 +831,34 @@ export class LocalSessionStore {
618
831
  console.warn(`LocalSessionStore: ${which} sidecar ${op} failed (${SIDECAR_DEGRADES[which]} persistence degraded): ${e instanceof Error ? e.message : String(e)}`);
619
832
  }
620
833
  async acquire(sessionId, opts) {
834
+ if (!sessionId && this.declaredPlacement(opts) !== undefined)
835
+ return this.load(uuidv7(), false, opts);
621
836
  if (!sessionId) {
622
837
  await this.meta.ensureLoaded();
623
838
  const session = await this.repo.create({});
624
839
  const id = (await session.getMetadata()).id;
625
- await this.writeMeta(id, () => ({ incarnation: mintSessionIncarnation(), owner: null }));
840
+ await this.writeMeta(id, () => ({ incarnation: mintSessionIncarnation(), owner: null, createdAt: rowBornNow() }));
626
841
  return this.handOut(id, session);
627
842
  }
628
843
  if (opts?.requireExisting)
629
- return this.load(sessionId, true);
844
+ return this.load(sessionId, true, opts);
630
845
  const inflight = this.pending.get(sessionId);
631
- if (inflight)
632
- return inflight;
633
- const p = this.load(sessionId).finally(() => this.pending.delete(sessionId));
846
+ if (inflight) {
847
+ const acquired = await inflight;
848
+ admitSessionPlacement(sessionId, await this.recordedPlacementOf(sessionId), opts ?? {}, placementCapabilityOf(this));
849
+ return acquired;
850
+ }
851
+ const p = this.load(sessionId, false, opts).finally(() => this.pending.delete(sessionId));
634
852
  this.pending.set(sessionId, p);
635
853
  return p;
636
854
  }
637
- async load(sessionId, requireExisting = false) {
855
+ declaredPlacement(opts) {
856
+ return this.placements !== undefined && opts?.requireExisting !== true ? opts?.placement : undefined;
857
+ }
858
+ async load(sessionId, requireExisting = false, opts) {
638
859
  return this.withSessionWriteLock(sessionId, async () => {
639
860
  try {
640
- return await this.openForItselfHeld(sessionId);
861
+ return await this.openForItselfHeld(sessionId, opts);
641
862
  }
642
863
  catch (e) {
643
864
  if (!isNotFound(e))
@@ -648,14 +869,34 @@ export class LocalSessionStore {
648
869
  await this.meta.ensureLoaded();
649
870
  if (isForkCreatedCell(this.metaCellOf(sessionId)))
650
871
  throw new SessionForkInProgressError(sessionId);
872
+ const placement = this.declaredPlacement(opts);
873
+ if (placement !== undefined)
874
+ return this.createPlacedHeld(sessionId, placement, opts ?? {});
651
875
  const session = await this.repo.create({ id: sessionId });
652
- await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null }));
876
+ await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null, createdAt: rowBornNow() }));
653
877
  return this.handOut(sessionId, session);
654
878
  });
655
879
  }
656
- async openForItselfHeld(sessionId) {
880
+ async openForItselfHeld(sessionId, opts) {
657
881
  await this.meta.ensureLoaded();
658
882
  const session = await openSessionForItself(sessionId, () => this.repo.open({ id: sessionId, createdAt: "" }));
883
+ admitSessionPlacement(sessionId, (await session.getMetadata()).placement, opts ?? {}, placementCapabilityOf(this));
884
+ return this.handOut(sessionId, session);
885
+ }
886
+ async createPlacedHeld(sessionId, placement, opts) {
887
+ if (this.metaCellOf(sessionId) !== undefined) {
888
+ throw new SessionError("placement_refused", `session ${sessionId} is already registered as an ordinary session — refusing to create a subagent transcript on its id`);
889
+ }
890
+ let session;
891
+ await createPlacedSessionRow(sessionId, placement, {
892
+ readFaces: sourceFacesVia((id) => this.headOf(id)),
893
+ insert: async (owner) => {
894
+ session = await this.repo.create({ id: sessionId, placement });
895
+ await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() } : undefined));
896
+ },
897
+ readOwner: async () => (await this.headOf(sessionId))?.owner,
898
+ }, this.log);
899
+ admitSessionPlacement(sessionId, (await session.getMetadata()).placement, opts, placementCapabilityOf(this));
659
900
  return this.handOut(sessionId, session);
660
901
  }
661
902
  async register(sessionId, owner) {
@@ -667,14 +908,14 @@ export class LocalSessionStore {
667
908
  const existed = await this.existsInRepo(sessionId);
668
909
  if (!existed && isForkCreatedCell(this.metaCellOf(sessionId)))
669
910
  throw new SessionForkInProgressError(sessionId);
670
- const claim = existed && owner !== null && ownerClaimWindowOpen();
911
+ const claim = existed && owner !== null && ownerClaimWindowOpen() && !(await this.isPlacedRow(sessionId));
671
912
  const wrote = await this.writeMeta(sessionId, (before) => {
672
913
  if (ownerRecorded(before))
673
914
  return undefined;
674
915
  if (claim)
675
916
  return before === undefined ? { incarnation: mintSessionIncarnation(), owner } : cellWith(before, { owner });
676
917
  if (!existed)
677
- return { incarnation: mintSessionIncarnation(), owner };
918
+ return { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() };
678
919
  return before === undefined ? { incarnation: mintSessionIncarnation() } : undefined;
679
920
  });
680
921
  if (!existed && wrote)
@@ -689,6 +930,26 @@ export class LocalSessionStore {
689
930
  }
690
931
  async release(sessionId) {
691
932
  this.pending.delete(sessionId);
933
+ await this.withSessionWriteLock(sessionId, async () => {
934
+ if ((await this.placedRecordOrThrow(sessionId)) === undefined)
935
+ return;
936
+ await this.dropRowHeld(sessionId);
937
+ await this.repo.delete({ id: sessionId, createdAt: "" });
938
+ });
939
+ }
940
+ async claimOwnerlessPlaced(sessionId, incarnation, owner) {
941
+ return this.withSessionWriteLock(sessionId, async () => {
942
+ if ((await this.placedRecordOrThrow(sessionId)) === undefined)
943
+ return false;
944
+ return this.writeMeta(sessionId, (before) => {
945
+ if (before === undefined)
946
+ return undefined;
947
+ const cell = readSessionMetaCell(before);
948
+ if (cell.incarnation !== incarnation || (cell.owner ?? null) !== null)
949
+ return undefined;
950
+ return cellWith(before, { owner });
951
+ });
952
+ });
692
953
  }
693
954
  async listSessions(opts) {
694
955
  await this.loadRowSidecars();
@@ -723,7 +984,7 @@ export class LocalSessionStore {
723
984
  return cell === undefined ? null : (readSessionMetaCell(cell).owner ?? null);
724
985
  }
725
986
  async summaryOf(meta, rollups) {
726
- const firstActivityAt = meta.createdAt || new Date(0).toISOString();
987
+ const firstActivityAt = rowCreatedAtOf(meta.id, this.metaCellOf(meta.id));
727
988
  const owner = this.ownerCellOf(meta.id);
728
989
  const cells = this.rowCellsOf(meta.id);
729
990
  const rowTag = this.rowTagKeyOf(meta.id, owner, cells);
@@ -782,21 +1043,26 @@ export class LocalSessionStore {
782
1043
  }
783
1044
  async forkToHeld(sourceId, owner, target) {
784
1045
  const sourceMeta = { id: sourceId, createdAt: "" };
785
- let forkOptions;
1046
+ let source;
786
1047
  try {
787
- forkOptions = await settledForkOptions(await this.repo.open(sourceMeta));
1048
+ source = await this.repo.open(sourceMeta);
788
1049
  }
789
1050
  catch (e) {
790
1051
  if (isNotFound(e))
791
1052
  return null;
792
1053
  throw e;
793
1054
  }
1055
+ if (((await source.getMetadata()).owner ?? null) !== null)
1056
+ throw new StampedSessionArchiveError(sourceId);
1057
+ const forkOptions = await settledForkOptions(source);
794
1058
  const forkOwner = forkOwnerOf(owner, this.metaCellOf(sourceId)?.owner);
795
1059
  const existing = this.metaCellOf(target.id);
796
1060
  const targetArchived = await this.existsInRepo(target.id);
797
1061
  const copy = () => this.repo.fork(sourceMeta, { ...forkOptions, id: target.id });
798
1062
  if (existing === undefined && !targetArchived) {
799
- const created = await this.writeMeta(target.id, (before) => before !== undefined ? undefined : { incarnation: mintSessionIncarnation(), ...(forkOwner !== undefined ? { owner: forkOwner } : {}), ...(target.key !== undefined ? { forkKey: target.key } : {}) });
1063
+ const created = await this.writeMeta(target.id, (before) => before !== undefined
1064
+ ? undefined
1065
+ : { incarnation: mintSessionIncarnation(), ...(forkOwner !== undefined ? { owner: forkOwner } : {}), ...(target.key !== undefined ? { forkKey: target.key } : {}), createdAt: rowBornNow(), forkedFrom: sourceId });
800
1066
  if (!created)
801
1067
  throw new SessionError("storage", `LocalSessionStore: fork target ${target.id} gained a session-meta cell under its own write lock — refusing to guess`);
802
1068
  try {
@@ -860,6 +1126,8 @@ export class LocalSessionStore {
860
1126
  if (!this.repo.importEntries)
861
1127
  throw new SessionError("storage", "session import is not supported by this local backend");
862
1128
  await this.meta.ensureLoaded();
1129
+ if (landing === "replace")
1130
+ await this.assertNotPlacedHeld(sessionId);
863
1131
  validateEntriesForImport(entries);
864
1132
  const current = this.metaCellOf(sessionId);
865
1133
  if (current?.execution?.role === "writer")
@@ -873,8 +1141,10 @@ export class LocalSessionStore {
873
1141
  case "create":
874
1142
  case "judged":
875
1143
  try {
876
- const archive = (await this.repo.headOf(sessionId)) ?? null;
877
- await this.repo.importEntries(sessionId, archive?.owner ?? undefined, entries, { expect: { row: archive } });
1144
+ const archive = (await this.archiveHeadOf(sessionId)) ?? null;
1145
+ if ((archive?.owner ?? null) !== null)
1146
+ throw new StampedSessionArchiveError(sessionId);
1147
+ await this.repo.importEntries(sessionId, undefined, entries, { expect: { row: archive } });
878
1148
  }
879
1149
  catch (err) {
880
1150
  await this.withdrawImportCell(sessionId, prior, err);
@@ -898,6 +1168,12 @@ export class LocalSessionStore {
898
1168
  }
899
1169
  await this.touch(sessionId);
900
1170
  }
1171
+ async assertNotPlacedHeld(sessionId) {
1172
+ const placed = await this.placedRecordOf(sessionId);
1173
+ if (placed !== undefined) {
1174
+ throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placed.kind} placement partition (a delegated agent's transcript) — a session import cannot replace its log`);
1175
+ }
1176
+ }
901
1177
  async importMayLand(sessionId, current, owner, landing) {
902
1178
  switch (landing) {
903
1179
  case "create":
@@ -917,7 +1193,9 @@ export class LocalSessionStore {
917
1193
  await this.writeMeta(sessionId, (before) => {
918
1194
  prior = before;
919
1195
  const cells = carriedRowCells(sessionId, before === undefined ? undefined : readSessionMetaCell(before));
920
- return { incarnation: cells.incarnation, owner, execution: MIRROR_CELL, ...(cells.forkKey !== null ? { forkKey: cells.forkKey } : {}) };
1196
+ const base = before === undefined ? { createdAt: rowBornNow() } : cellObjectOf(before);
1197
+ const { forkKey: _carriedByTheRule, ...kept } = base;
1198
+ return { ...kept, incarnation: cells.incarnation, owner, execution: MIRROR_CELL, ...(cells.forkKey !== null ? { forkKey: cells.forkKey } : {}) };
921
1199
  });
922
1200
  return prior;
923
1201
  }
@@ -950,6 +1228,7 @@ export class LocalSessionStore {
950
1228
  throw new StagedCommitOwnerChangedError(sessionId);
951
1229
  if (await this.forkingOf(sessionId, current))
952
1230
  throw new SessionForkInProgressError(sessionId);
1231
+ await this.assertNotPlacedHeld(sessionId);
953
1232
  const dstEntries = await this.exportEntries(sessionId);
954
1233
  const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
955
1234
  if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
@@ -992,15 +1271,217 @@ export class LocalSessionStore {
992
1271
  if (!isNotFound(e))
993
1272
  throw e;
994
1273
  }
1274
+ await this.dropRowHeld(sessionId);
1275
+ return true;
1276
+ }
1277
+ async dropRowHeld(sessionId) {
1278
+ this.rowsDropped.add(sessionId);
995
1279
  for (const [map, which] of [[this.titles, "title"], [this.pinned, "pinned"], [this.instructionSources, "instruction-source"], [this.workspaces, "workspace"]]) {
996
1280
  await map.ensureLoaded();
997
1281
  if (!(await map.deleteKey(sessionId)))
998
1282
  throw new SessionError("storage", `LocalSessionStore: ${which} sidecar write failed while deleting session ${sessionId} — its ${which} is still on disk; retry the delete`);
999
1283
  }
1000
- await this.dropMeta(sessionId);
1284
+ await this.ensureLoaded();
1001
1285
  this.lastActivity.delete(sessionId);
1002
1286
  this.scheduleFlush();
1003
- return true;
1287
+ await this.dropMeta(sessionId);
1288
+ }
1289
+ syncArchiveDirectory() {
1290
+ if (this.dataRoot === undefined)
1291
+ return;
1292
+ fsyncPaths(join(this.dataRoot, "sessions"));
1293
+ }
1294
+ archiveSnapshotDir() {
1295
+ return this.dataRoot === undefined ? undefined : join(this.dataRoot, LOCAL_ARCHIVE_SNAPSHOT_DIR);
1296
+ }
1297
+ async settleArchiveSnapshots() {
1298
+ const dir = this.archiveSnapshotDir();
1299
+ if (dir === undefined)
1300
+ return 0;
1301
+ let settled = 0;
1302
+ let names;
1303
+ try {
1304
+ names = namesIn(dir);
1305
+ }
1306
+ catch (e) {
1307
+ throw new LocalArchiveSnapshotsPresentError(dir, [], unreadableDirDetail(e));
1308
+ }
1309
+ for (const name of names) {
1310
+ try {
1311
+ if (name.endsWith(".tmp")) {
1312
+ unlinkSync(join(dir, name));
1313
+ continue;
1314
+ }
1315
+ if (!name.endsWith(".json"))
1316
+ continue;
1317
+ await this.settleArchiveSnapshot(dir, name);
1318
+ settled += 1;
1319
+ }
1320
+ catch (e) {
1321
+ if (e instanceof LocalArchiveSnapshotsPresentError)
1322
+ throw e;
1323
+ throw new LocalArchiveSnapshotsPresentError(dir, names, `settling ${name} failed (${e instanceof Error ? e.message : String(e)})`);
1324
+ }
1325
+ }
1326
+ this.archiveSnapshotsSettled += settled;
1327
+ assertNoArchiveSnapshots(dir);
1328
+ return settled;
1329
+ }
1330
+ async settleArchiveSnapshot(dir, name) {
1331
+ const path = join(dir, name);
1332
+ const id = name.slice(0, -".json".length);
1333
+ const refuse = (why, cause) => new LocalArchiveSnapshotsPresentError(dir, namesIn(dir), `${name} (session ${id}) cannot be settled: ${why}${cause === undefined ? "" : ` (${cause instanceof Error ? cause.message : String(cause)})`}`);
1334
+ let record;
1335
+ try {
1336
+ record = decodeJsonColumn(readFileSync(path, "utf8"), readArchiveSnapshotRecord, `${name} (a local archive snapshot)`);
1337
+ }
1338
+ catch (e) {
1339
+ throw refuse("the record is unreadable", e instanceof SqlJsonColumnError ? e.problem : e);
1340
+ }
1341
+ if (record.id !== id)
1342
+ throw refuse(`the record names session ${record.id}`);
1343
+ if (!this.repo.importEntries)
1344
+ throw refuse("this local backend's session repo cannot import a log");
1345
+ await this.withSessionWriteLock(id, async () => {
1346
+ let head;
1347
+ try {
1348
+ head = await this.archiveHeadOf(id);
1349
+ }
1350
+ catch (e) {
1351
+ throw refuse("the session's stored archive is unreadable", e);
1352
+ }
1353
+ if (head === undefined) {
1354
+ try {
1355
+ await this.repo.importEntries(id, undefined, record.entries, { expect: { row: null }, preserveActorAssertions: true });
1356
+ this.syncArchiveDirectory();
1357
+ }
1358
+ catch (e) {
1359
+ throw refuse("restoring the archive from the snapshot failed", e);
1360
+ }
1361
+ await this.fillRowFacts(id, record.rowFacts);
1362
+ }
1363
+ else if (head.owner === null) {
1364
+ await this.fillRowFacts(id, record.rowFacts);
1365
+ }
1366
+ else if (head.incarnation !== (record.archiveIncarnation ?? undefined)) {
1367
+ throw refuse("the session's archive is a different generation from the one the snapshot was taken of");
1368
+ }
1369
+ unlinkSync(path);
1370
+ fsyncPaths(dir);
1371
+ });
1372
+ }
1373
+ async fillRowFacts(sessionId, facts) {
1374
+ await this.writeMeta(sessionId, (before) => rowFactsFilled(before, facts));
1375
+ }
1376
+ async migrateLocalArchives() {
1377
+ const dir = this.archiveSnapshotDir();
1378
+ if (dir === undefined)
1379
+ return { rowFactsBackfilled: 0, anonymized: 0, keptStamped: [], unreadable: [], snapshotsRecovered: 0 };
1380
+ await this.settleArchiveSnapshots();
1381
+ if (!this.repo.readRow || !this.repo.importEntries)
1382
+ throw new SessionError("storage", "LocalSessionStore: the one-time migration M33 needs a session repo that reads a whole row and imports a log (readRow / importEntries)");
1383
+ await this.meta.ensureLoaded();
1384
+ const listed = await this.repo.list();
1385
+ const listedIds = new Set(listed.map((row) => row.id));
1386
+ const unreadable = [];
1387
+ for (const id of [...this.meta.state.entries.keys()].sort()) {
1388
+ if (listedIds.has(id))
1389
+ continue;
1390
+ try {
1391
+ await this.archiveHeadOf(id);
1392
+ }
1393
+ catch (e) {
1394
+ const errno = errnoOf(e);
1395
+ unreadable.push({ sessionId: id, ...(errno !== undefined ? { errno } : {}) });
1396
+ }
1397
+ }
1398
+ let rowFactsBackfilled = 0;
1399
+ const committed = await this.meta.commit((draft) => {
1400
+ rowFactsBackfilled = 0;
1401
+ for (const row of listed) {
1402
+ const next = rowFactsFilled(draft.get(row.id), archivedRowFactsOf(row));
1403
+ if (next === undefined)
1404
+ continue;
1405
+ draft.set(row.id, next);
1406
+ rowFactsBackfilled += 1;
1407
+ }
1408
+ return rowFactsBackfilled > 0;
1409
+ });
1410
+ if (!committed)
1411
+ throw new SessionError("storage", "LocalSessionStore: the one-time migration M33 could not write the row facts it backfilled into the session-meta sidecar — no archive was touched");
1412
+ this.meta.syncToDisk();
1413
+ let anonymized = 0;
1414
+ if (listed.some((row) => (row.owner ?? null) !== null)) {
1415
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
1416
+ fsyncPaths(dir, dirname(dir));
1417
+ }
1418
+ const keptStamped = [];
1419
+ for (const row of [...listed].sort((a, b) => (a.id < b.id ? -1 : 1))) {
1420
+ if ((row.owner ?? null) === null)
1421
+ continue;
1422
+ const step = await this.withSessionWriteLock(row.id, () => this.anonymizeArchiveHeld(row.id, dir));
1423
+ switch (step.outcome) {
1424
+ case "anonymized":
1425
+ anonymized += 1;
1426
+ break;
1427
+ case "skipped":
1428
+ break;
1429
+ case "kept":
1430
+ keptStamped.push({ sessionId: row.id, reason: step.reason, ...(step.detail !== undefined ? { detail: step.detail } : {}) });
1431
+ break;
1432
+ case "unreadable":
1433
+ unreadable.push({ sessionId: row.id, ...(step.errno !== undefined ? { errno: step.errno } : {}) });
1434
+ break;
1435
+ default: {
1436
+ const unknownOutcome = step;
1437
+ throw new Error(`LocalSessionStore: no M33 outcome ${JSON.stringify(unknownOutcome)}`);
1438
+ }
1439
+ }
1440
+ }
1441
+ const snapshotsRecovered = this.archiveSnapshotsSettled;
1442
+ this.archiveSnapshotsSettled = 0;
1443
+ return { rowFactsBackfilled, anonymized, keptStamped, unreadable, snapshotsRecovered };
1444
+ }
1445
+ async anonymizeArchiveHeld(sessionId, dir) {
1446
+ const scope = { dropped: 0 };
1447
+ let row;
1448
+ try {
1449
+ row = await archiveRewriteReadScope.run(scope, () => this.repo.readRow(sessionId));
1450
+ }
1451
+ catch (e) {
1452
+ return { outcome: "unreadable", errno: errnoOf(e) };
1453
+ }
1454
+ if (row === undefined || (row.head.owner ?? null) === null)
1455
+ return { outcome: "skipped" };
1456
+ if (scope.dropped > 0)
1457
+ return { outcome: "kept", reason: "records_skipped" };
1458
+ let validated;
1459
+ try {
1460
+ validated = validateEntriesForImport(row.entries, { preserveActorAssertions: true });
1461
+ }
1462
+ catch (e) {
1463
+ return { outcome: "kept", reason: "invalid", detail: e instanceof Error ? e.message : String(e) };
1464
+ }
1465
+ if (!sameLog(validated, row.entries))
1466
+ return { outcome: "kept", reason: "normalizes" };
1467
+ const cell = this.metaCellOf(sessionId);
1468
+ if (cell?.createdAt === undefined)
1469
+ throw new Error(`LocalSessionStore: the one-time migration M33 reached session ${sessionId}'s archive before its row facts were backfilled — refusing to rewrite it`);
1470
+ const record = {
1471
+ id: sessionId,
1472
+ archiveIncarnation: row.head.incarnation ?? null,
1473
+ rowFacts: { createdAt: cell.createdAt, ...(cell.forkedFrom !== undefined ? { forkedFrom: cell.forkedFrom } : {}) },
1474
+ entries: row.entries,
1475
+ };
1476
+ const path = join(dir, `${sessionId}.json`);
1477
+ atomicWriteFile(dir, path, JSON.stringify(record));
1478
+ fsyncPaths(dir);
1479
+ await this.repo.delete({ id: sessionId, createdAt: "" }, { row: { owner: row.head.owner, incarnation: row.head.incarnation } });
1480
+ await this.repo.importEntries(sessionId, undefined, row.entries, { expect: { row: null }, preserveActorAssertions: true });
1481
+ this.syncArchiveDirectory();
1482
+ unlinkSync(path);
1483
+ fsyncPaths(dir);
1484
+ return { outcome: "anonymized" };
1004
1485
  }
1005
1486
  get size() {
1006
1487
  return this.pending.size;