@sema-agent/server 7.137.0 → 7.139.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 +600 -142
  2. package/README.md +7 -3
  3. package/README.zh-CN.md +6 -3
  4. package/USAGE.md +132 -41
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/wire.d.ts +3 -3
  8. package/dist/approval-card.d.ts +23 -16
  9. package/dist/approval-card.js +1 -0
  10. package/dist/audit.js +1 -0
  11. package/dist/background-agent-name.d.ts +5 -0
  12. package/dist/background-agent-name.js +5 -0
  13. package/dist/bench/s1/arms.d.ts +1 -1
  14. package/dist/bench/s1/arms.js +1 -3
  15. package/dist/bench/s1/run-firm.js +2 -2
  16. package/dist/boot/budget-tracing.d.ts +1 -12
  17. package/dist/boot/deployment-mods.d.ts +46 -16
  18. package/dist/boot/deployment-mods.js +82 -6
  19. package/dist/boot/device-lane.d.ts +8 -3
  20. package/dist/boot/device-lane.js +7 -2
  21. package/dist/boot/execution-env.d.ts +4 -2
  22. package/dist/boot/execution-env.js +16 -24
  23. package/dist/boot/execution-lane.d.ts +1 -1
  24. package/dist/boot/governance-seams.d.ts +12 -18
  25. package/dist/boot/governance-seams.js +10 -24
  26. package/dist/boot/leader.d.ts +3 -0
  27. package/dist/boot/leader.js +1 -0
  28. package/dist/boot/memory-consolidation.js +2 -2
  29. package/dist/boot/org-delivery.js +15 -2
  30. package/dist/boot/resolve-spec.d.ts +4 -1
  31. package/dist/boot/resolve-spec.js +29 -19
  32. package/dist/boot/retention-lane.d.ts +14 -8
  33. package/dist/boot/retention-lane.js +2 -8
  34. package/dist/boot/runner-deps.d.ts +10 -4
  35. package/dist/boot/runner-deps.js +2 -0
  36. package/dist/boot/session-faces.d.ts +0 -3
  37. package/dist/boot/session-faces.js +15 -8
  38. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  39. package/dist/boot/stage-04-stores.d.ts +1 -12
  40. package/dist/boot/stage-05-execution-env.d.ts +3 -12
  41. package/dist/boot/stage-05-execution-env.js +8 -5
  42. package/dist/boot/stage-06-runners.d.ts +3 -12
  43. package/dist/boot/stage-06-runners.js +5 -3
  44. package/dist/boot/stage-07-capability-layer.d.ts +3 -12
  45. package/dist/boot/stage-07-capability-layer.js +2 -2
  46. package/dist/boot/stage-08-reapers.d.ts +3 -12
  47. package/dist/boot/stage-09-leader.d.ts +3 -12
  48. package/dist/boot/stage-09-leader.js +2 -3
  49. package/dist/boot/stage-10-http-server.d.ts +3 -12
  50. package/dist/boot/stage-10-http-server.js +5 -3
  51. package/dist/boot/stores.js +4 -4
  52. package/dist/boot/web-admin.d.ts +5 -2
  53. package/dist/boot/web-admin.js +5 -5
  54. package/dist/brain.js +5 -5
  55. package/dist/budget.d.ts +17 -15
  56. package/dist/budget.js +2 -2
  57. package/dist/capabilities/mcp-probe.d.ts +36 -11
  58. package/dist/capabilities/mcp-probe.js +44 -5
  59. package/dist/capabilities/scenarios.d.ts +1 -1
  60. package/dist/capability-facts.d.ts +21 -7
  61. package/dist/capability-facts.js +13 -3
  62. package/dist/checkpoint-identity.d.ts +13 -6
  63. package/dist/config-catalog.js +14 -8
  64. package/dist/config-invariants.d.ts +23 -3
  65. package/dist/config-invariants.js +42 -0
  66. package/dist/config-provider.d.ts +19 -24
  67. package/dist/config-provider.js +27 -12
  68. package/dist/config-types.d.ts +25 -12
  69. package/dist/config.d.ts +33 -0
  70. package/dist/config.js +52 -20
  71. package/dist/delegated-session-owner.d.ts +2 -2
  72. package/dist/deployment-shape.d.ts +7 -7
  73. package/dist/device-enrollment.d.ts +41 -2
  74. package/dist/device-enrollment.js +27 -12
  75. package/dist/device-store.d.ts +58 -85
  76. package/dist/device-store.js +19 -49
  77. package/dist/device-ws-hub.js +2 -1
  78. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  79. package/dist/exec-cut.d.ts +60 -0
  80. package/dist/exec-cut.js +36 -0
  81. package/dist/execution-lane-caps.d.ts +19 -0
  82. package/dist/execution-lane-caps.js +10 -0
  83. package/dist/execution-placement.d.ts +259 -0
  84. package/dist/execution-placement.js +199 -0
  85. package/dist/field-conflict.d.ts +3 -2
  86. package/dist/field-conflict.js +1 -0
  87. package/dist/gate-settlement.d.ts +71 -0
  88. package/dist/gate-settlement.js +40 -0
  89. package/dist/hooks/classic-stations.d.ts +1 -0
  90. package/dist/hooks/classic-stations.js +1 -0
  91. package/dist/hooks/command-spawn.d.ts +2 -2
  92. package/dist/hooks/hook-runner.js +14 -14
  93. package/dist/hosted-posture.d.ts +2 -2
  94. package/dist/http/admission.js +10 -19
  95. package/dist/http/checkpoint-ctx.d.ts +21 -12
  96. package/dist/http/checkpoint-ctx.js +4 -2
  97. package/dist/http/claimed-run.d.ts +1 -1
  98. package/dist/http/dispatch.d.ts +4 -0
  99. package/dist/http/dispatch.js +3 -0
  100. package/dist/http/durable-run-leg.js +12 -6
  101. package/dist/http/execution-context.d.ts +218 -0
  102. package/dist/http/execution-context.js +330 -0
  103. package/dist/http/memory-compliance-failure-reply.js +3 -0
  104. package/dist/http/resume-legs.d.ts +5 -11
  105. package/dist/http/resume-legs.js +179 -104
  106. package/dist/http/route-ctx.d.ts +44 -59
  107. package/dist/http/route-ctx.js +2 -5
  108. package/dist/http/routes/a2a-serve.js +10 -4
  109. package/dist/http/routes/agents-roster.d.ts +1 -1
  110. package/dist/http/routes/agents-roster.js +5 -1
  111. package/dist/http/routes/approvals-assistant.js +6 -6
  112. package/dist/http/routes/background.d.ts +29 -1
  113. package/dist/http/routes/background.js +29 -2
  114. package/dist/http/routes/capabilities.js +12 -10
  115. package/dist/http/routes/devices.d.ts +13 -19
  116. package/dist/http/routes/devices.js +21 -26
  117. package/dist/http/routes/mcp-reconnect.d.ts +3 -2
  118. package/dist/http/routes/mcp-reconnect.js +7 -5
  119. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  120. package/dist/http/routes/memory-origin.d.ts +1 -1
  121. package/dist/http/routes/rules.js +3 -3
  122. package/dist/http/routes/runs.js +11 -7
  123. package/dist/http/routes/session-sync.js +110 -15
  124. package/dist/http/routes/sessions.d.ts +6 -0
  125. package/dist/http/routes/sessions.js +119 -13
  126. package/dist/http/routes/side-query.d.ts +12 -4
  127. package/dist/http/routes/side-query.js +6 -8
  128. package/dist/http/routes/tasks.js +800 -782
  129. package/dist/http/server.d.ts +61 -39
  130. package/dist/http/server.js +11 -0
  131. package/dist/http/session-inherit-env.d.ts +3 -4
  132. package/dist/http/session-inherit-env.js +1 -1
  133. package/dist/http/wire-types.d.ts +22 -18
  134. package/dist/identity-source.d.ts +1 -1
  135. package/dist/index.d.ts +3 -0
  136. package/dist/index.js +1 -0
  137. package/dist/leader/wire.d.ts +7 -0
  138. package/dist/leader/wire.js +5 -4
  139. package/dist/mcp-assertion-header.d.ts +51 -40
  140. package/dist/mcp-assertion-header.js +10 -11
  141. package/dist/mcp-authorization-face.d.ts +27 -0
  142. package/dist/mcp-authorization-face.js +15 -0
  143. package/dist/memory-operator-faces.d.ts +13 -4
  144. package/dist/memory-operator-faces.js +1 -17
  145. package/dist/memory-sync-client.d.ts +53 -14
  146. package/dist/memory-sync-client.js +187 -92
  147. package/dist/memory-sync.d.ts +1 -1
  148. package/dist/memory-sync.js +3 -2
  149. package/dist/model-route-gap.d.ts +6 -22
  150. package/dist/model-route-gap.js +7 -5
  151. package/dist/mods-declaration.d.ts +110 -0
  152. package/dist/mods-declaration.js +109 -0
  153. package/dist/observability/cost-taxonomy.d.ts +2 -2
  154. package/dist/observability/err-text.d.ts +5 -0
  155. package/dist/observability/err-text.js +9 -0
  156. package/dist/observability/fail-open.d.ts +17 -1
  157. package/dist/observability/fail-open.js +17 -1
  158. package/dist/observability/run-terminal.js +0 -1
  159. package/dist/observability/secret-env-scrub.d.ts +1 -1
  160. package/dist/observability/secret-env-scrub.js +1 -1
  161. package/dist/orchestration/workflow-notify-journal.js +4 -4
  162. package/dist/org-memory-admission.d.ts +4 -3
  163. package/dist/org-memory-admission.js +6 -6
  164. package/dist/parked-decide.d.ts +49 -11
  165. package/dist/parked-decide.js +25 -26
  166. package/dist/parked-revive-surface.d.ts +12 -15
  167. package/dist/parked-revive-surface.js +3 -6
  168. package/dist/peer-seat.d.ts +156 -0
  169. package/dist/peer-seat.js +273 -0
  170. package/dist/permission-rule-vocab.d.ts +7 -7
  171. package/dist/permission-rule-vocab.js +3 -3
  172. package/dist/plain-string-record.d.ts +38 -0
  173. package/dist/plain-string-record.js +38 -0
  174. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  175. package/dist/plugins/approval-ask-store-file.js +11 -0
  176. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  177. package/dist/plugins/approval-ask-store-memory.js +12 -0
  178. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  179. package/dist/plugins/approval-ask-store-sql.js +30 -12
  180. package/dist/plugins/background-agent-store-sql.js +6 -3
  181. package/dist/plugins/caching-session-store.d.ts +11 -2
  182. package/dist/plugins/caching-session-store.js +15 -3
  183. package/dist/plugins/checkpoint-store-sql.d.ts +21 -25
  184. package/dist/plugins/checkpoint-store-sql.js +13 -15
  185. package/dist/plugins/device-store-sql.d.ts +25 -51
  186. package/dist/plugins/device-store-sql.js +49 -103
  187. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  188. package/dist/plugins/execution-placement-sql.js +223 -0
  189. package/dist/plugins/file-history-store-sql.d.ts +7 -6
  190. package/dist/plugins/file-history-store-sql.js +8 -7
  191. package/dist/plugins/file-run-store.d.ts +6 -1
  192. package/dist/plugins/file-run-store.js +4 -0
  193. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  194. package/dist/plugins/fork-routing-session-store.js +16 -0
  195. package/dist/plugins/k8s-pod-reaper.d.ts +2 -8
  196. package/dist/plugins/k8s-pod-reaper.js +3 -3
  197. package/dist/plugins/local-checkpoint-store.d.ts +1 -11
  198. package/dist/plugins/local-checkpoint-store.js +0 -1
  199. package/dist/plugins/local-session-store.d.ts +183 -45
  200. package/dist/plugins/local-session-store.js +412 -105
  201. package/dist/plugins/mailbox-store-sql.d.ts +11 -3
  202. package/dist/plugins/mailbox-store-sql.js +103 -15
  203. package/dist/plugins/memory-key-guards.d.ts +4 -0
  204. package/dist/plugins/memory-key-guards.js +1 -0
  205. package/dist/plugins/memory-run-store.d.ts +1 -1
  206. package/dist/plugins/memory-sync-store-pg.d.ts +16 -5
  207. package/dist/plugins/memory-sync-store-tidb.d.ts +3 -3
  208. package/dist/plugins/one-time-migrations.d.ts +117 -6
  209. package/dist/plugins/one-time-migrations.js +383 -14
  210. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  211. package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
  212. package/dist/plugins/permission-rule-store-sql.js +34 -29
  213. package/dist/plugins/pg-pool.d.ts +7 -3
  214. package/dist/plugins/pg-session-storage.d.ts +41 -22
  215. package/dist/plugins/pg-session-storage.js +136 -55
  216. package/dist/plugins/remote-env-adb.d.ts +22 -4
  217. package/dist/plugins/remote-env-adb.js +70 -62
  218. package/dist/plugins/remote-env-device.d.ts +32 -12
  219. package/dist/plugins/remote-env-device.js +18 -17
  220. package/dist/plugins/remote-env-e2b.d.ts +19 -3
  221. package/dist/plugins/remote-env-e2b.js +41 -42
  222. package/dist/plugins/remote-env-file-error.d.ts +2 -0
  223. package/dist/plugins/remote-env-file-error.js +1 -1
  224. package/dist/plugins/remote-env-host.d.ts +23 -8
  225. package/dist/plugins/remote-env-host.js +66 -70
  226. package/dist/plugins/remote-env-k8s.d.ts +13 -16
  227. package/dist/plugins/remote-env-k8s.js +61 -54
  228. package/dist/plugins/remote-env-local-docker.d.ts +30 -5
  229. package/dist/plugins/remote-env-local-docker.js +46 -59
  230. package/dist/plugins/remote-env-ssh.d.ts +18 -4
  231. package/dist/plugins/remote-env-ssh.js +72 -56
  232. package/dist/plugins/remote-shell.d.ts +100 -3
  233. package/dist/plugins/remote-shell.js +166 -2
  234. package/dist/plugins/retention-store-sql.d.ts +23 -8
  235. package/dist/plugins/retention-store-sql.js +13 -1
  236. package/dist/plugins/run-store-sql.d.ts +13 -3
  237. package/dist/plugins/run-store-sql.js +5 -0
  238. package/dist/plugins/schema-consistency.d.ts +15 -5
  239. package/dist/plugins/schema-consistency.js +14 -8
  240. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  241. package/dist/plugins/schema-drift-sentence.js +38 -0
  242. package/dist/plugins/session-fork.d.ts +63 -12
  243. package/dist/plugins/session-fork.js +38 -21
  244. package/dist/plugins/session-ownership.d.ts +7 -0
  245. package/dist/plugins/session-ownership.js +5 -0
  246. package/dist/plugins/session-row-sql.d.ts +22 -0
  247. package/dist/plugins/session-row-sql.js +81 -0
  248. package/dist/plugins/session-store.d.ts +1 -1
  249. package/dist/plugins/session-workspace-cell.d.ts +17 -0
  250. package/dist/plugins/session-workspace-cell.js +38 -0
  251. package/dist/plugins/store-backend.d.ts +1 -1
  252. package/dist/plugins/store-backend.js +26 -14
  253. package/dist/plugins/store-contracts.d.ts +25 -5
  254. package/dist/plugins/tidb-pool.d.ts +12 -7
  255. package/dist/plugins/tidb-pool.js +15 -3
  256. package/dist/plugins/tidb-session-store.d.ts +74 -28
  257. package/dist/plugins/tidb-session-store.js +132 -53
  258. package/dist/plugins/tool-result-store-sql.d.ts +16 -8
  259. package/dist/plugins/tool-result-store-sql.js +26 -17
  260. package/dist/plugins/worktree-isolation.d.ts +3 -2
  261. package/dist/plugins/worktree-isolation.js +1 -7
  262. package/dist/project-identity.d.ts +1 -14
  263. package/dist/project-identity.js +0 -4
  264. package/dist/project-memory.d.ts +9 -15
  265. package/dist/project-memory.js +5 -4
  266. package/dist/request-key-closure.js +1 -1
  267. package/dist/request-value-closure.d.ts +6 -7
  268. package/dist/request-value-closure.js +1 -3
  269. package/dist/rules-consent.js +6 -4
  270. package/dist/run-local.d.ts +6 -0
  271. package/dist/run-local.js +36 -26
  272. package/dist/runs.d.ts +9 -2
  273. package/dist/runs.js +9 -2
  274. package/dist/sandbox-git-plane.d.ts +2 -2
  275. package/dist/security.d.ts +38 -12
  276. package/dist/security.js +8 -2
  277. package/dist/server-secret-env.d.ts +15 -8
  278. package/dist/server-secret-env.js +4 -2
  279. package/dist/session-children-retire.d.ts +27 -9
  280. package/dist/session-children-retire.js +9 -5
  281. package/dist/session-incarnation.d.ts +19 -10
  282. package/dist/session-incarnation.js +5 -3
  283. package/dist/session-row.d.ts +114 -5
  284. package/dist/session-row.js +55 -5
  285. package/dist/session-titler.d.ts +2 -2
  286. package/dist/session-workspace.d.ts +130 -0
  287. package/dist/session-workspace.js +83 -0
  288. package/dist/spec-fields.d.ts +1 -1
  289. package/dist/standing-answer-grant.d.ts +2 -13
  290. package/dist/standing-answer-grant.js +0 -7
  291. package/dist/task-a2a.d.ts +44 -14
  292. package/dist/task-a2a.js +46 -20
  293. package/dist/task-cwd.d.ts +6 -1
  294. package/dist/task-mcp.d.ts +33 -20
  295. package/dist/task-mcp.js +46 -22
  296. package/dist/tool-approval.d.ts +38 -8
  297. package/dist/tool-approval.js +57 -13
  298. package/dist/trace/core-keyset-guard.d.ts +33 -18
  299. package/dist/trace/engine-notice-wire.d.ts +1 -1
  300. package/dist/trace/engine-notice-wire.js +2 -0
  301. package/dist/trace/exit-seat.d.ts +20 -1
  302. package/dist/trace/exit-seat.js +3 -0
  303. package/dist/trace/extension-call-face.d.ts +33 -0
  304. package/dist/trace/extension-call-face.js +39 -0
  305. package/dist/trace/project.d.ts +26 -7
  306. package/dist/trace/project.js +30 -3
  307. package/dist/trace/redact.js +42 -13
  308. package/dist/trace/self-minted-wire.d.ts +240 -12
  309. package/dist/trace/self-minted-wire.js +100 -4
  310. package/dist/trace/wire-projection-faces.d.ts +6 -6
  311. package/dist/trace/wire-projection-faces.js +6 -5
  312. package/dist/url-refusal.d.ts +1 -1
  313. package/dist/user-authored-text.d.ts +2 -2
  314. package/dist/user-authored-text.js +16 -1
  315. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  316. package/dist/web-admin-client/apply-effective.js +20 -10
  317. package/dist/web-admin-client/center-request.d.ts +3 -2
  318. package/dist/web-admin-client/center-request.js +2 -3
  319. package/dist/web-admin-client/skills-mcp.d.ts +8 -4
  320. package/dist/web-admin-client/skills-mcp.js +32 -18
  321. package/dist/web-admin-client/types.d.ts +11 -0
  322. package/package.json +3 -3
  323. package/wire-changes/server@7.138.0.json +446 -0
  324. package/wire-changes/server@7.139.0.json +348 -0
  325. package/dist/http/device-placement.d.ts +0 -175
  326. package/dist/http/device-placement.js +0 -145
  327. package/dist/http/mcp-remote-text.d.ts +0 -5
  328. package/dist/http/mcp-remote-text.js +0 -7
@@ -1,22 +1,56 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
- import { assertFirstOwnerWritable, forkOwnerOf } from "./session-ownership.js";
4
- import { FileSessionRepo, SessionError, StoredSession, settledForkOptions, validateEntriesForImport } from "@sema-agent/core";
3
+ import { assertFirstOwnerWritable, forkOwnerOf, importerOwnerOf } from "./session-ownership.js";
4
+ import { FileSessionRepo, SessionError, StoredSession, sessionLogMarkOf, settledForkOptions, validateEntriesForImport } from "@sema-agent/core";
5
5
  import { z } from "zod";
6
6
  import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
7
+ import { judgeSessionRowWrite, sessionRowTagKey, settleSessionRowWrite, } from "../session-row.js";
7
8
  import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
8
9
  import { createZodColumnReader, decodeJsonColumn, readStringRecordColumn, SqlJsonColumnError } from "./sql-json-column.js";
9
10
  import { ONE_TIME_MIGRATIONS, migrationWindowOpen } from "./one-time-migrations.js";
10
11
  import { createLogger } from "../observability/logger.js";
11
12
  import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
12
- import { UnreadableLeaf, carriedIncarnation, headFromRow, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
13
+ import { UnreadableLeaf, carriedRowCells, headFromRow, incarnationFromCell, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
14
+ import { isExecutionLane } from "../execution-lane-caps.js";
15
+ import { SessionForkInProgressError, SessionWriterPresentError, WRITABLE, assertPlaceableLane, isUnusableArchive, settledMirrorOutcome, settledPlaceOutcome, } from "../execution-placement.js";
16
+ import { classifyForkTarget, forkByFreshId } from "./session-fork.js";
13
17
  import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
18
+ import { decodeSessionWorkspaceCell, encodeSessionWorkspaceCell } from "./session-workspace-cell.js";
14
19
  import { noteReplayInSessionOpenScope, openSessionForItself } from "../session-records-skipped-notice.js";
15
20
  import { createCorruptReadFailOpenSeat } from "../observability/corrupt-read-seat.js";
16
21
  import { buildObjectivePreview, isAttachmentFrame } from "../user-authored-text.js";
17
22
  import { classifySyncRelationshipByIds, ownerCellIs, SyncConflictError, StagedCommitOwnerChangedError, stagingIdFor, } from "../session-sync-kernel.js";
18
23
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
19
24
  const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
25
+ function archiveFaultWord(code) {
26
+ switch (code) {
27
+ case "not_found":
28
+ return "absent";
29
+ case "corrupt_session":
30
+ case "invalid_session":
31
+ return code;
32
+ case "invalid_entry":
33
+ case "invalid_fork_target":
34
+ case "storage":
35
+ case "conflict":
36
+ case "placement_refused":
37
+ case "unknown":
38
+ return "storage";
39
+ default: {
40
+ const unknownCode = code;
41
+ throw new Error(`LocalSessionStore: no archive state for session error code ${JSON.stringify(unknownCode)}`);
42
+ }
43
+ }
44
+ }
45
+ function unusableArchiveOf(word, fault) {
46
+ const code = fault instanceof SessionError ? undefined : fault?.code;
47
+ return { state: "unusable", word, fault, ...(word === "storage" && typeof code === "string" ? { errno: code } : {}) };
48
+ }
49
+ function importRowRefused(sessionId, landing) {
50
+ return new SessionError("conflict", landing === "create"
51
+ ? `session ${sessionId} already exists — importEntries only creates a session (replaceEntries rewrites one); nothing was written`
52
+ : `session ${sessionId} is not the importer's row — an import replaces only a log whose row the importer owns (or an anonymous row, by an anonymous import); nothing was written`);
53
+ }
20
54
  const SESSION_OBJECTIVE_PREVIEW_CHARS = 200;
21
55
  function userText(entry) {
22
56
  if (entry.type !== "message")
@@ -35,13 +69,26 @@ function userText(entry) {
35
69
  }
36
70
  return null;
37
71
  }
38
- const SIDECAR_DEGRADES = { lastActivity: "recency", title: "title", pinned: "pin state", "instruction-source": "instruction-source baseline", "session-meta": "session owner + incarnation (the write that needed it failed loudly)" };
72
+ const SIDECAR_DEGRADES = { lastActivity: "recency", title: "title", pinned: "pin state", "instruction-source": "instruction-source baseline", workspace: "each session's last recorded place and project (the next frame records it again)", "session-meta": "session owner + incarnation (the write that needed it failed loudly)" };
39
73
  const readPinnedSidecar = createZodColumnReader(z.record(z.string(), z.literal(true)));
74
+ const MIRROR_CELL = { role: "mirror" };
40
75
  function readSessionMetaCell(cell) {
41
- return typeof cell === "string" ? { incarnation: cell, owner: undefined } : 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 };
42
77
  }
43
78
  const ownerRecorded = (cell) => cell !== undefined && readSessionMetaCell(cell).owner !== undefined;
44
- const SESSION_META_SIDECAR = z.record(z.string(), z.union([z.string(), z.object({ incarnation: z.string(), owner: z.string().nullable() }).strict()]));
79
+ 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 } : {}) };
82
+ }
83
+ const isForkCreatedCell = (meta) => meta !== undefined && meta.forkKey !== undefined && meta.execution === undefined;
84
+ function readingOfCell(sessionId, meta, forking) {
85
+ return { occupant: sessionFaceFromRow(sessionId, meta.owner ?? null, meta.incarnation), cell: forking ? { role: "forking" } : (meta.execution ?? { role: "absent" }) };
86
+ }
87
+ const LOCAL_EXECUTION_CELL = z.discriminatedUnion("role", [
88
+ z.object({ role: z.literal("writer"), lane: z.custom((v) => typeof v === "string" && isExecutionLane(v), { message: "not an execution lane" }) }).strict(),
89
+ z.object({ role: z.literal("mirror") }).strict(),
90
+ ]);
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()]));
45
92
  const readSessionMetaSidecar = createZodColumnReader(SESSION_META_SIDECAR);
46
93
  function ownerClaimWindowOpen() {
47
94
  const window = ONE_TIME_MIGRATIONS.find((w) => w.id === "M10");
@@ -180,9 +227,12 @@ export class LocalSessionStore {
180
227
  titles;
181
228
  pinned;
182
229
  instructionSources;
230
+ workspaces;
183
231
  meta;
184
232
  log;
185
233
  sessionWriteLocks;
234
+ executionPlacement;
235
+ executionPlacementBackfill;
186
236
  constructor(repo, runs, dataDir, opts) {
187
237
  this.repo = repo;
188
238
  this.runs = runs;
@@ -192,11 +242,92 @@ export class LocalSessionStore {
192
242
  this.titles = sidecar("session-titles.json", "title", readStringRecordColumn);
193
243
  this.pinned = sidecar("session-pinned.json", "pinned", readPinnedSidecar);
194
244
  this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source", readStringRecordColumn);
245
+ this.workspaces = sidecar("session-workspaces.json", "workspace", readStringRecordColumn);
195
246
  this.meta = sidecar("session-incarnations.json", "session-meta", readSessionMetaSidecar, true);
196
247
  const root = dataDir ? resolve(dataDir) : undefined;
197
248
  if (root !== undefined && !SESSION_WRITE_LOCKS_BY_ROOT.has(root))
198
249
  SESSION_WRITE_LOCKS_BY_ROOT.set(root, new Map());
199
250
  this.sessionWriteLocks = root !== undefined ? SESSION_WRITE_LOCKS_BY_ROOT.get(root) : new Map();
251
+ this.executionPlacement = {
252
+ read: (sessionId) => this.readExecutionPlacement(sessionId),
253
+ place: (sessionId, occupant, cell) => this.placeExecution(sessionId, occupant, cell),
254
+ markMirror: (sessionId, occupant) => this.markMirrorExecution(sessionId, occupant),
255
+ };
256
+ this.executionPlacementBackfill = {
257
+ read: (sessionId) => this.placementRead(sessionId),
258
+ place: (sessionId, occupant, cell) => this.placeExecution(sessionId, occupant, cell),
259
+ };
260
+ }
261
+ async forkingRead(sessionId, meta) {
262
+ if (!isForkCreatedCell(meta))
263
+ return false;
264
+ const probe = await this.probeArchive(sessionId);
265
+ if (probe.state === "unusable" && probe.word === "storage")
266
+ return probe;
267
+ return probe.state === "absent";
268
+ }
269
+ async forkingOf(sessionId, meta) {
270
+ const forking = await this.forkingRead(sessionId, meta);
271
+ if (typeof forking !== "boolean")
272
+ throw forking.fault;
273
+ return forking;
274
+ }
275
+ async placementRead(sessionId) {
276
+ const read = await this.metaRead(sessionId);
277
+ if (read.state === "unusable")
278
+ return read;
279
+ if (read.state === "absent")
280
+ return undefined;
281
+ const forking = await this.forkingRead(sessionId, read.meta);
282
+ return typeof forking === "boolean" ? readingOfCell(sessionId, read.meta, forking) : forking;
283
+ }
284
+ async readExecutionPlacement(sessionId) {
285
+ const reading = await this.placementRead(sessionId);
286
+ if (isUnusableArchive(reading))
287
+ throw reading.fault;
288
+ return reading;
289
+ }
290
+ async placeExecution(sessionId, occupant, cell) {
291
+ assertPlaceableLane(sessionId, cell);
292
+ return this.withSessionWriteLock(sessionId, () => this.settleExecutionHeld(sessionId, (reading) => {
293
+ const settled = settledPlaceOutcome(sessionId, reading, occupant, cell);
294
+ return settled === WRITABLE ? { outcome: { outcome: "placed" }, execution: { role: "writer", lane: cell.lane } } : { outcome: settled };
295
+ }));
296
+ }
297
+ async markMirrorExecution(sessionId, occupant) {
298
+ return this.withSessionWriteLock(sessionId, async () => {
299
+ await this.meta.ensureLoaded();
300
+ const claim = this.metaCellOf(sessionId) === undefined ? undefined : await this.runs.getActiveTaskId(sessionId);
301
+ return this.settleExecutionHeld(sessionId, (reading) => {
302
+ const settled = settledMirrorOutcome(sessionId, reading, occupant);
303
+ if (settled !== WRITABLE)
304
+ return { outcome: settled };
305
+ return claim === undefined ? { outcome: { outcome: "mirrored" }, execution: MIRROR_CELL } : { outcome: { outcome: "active", activeTaskId: claim } };
306
+ });
307
+ });
308
+ }
309
+ async settleExecutionHeld(sessionId, judge) {
310
+ await this.meta.ensureLoaded();
311
+ const forking = await this.forkingOf(sessionId, this.metaCellOf(sessionId));
312
+ let settled;
313
+ let fault;
314
+ await this.writeMeta(sessionId, (before) => {
315
+ try {
316
+ settled = judge(before === undefined ? undefined : readingOfCell(sessionId, readSessionMetaCell(before), forking));
317
+ }
318
+ catch (e) {
319
+ fault = e;
320
+ return undefined;
321
+ }
322
+ return settled.execution === undefined || before === undefined ? undefined : cellWith(before, { execution: settled.execution });
323
+ });
324
+ if (fault !== undefined)
325
+ throw fault;
326
+ return settled.outcome;
327
+ }
328
+ metaCellOf(sessionId) {
329
+ const cell = this.meta.state.entries.get(sessionId);
330
+ return cell === undefined ? undefined : readSessionMetaCell(cell);
200
331
  }
201
332
  async writeMeta(sessionId, next) {
202
333
  await this.meta.ensureLoaded();
@@ -231,29 +362,44 @@ export class LocalSessionStore {
231
362
  locks.delete(sessionId);
232
363
  }
233
364
  }
234
- async existsInRepo(sessionId) {
365
+ async probeArchive(sessionId) {
235
366
  try {
236
367
  await this.repo.open({ id: sessionId, createdAt: "" });
237
- return true;
368
+ return { state: "readable" };
238
369
  }
239
370
  catch (e) {
240
- if (isNotFound(e))
241
- return false;
242
- throw e;
371
+ const word = e instanceof SessionError ? archiveFaultWord(e.code) : "storage";
372
+ return word === "absent" ? { state: "absent" } : unusableArchiveOf(word, e);
243
373
  }
244
374
  }
375
+ async existsInRepo(sessionId) {
376
+ const probe = await this.probeArchive(sessionId);
377
+ if (probe.state === "unusable")
378
+ throw probe.fault;
379
+ return probe.state === "readable";
380
+ }
245
381
  async metaOf(sessionId) {
382
+ const read = await this.metaRead(sessionId);
383
+ if (read.state === "unusable")
384
+ throw read.fault;
385
+ return read.state === "found" ? read.meta : undefined;
386
+ }
387
+ async metaRead(sessionId) {
246
388
  await this.meta.ensureLoaded();
247
389
  const cell = this.meta.state.entries.get(sessionId);
248
390
  if (cell !== undefined)
249
- return readSessionMetaCell(cell);
250
- if (!(await this.existsInRepo(sessionId)))
251
- return undefined;
252
- return this.cellOfPresent(sessionId);
391
+ return { state: "found", meta: readSessionMetaCell(cell) };
392
+ const probe = await this.probeArchive(sessionId);
393
+ if (probe.state === "unusable")
394
+ return probe;
395
+ if (probe.state === "absent")
396
+ return { state: "absent" };
397
+ const meta = await this.cellOfPresent(sessionId);
398
+ return meta === undefined ? { state: "absent" } : { state: "found", meta };
253
399
  }
254
400
  async cellOfPresent(sessionId) {
255
401
  if (!this.meta.state.entries.has(sessionId))
256
- await this.writeMeta(sessionId, (before) => (before === undefined ? mintSessionIncarnation() : undefined));
402
+ await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation() } : undefined));
257
403
  const cell = this.meta.state.entries.get(sessionId);
258
404
  return cell === undefined ? undefined : readSessionMetaCell(cell);
259
405
  }
@@ -279,55 +425,86 @@ export class LocalSessionStore {
279
425
  return this.titles.state.entries.has(sessionId) ? "titled" : "untitled";
280
426
  }
281
427
  async setTitleIfNull(sessionId, title) {
282
- await this.titles.ensureLoaded();
283
- const entries = this.titles.state.entries;
284
- if (entries.has(sessionId))
285
- return false;
286
- entries.set(sessionId, title);
287
- await this.titles.persist();
288
- return true;
428
+ return this.withSessionWriteLock(sessionId, async () => {
429
+ await this.titles.ensureLoaded();
430
+ const entries = this.titles.state.entries;
431
+ if (entries.has(sessionId))
432
+ return false;
433
+ entries.set(sessionId, title);
434
+ await this.titles.persist();
435
+ return true;
436
+ });
289
437
  }
290
- async patchSessionMeta(sessionId, patch, face) {
438
+ async writeSessionRowCells(sessionId, cells, how) {
291
439
  return this.withSessionWriteLock(sessionId, async () => {
292
- await this.meta.ensureLoaded();
293
- if (!this.occupiedBy(sessionId, face) || (await this.listedMeta(sessionId)) === undefined)
294
- return undefined;
295
- const { title, pinned } = patch;
296
- if (title !== undefined) {
297
- await this.titles.ensureLoaded();
298
- const ok = await this.titles.commit((draft) => {
299
- if (title === null)
300
- return draft.delete(sessionId);
301
- if (draft.get(sessionId) === title)
302
- return false;
303
- draft.set(sessionId, title);
304
- return true;
305
- });
306
- if (!ok)
307
- throw new SessionError("storage", `LocalSessionStore: title sidecar write failed for session ${sessionId} — the title was not recorded`);
308
- }
309
- if (pinned !== undefined) {
310
- await this.pinned.ensureLoaded();
311
- const ok = await this.pinned.commit((draft) => {
312
- if (!pinned)
313
- return draft.delete(sessionId);
314
- if (draft.get(sessionId) === true)
315
- return false;
316
- draft.set(sessionId, true);
317
- return true;
318
- });
319
- if (!ok)
320
- throw new SessionError("storage", `LocalSessionStore: pin sidecar write failed for session ${sessionId} — the pin was not recorded`);
321
- }
322
- const rollups = await this.runRollups();
323
440
  await this.loadRowSidecars();
324
- const meta = await this.listedMeta(sessionId);
325
- return meta === undefined ? undefined : this.summaryOf(meta, rollups);
441
+ const locked = await this.lockedRowOf(sessionId);
442
+ const mark = how.row.log === undefined || locked === undefined ? undefined : sessionLogMarkOf((await this.exportEntries(sessionId)) ?? []);
443
+ const verdict = judgeSessionRowWrite(sessionId, how, locked, () => {
444
+ if (mark === undefined)
445
+ throw new Error(`LocalSessionStore.writeSessionRowCells: the judge asked session ${sessionId}'s log mark, but the expectation carries none`);
446
+ return mark;
447
+ });
448
+ switch (verdict) {
449
+ case "absent":
450
+ return { kind: "absent" };
451
+ case "mirror":
452
+ return { kind: "mirror" };
453
+ case "write":
454
+ await this.writeRowCellsHeld(sessionId, cells);
455
+ return settleSessionRowWrite(how, verdict, await this.listRowOf(sessionId));
456
+ case "changed":
457
+ return settleSessionRowWrite(how, verdict, await this.listRowOf(sessionId));
458
+ default: {
459
+ const unknownVerdict = verdict;
460
+ throw new Error(`LocalSessionStore.writeSessionRowCells: no verdict ${JSON.stringify(unknownVerdict)}`);
461
+ }
462
+ }
326
463
  });
327
464
  }
328
- occupiedBy(sessionId, face) {
329
- const cell = this.meta.state.entries.get(sessionId);
330
- return cell !== undefined && readSessionMetaCell(cell).incarnation === face.incarnation;
465
+ async lockedRowOf(sessionId) {
466
+ const meta = this.metaCellOf(sessionId);
467
+ if (meta === undefined || (await this.listedMeta(sessionId)) === undefined)
468
+ return undefined;
469
+ return { owner: meta.owner ?? null, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent"), placed: false, mirror: meta.execution?.role === "mirror", cells: this.rowCellsOf(sessionId) };
470
+ }
471
+ rowCellsOf(sessionId) {
472
+ return { title: this.titles.state.entries.get(sessionId) ?? null, pinned: this.pinned.state.entries.get(sessionId) === true };
473
+ }
474
+ async writeRowCellsHeld(sessionId, cells) {
475
+ const { title, pinned } = cells;
476
+ if (title !== undefined) {
477
+ const ok = await this.titles.commit((draft) => {
478
+ if (title === null)
479
+ return draft.delete(sessionId);
480
+ if (draft.get(sessionId) === title)
481
+ return false;
482
+ draft.set(sessionId, title);
483
+ return true;
484
+ });
485
+ if (!ok)
486
+ throw new SessionError("storage", `LocalSessionStore: title sidecar write failed for session ${sessionId} — the title was not recorded`);
487
+ }
488
+ if (pinned !== undefined) {
489
+ const ok = await this.pinned.commit((draft) => {
490
+ if (!pinned)
491
+ return draft.delete(sessionId);
492
+ if (draft.get(sessionId) === true)
493
+ return false;
494
+ draft.set(sessionId, true);
495
+ return true;
496
+ });
497
+ if (!ok)
498
+ throw new SessionError("storage", `LocalSessionStore: pin sidecar write failed for session ${sessionId} — the pin was not recorded`);
499
+ }
500
+ }
501
+ async listRowOf(sessionId) {
502
+ await this.loadRowSidecars();
503
+ const listed = await this.listedMeta(sessionId);
504
+ const meta = this.metaCellOf(sessionId);
505
+ if (listed === undefined || meta === undefined)
506
+ return undefined;
507
+ return { row: await this.summaryOf(listed, await this.runRollups()), occupant: { owner: meta.owner ?? null, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent") } };
331
508
  }
332
509
  async sessionSummaryOf(sessionId) {
333
510
  await this.loadRowSidecars();
@@ -352,6 +529,26 @@ export class LocalSessionStore {
352
529
  if (!committed)
353
530
  throw new Error("LocalSessionStore: instruction-source sidecar write failed — baseline not recorded");
354
531
  }
532
+ async readSessionWorkspace(sessionId) {
533
+ await this.workspaces.ensureLoaded();
534
+ return decodeSessionWorkspaceCell(this.workspaces.state.entries.get(sessionId) ?? null, sessionId);
535
+ }
536
+ async recordSessionWorkspace(sessionId, incarnation, workspace) {
537
+ const text = encodeSessionWorkspaceCell(workspace, JSON.stringify, sessionId);
538
+ await this.withSessionWriteLock(sessionId, async () => {
539
+ if ((await this.metaOf(sessionId))?.incarnation !== incarnation)
540
+ return;
541
+ await this.workspaces.ensureLoaded();
542
+ const committed = await this.workspaces.commit((draft) => {
543
+ if (draft.get(sessionId) === text)
544
+ return false;
545
+ draft.set(sessionId, text);
546
+ return true;
547
+ });
548
+ if (!committed)
549
+ throw new SessionError("storage", `LocalSessionStore: workspace sidecar write failed for session ${sessionId} — its place was not recorded`);
550
+ });
551
+ }
355
552
  async ensureLoaded() {
356
553
  if (!this.sidecarPath)
357
554
  return;
@@ -449,6 +646,8 @@ export class LocalSessionStore {
449
646
  throw e;
450
647
  }
451
648
  await this.meta.ensureLoaded();
649
+ if (isForkCreatedCell(this.metaCellOf(sessionId)))
650
+ throw new SessionForkInProgressError(sessionId);
452
651
  const session = await this.repo.create({ id: sessionId });
453
652
  await this.writeMeta(sessionId, (before) => ({ incarnation: mintSessionIncarnation(), owner: before !== undefined && ownerRecorded(before) ? (readSessionMetaCell(before).owner ?? null) : null }));
454
653
  return this.handOut(sessionId, session);
@@ -466,13 +665,17 @@ export class LocalSessionStore {
466
665
  return;
467
666
  assertFirstOwnerWritable(sessionId, owner);
468
667
  const existed = await this.existsInRepo(sessionId);
668
+ if (!existed && isForkCreatedCell(this.metaCellOf(sessionId)))
669
+ throw new SessionForkInProgressError(sessionId);
469
670
  const claim = existed && owner !== null && ownerClaimWindowOpen();
470
671
  const wrote = await this.writeMeta(sessionId, (before) => {
471
672
  if (ownerRecorded(before))
472
673
  return undefined;
473
- if (!existed || claim)
474
- return { incarnation: existed && before !== undefined ? readSessionMetaCell(before).incarnation : mintSessionIncarnation(), owner };
475
- return before === undefined ? mintSessionIncarnation() : undefined;
674
+ if (claim)
675
+ return before === undefined ? { incarnation: mintSessionIncarnation(), owner } : cellWith(before, { owner });
676
+ if (!existed)
677
+ return { incarnation: mintSessionIncarnation(), owner };
678
+ return before === undefined ? { incarnation: mintSessionIncarnation() } : undefined;
476
679
  });
477
680
  if (!existed && wrote)
478
681
  await this.repo.create({ id: sessionId });
@@ -480,9 +683,6 @@ export class LocalSessionStore {
480
683
  this.log.info("session_owner_claimed", { sessionId });
481
684
  });
482
685
  }
483
- async restampOwner(sessionId, owner) {
484
- await this.writeMeta(sessionId, (before) => (before === undefined ? undefined : { incarnation: readSessionMetaCell(before).incarnation, owner }));
485
- }
486
686
  async touch(sessionId) {
487
687
  this.lastActivity.set(sessionId, new Date().toISOString());
488
688
  this.scheduleFlush();
@@ -524,17 +724,29 @@ export class LocalSessionStore {
524
724
  }
525
725
  async summaryOf(meta, rollups) {
526
726
  const firstActivityAt = meta.createdAt || new Date(0).toISOString();
727
+ const owner = this.ownerCellOf(meta.id);
728
+ const cells = this.rowCellsOf(meta.id);
729
+ const rowTag = this.rowTagKeyOf(meta.id, owner, cells);
730
+ const lastActivityAt = this.lastActivity.get(meta.id) ?? firstActivityAt;
731
+ const objectivePreview = await this.previewOf(meta);
527
732
  return {
528
733
  sessionId: meta.id,
529
- owner: this.ownerCellOf(meta.id),
530
- lastActivityAt: this.lastActivity.get(meta.id) ?? firstActivityAt,
734
+ owner,
735
+ lastActivityAt,
531
736
  firstActivityAt,
532
- objectivePreview: await this.previewOf(meta),
737
+ objectivePreview,
533
738
  ...this.runProjectionOf(meta.id, rollups),
534
- title: this.titles.state.entries.get(meta.id) ?? null,
535
- pinned: this.pinned.state.entries.get(meta.id) === true,
739
+ title: cells.title,
740
+ pinned: cells.pinned,
741
+ ...rowTag,
536
742
  };
537
743
  }
744
+ rowTagKeyOf(sessionId, owner, cells) {
745
+ const meta = this.metaCellOf(sessionId);
746
+ if (meta === undefined)
747
+ return {};
748
+ return sessionRowTagKey({ sessionId, owner, incarnation: incarnationFromCell(sessionId, meta.incarnation, "absent"), cells });
749
+ }
538
750
  async runRollups() {
539
751
  const rows = await this.runs.listSessions({ limit: Number.MAX_SAFE_INTEGER });
540
752
  return new Map(rows.map((r) => [r.sessionId, { runCount: r.runCount, lastStatus: r.lastStatus, lastRunId: r.lastRunId, ...(r.lastTaskRunId !== undefined ? { lastTaskRunId: r.lastTaskRunId } : {}) }]));
@@ -559,24 +771,52 @@ export class LocalSessionStore {
559
771
  }
560
772
  }
561
773
  async fork(sourceId, owner) {
774
+ return forkByFreshId((s, o, t) => this.forkTo(s, o, t), sourceId, owner);
775
+ }
776
+ async forkTo(sourceId, owner, target) {
562
777
  await this.meta.ensureLoaded();
563
- return this.withSessionWriteLock(sourceId, async () => {
564
- const source = this.meta.state.entries.get(sourceId);
565
- const forkOwner = forkOwnerOf(owner, source === undefined ? undefined : readSessionMetaCell(source).owner);
778
+ if (target.id === sourceId)
779
+ return this.withSessionWriteLock(sourceId, () => this.forkToHeld(sourceId, owner, target));
780
+ const [first, second] = sourceId < target.id ? [sourceId, target.id] : [target.id, sourceId];
781
+ return this.withSessionWriteLock(first, () => this.withSessionWriteLock(second, () => this.forkToHeld(sourceId, owner, target)));
782
+ }
783
+ async forkToHeld(sourceId, owner, target) {
784
+ const sourceMeta = { id: sourceId, createdAt: "" };
785
+ let forkOptions;
786
+ try {
787
+ forkOptions = await settledForkOptions(await this.repo.open(sourceMeta));
788
+ }
789
+ catch (e) {
790
+ if (isNotFound(e))
791
+ return null;
792
+ throw e;
793
+ }
794
+ const forkOwner = forkOwnerOf(owner, this.metaCellOf(sourceId)?.owner);
795
+ const existing = this.metaCellOf(target.id);
796
+ const targetArchived = await this.existsInRepo(target.id);
797
+ const copy = () => this.repo.fork(sourceMeta, { ...forkOptions, id: target.id });
798
+ 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 } : {}) });
800
+ if (!created)
801
+ throw new SessionError("storage", `LocalSessionStore: fork target ${target.id} gained a session-meta cell under its own write lock — refusing to guess`);
566
802
  try {
567
- const sourceMeta = { id: sourceId, createdAt: "" };
568
- const forkOptions = await settledForkOptions(await this.repo.open(sourceMeta));
569
- const session = await this.repo.fork(sourceMeta, forkOptions);
570
- const id = (await session.getMetadata()).id;
571
- await this.writeMeta(id, () => (forkOwner === undefined ? mintSessionIncarnation() : { incarnation: mintSessionIncarnation(), owner: forkOwner }));
572
- return id;
803
+ await copy();
573
804
  }
574
- catch (e) {
575
- if (isNotFound(e))
576
- return null;
577
- throw e;
805
+ catch (err) {
806
+ try {
807
+ await this.dropMeta(target.id);
808
+ }
809
+ catch (dropErr) {
810
+ throw new SessionError("storage", `LocalSessionStore: fork into ${target.id} failed while copying (${err instanceof Error ? err.message : String(err)}) and its target cell could not be withdrawn (${dropErr instanceof Error ? dropErr.message : String(dropErr)}) — the target stays in the fork-in-progress state; retry the same fork or delete the session`, err instanceof Error ? err : undefined);
811
+ }
812
+ throw err;
578
813
  }
579
- });
814
+ return { outcome: "created", id: target.id };
815
+ }
816
+ const out = classifyForkTarget(target, { owner: existing?.owner, forkKey: existing?.forkKey }, forkOwner);
817
+ if (out.outcome === "replayed" && !targetArchived)
818
+ await copy();
819
+ return out;
580
820
  }
581
821
  async exportEntries(sessionId) {
582
822
  if (!this.repo.exportEntries)
@@ -607,28 +847,90 @@ export class LocalSessionStore {
607
847
  return gen();
608
848
  }
609
849
  async importEntries(sessionId, owner, entries) {
610
- await this.withSessionWriteLock(sessionId, () => this.importEntriesAs(sessionId, owner, entries, mintSessionIncarnation()));
611
- }
612
- async importEntriesAs(sessionId, owner, entries, incarnation) {
613
- if (!this.repo.importEntries)
614
- throw new SessionError("storage", "session import is not supported by this local backend");
615
- await this.meta.ensureLoaded();
616
- await this.repo.importEntries(sessionId, owner ?? undefined, entries);
617
- await this.writeMeta(sessionId, () => ({ incarnation, owner }));
850
+ await this.withImportLock(sessionId, owner, (importer) => this.importHeld(sessionId, importer, entries, "create"));
618
851
  }
619
852
  async replaceEntries(sessionId, owner, entries) {
620
- await this.withSessionWriteLock(sessionId, () => this.replaceEntriesHeld(sessionId, owner, entries));
853
+ await this.withImportLock(sessionId, owner, (importer) => this.importHeld(sessionId, importer, entries, "replace"));
854
+ }
855
+ async withImportLock(sessionId, owner, body) {
856
+ const importer = importerOwnerOf(owner);
857
+ return this.withSessionWriteLock(sessionId, () => body(importer));
621
858
  }
622
- async replaceEntriesHeld(sessionId, owner, entries) {
859
+ async importHeld(sessionId, owner, entries, landing) {
623
860
  if (!this.repo.importEntries)
624
861
  throw new SessionError("storage", "session import is not supported by this local backend");
625
- if (!this.repo.delete)
626
- throw new SessionError("storage", "session replace is not supported by this local backend (no delete seam)");
862
+ await this.meta.ensureLoaded();
627
863
  validateEntriesForImport(entries);
628
- const current = await this.metaOf(sessionId);
629
- const prior = current === undefined ? undefined : { owner: current.owner ?? null, incarnation: current.incarnation };
630
- await this.deleteSessionHeld(sessionId, owner);
631
- await this.importEntriesAs(sessionId, owner, entries, carriedIncarnation(sessionId, prior, owner));
864
+ const current = this.metaCellOf(sessionId);
865
+ if (current?.execution?.role === "writer")
866
+ throw new SessionWriterPresentError(sessionId, current.execution.lane);
867
+ if (await this.forkingOf(sessionId, current))
868
+ throw new SessionForkInProgressError(sessionId);
869
+ if (!(await this.importMayLand(sessionId, current, owner, landing)))
870
+ throw importRowRefused(sessionId, landing);
871
+ const prior = await this.landImportCell(sessionId, owner);
872
+ switch (landing) {
873
+ case "create":
874
+ case "judged":
875
+ try {
876
+ const archive = (await this.repo.headOf(sessionId)) ?? null;
877
+ await this.repo.importEntries(sessionId, archive?.owner ?? undefined, entries, { expect: { row: archive } });
878
+ }
879
+ catch (err) {
880
+ await this.withdrawImportCell(sessionId, prior, err);
881
+ throw err;
882
+ }
883
+ break;
884
+ case "replace":
885
+ try {
886
+ await this.repo.delete({ id: sessionId, createdAt: "" });
887
+ }
888
+ catch (e) {
889
+ if (!isNotFound(e))
890
+ throw e;
891
+ }
892
+ await this.repo.importEntries(sessionId, undefined, entries, { expect: { row: null } });
893
+ break;
894
+ default: {
895
+ const unknownLanding = landing;
896
+ throw new Error(`LocalSessionStore: no import landing ${JSON.stringify(unknownLanding)}`);
897
+ }
898
+ }
899
+ await this.touch(sessionId);
900
+ }
901
+ async importMayLand(sessionId, current, owner, landing) {
902
+ switch (landing) {
903
+ case "create":
904
+ return current === undefined && !(await this.existsInRepo(sessionId));
905
+ case "replace":
906
+ return current !== undefined ? ownerEq(current.owner ?? null, owner) : ownerEq(null, owner) || !(await this.existsInRepo(sessionId));
907
+ case "judged":
908
+ return true;
909
+ default: {
910
+ const unknownLanding = landing;
911
+ throw new Error(`LocalSessionStore: no import landing ${JSON.stringify(unknownLanding)}`);
912
+ }
913
+ }
914
+ }
915
+ async landImportCell(sessionId, owner) {
916
+ let prior;
917
+ await this.writeMeta(sessionId, (before) => {
918
+ prior = before;
919
+ const cells = carriedRowCells(sessionId, before === undefined ? undefined : readSessionMetaCell(before));
920
+ return { incarnation: cells.incarnation, owner, execution: MIRROR_CELL, ...(cells.forkKey !== null ? { forkKey: cells.forkKey } : {}) };
921
+ });
922
+ return prior;
923
+ }
924
+ async withdrawImportCell(sessionId, prior, cause) {
925
+ try {
926
+ if (prior === undefined)
927
+ await this.dropMeta(sessionId);
928
+ else
929
+ await this.writeMeta(sessionId, () => prior);
930
+ }
931
+ catch (withdrawErr) {
932
+ throw new SessionError("storage", `LocalSessionStore: import into ${sessionId} failed (${cause instanceof Error ? cause.message : String(cause)}) and its session-meta cell could not be put back (${withdrawErr instanceof Error ? withdrawErr.message : String(withdrawErr)}) — the session reads as a mirror until the next push`, cause instanceof Error ? cause : undefined);
933
+ }
632
934
  }
633
935
  beginImportStaging(realSessionId, token) {
634
936
  const h = new LocalStagingHandle(this, realSessionId, stagingIdFor(realSessionId, token));
@@ -642,10 +944,12 @@ export class LocalSessionStore {
642
944
  this.stagings.delete(stagingId);
643
945
  }
644
946
  async commitStagedImport(sessionId, owner, staged, opts) {
645
- return this.withSessionWriteLock(sessionId, async () => {
947
+ return this.withImportLock(sessionId, owner, async (importer) => {
646
948
  const current = await this.metaOf(sessionId);
647
949
  if (!ownerCellIs(opts.expectedOwner, current === undefined ? undefined : (current.owner ?? null)))
648
950
  throw new StagedCommitOwnerChangedError(sessionId);
951
+ if (await this.forkingOf(sessionId, current))
952
+ throw new SessionForkInProgressError(sessionId);
649
953
  const dstEntries = await this.exportEntries(sessionId);
650
954
  const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
651
955
  if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
@@ -658,11 +962,14 @@ export class LocalSessionStore {
658
962
  throw new SyncConflictError(sessionId, contentForkRelation(diverged, rel.newEntryIds));
659
963
  }
660
964
  const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
965
+ if (current?.execution?.role === "writer")
966
+ throw new SessionWriterPresentError(sessionId, current.execution.lane);
661
967
  if (rel.relation !== "identical" || !contentEqual) {
662
- await this.replaceEntriesHeld(sessionId, owner, staged);
968
+ await this.importHeld(sessionId, importer, staged, "judged");
663
969
  }
664
970
  else {
665
- await this.restampOwner(sessionId, owner);
971
+ await this.landImportCell(sessionId, importer);
972
+ await this.touch(sessionId);
666
973
  }
667
974
  const meta = await this.metaOf(sessionId);
668
975
  if (meta === undefined)
@@ -685,7 +992,7 @@ export class LocalSessionStore {
685
992
  if (!isNotFound(e))
686
993
  throw e;
687
994
  }
688
- for (const [map, which] of [[this.titles, "title"], [this.pinned, "pinned"], [this.instructionSources, "instruction-source"]]) {
995
+ for (const [map, which] of [[this.titles, "title"], [this.pinned, "pinned"], [this.instructionSources, "instruction-source"], [this.workspaces, "workspace"]]) {
689
996
  await map.ensureLoaded();
690
997
  if (!(await map.deleteKey(sessionId)))
691
998
  throw new SessionError("storage", `LocalSessionStore: ${which} sidecar write failed while deleting session ${sessionId} — its ${which} is still on disk; retry the delete`);