@code-yeongyu/senpi-agent-core 2026.9.12 → 2026.9.13

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 (344) hide show
  1. package/README.md +4 -0
  2. package/dist/harness/agent-harness.d.ts +639 -384
  3. package/dist/harness/agent-harness.d.ts.map +1 -1
  4. package/dist/harness/agent-harness.js +5 -250
  5. package/dist/harness/agent-harness.js.map +1 -1
  6. package/dist/harness/compaction/branch-summarization.d.ts +12 -6
  7. package/dist/harness/compaction/branch-summarization.d.ts.map +1 -1
  8. package/dist/harness/compaction/branch-summarization.js +33 -27
  9. package/dist/harness/compaction/branch-summarization.js.map +1 -1
  10. package/dist/harness/compaction/compaction.d.ts +30 -8
  11. package/dist/harness/compaction/compaction.d.ts.map +1 -1
  12. package/dist/harness/compaction/compaction.js +53 -64
  13. package/dist/harness/compaction/compaction.js.map +1 -1
  14. package/dist/harness/config.d.ts +9 -0
  15. package/dist/harness/config.d.ts.map +1 -0
  16. package/dist/harness/config.js +35 -0
  17. package/dist/harness/config.js.map +1 -0
  18. package/dist/harness/context.d.ts +9 -0
  19. package/dist/harness/context.d.ts.map +1 -0
  20. package/dist/harness/context.js +13 -0
  21. package/dist/harness/context.js.map +1 -0
  22. package/dist/harness/env/nodejs.d.ts +25 -28
  23. package/dist/harness/env/nodejs.d.ts.map +1 -1
  24. package/dist/harness/env/nodejs.js +368 -125
  25. package/dist/harness/env/nodejs.js.map +1 -1
  26. package/dist/harness/events.d.ts +20 -38
  27. package/dist/harness/events.d.ts.map +1 -1
  28. package/dist/harness/events.js +233 -55
  29. package/dist/harness/events.js.map +1 -1
  30. package/dist/harness/execution/assistant.d.ts +42 -0
  31. package/dist/harness/execution/assistant.d.ts.map +1 -0
  32. package/dist/harness/execution/assistant.js +82 -0
  33. package/dist/harness/execution/assistant.js.map +1 -0
  34. package/dist/harness/execution/effect-gate.d.ts +22 -0
  35. package/dist/harness/execution/effect-gate.d.ts.map +1 -0
  36. package/dist/harness/execution/effect-gate.js +48 -0
  37. package/dist/harness/execution/effect-gate.js.map +1 -0
  38. package/dist/harness/execution/tools.d.ts +67 -0
  39. package/dist/harness/execution/tools.d.ts.map +1 -0
  40. package/dist/harness/execution/tools.js +121 -0
  41. package/dist/harness/execution/tools.js.map +1 -0
  42. package/dist/harness/hooks.d.ts +38 -0
  43. package/dist/harness/hooks.d.ts.map +1 -0
  44. package/dist/harness/hooks.js +404 -0
  45. package/dist/harness/hooks.js.map +1 -0
  46. package/dist/harness/messages.d.ts +2 -2
  47. package/dist/harness/messages.d.ts.map +1 -1
  48. package/dist/harness/messages.js.map +1 -1
  49. package/dist/harness/prompt-templates.d.ts +3 -2
  50. package/dist/harness/prompt-templates.d.ts.map +1 -1
  51. package/dist/harness/prompt-templates.js +17 -17
  52. package/dist/harness/prompt-templates.js.map +1 -1
  53. package/dist/harness/result.d.ts +93 -0
  54. package/dist/harness/result.d.ts.map +1 -1
  55. package/dist/harness/result.js +39 -0
  56. package/dist/harness/result.js.map +1 -1
  57. package/dist/harness/runtime/drive/boundary.d.ts +28 -0
  58. package/dist/harness/runtime/drive/boundary.d.ts.map +1 -0
  59. package/dist/harness/runtime/drive/boundary.js +173 -0
  60. package/dist/harness/runtime/drive/boundary.js.map +1 -0
  61. package/dist/harness/runtime/drive/checkpoint.d.ts +8 -0
  62. package/dist/harness/runtime/drive/checkpoint.d.ts.map +1 -0
  63. package/dist/harness/runtime/drive/checkpoint.js +133 -0
  64. package/dist/harness/runtime/drive/checkpoint.js.map +1 -0
  65. package/dist/harness/runtime/drive/deferred.d.ts +15 -0
  66. package/dist/harness/runtime/drive/deferred.d.ts.map +1 -0
  67. package/dist/harness/runtime/drive/deferred.js +179 -0
  68. package/dist/harness/runtime/drive/deferred.js.map +1 -0
  69. package/dist/harness/runtime/drive/generation.d.ts +8 -0
  70. package/dist/harness/runtime/drive/generation.d.ts.map +1 -0
  71. package/dist/harness/runtime/drive/generation.js +205 -0
  72. package/dist/harness/runtime/drive/generation.js.map +1 -0
  73. package/dist/harness/runtime/drive/reconcile.d.ts +5 -0
  74. package/dist/harness/runtime/drive/reconcile.d.ts.map +1 -0
  75. package/dist/harness/runtime/drive/reconcile.js +140 -0
  76. package/dist/harness/runtime/drive/reconcile.js.map +1 -0
  77. package/dist/harness/runtime/drive/recovery.d.ts +8 -0
  78. package/dist/harness/runtime/drive/recovery.d.ts.map +1 -0
  79. package/dist/harness/runtime/drive/recovery.js +85 -0
  80. package/dist/harness/runtime/drive/recovery.js.map +1 -0
  81. package/dist/harness/runtime/drive/response.d.ts +23 -0
  82. package/dist/harness/runtime/drive/response.d.ts.map +1 -0
  83. package/dist/harness/runtime/drive/response.js +394 -0
  84. package/dist/harness/runtime/drive/response.js.map +1 -0
  85. package/dist/harness/runtime/drive/retry.d.ts +4 -0
  86. package/dist/harness/runtime/drive/retry.d.ts.map +1 -0
  87. package/dist/harness/runtime/drive/retry.js +34 -0
  88. package/dist/harness/runtime/drive/retry.js.map +1 -0
  89. package/dist/harness/runtime/drive/structural.d.ts +32 -0
  90. package/dist/harness/runtime/drive/structural.d.ts.map +1 -0
  91. package/dist/harness/runtime/drive/structural.js +929 -0
  92. package/dist/harness/runtime/drive/structural.js.map +1 -0
  93. package/dist/harness/runtime/drive/terminal.d.ts +7 -0
  94. package/dist/harness/runtime/drive/terminal.d.ts.map +1 -0
  95. package/dist/harness/runtime/drive/terminal.js +49 -0
  96. package/dist/harness/runtime/drive/terminal.js.map +1 -0
  97. package/dist/harness/runtime/drive/tool-placement.d.ts +14 -0
  98. package/dist/harness/runtime/drive/tool-placement.d.ts.map +1 -0
  99. package/dist/harness/runtime/drive/tool-placement.js +244 -0
  100. package/dist/harness/runtime/drive/tool-placement.js.map +1 -0
  101. package/dist/harness/runtime/drive/tools.d.ts +6 -0
  102. package/dist/harness/runtime/drive/tools.d.ts.map +1 -0
  103. package/dist/harness/runtime/drive/tools.js +449 -0
  104. package/dist/harness/runtime/drive/tools.js.map +1 -0
  105. package/dist/harness/runtime/drive.d.ts +6 -0
  106. package/dist/harness/runtime/drive.d.ts.map +1 -0
  107. package/dist/harness/runtime/drive.js +93 -0
  108. package/dist/harness/runtime/drive.js.map +1 -0
  109. package/dist/harness/runtime/harness.d.ts +59 -0
  110. package/dist/harness/runtime/harness.d.ts.map +1 -0
  111. package/dist/harness/runtime/harness.js +302 -0
  112. package/dist/harness/runtime/harness.js.map +1 -0
  113. package/dist/harness/runtime/index.d.ts +2 -0
  114. package/dist/harness/runtime/index.d.ts.map +1 -0
  115. package/dist/harness/runtime/index.js +2 -0
  116. package/dist/harness/runtime/index.js.map +1 -0
  117. package/dist/harness/runtime/lane.d.ts +118 -0
  118. package/dist/harness/runtime/lane.d.ts.map +1 -0
  119. package/dist/harness/runtime/lane.js +1559 -0
  120. package/dist/harness/runtime/lane.js.map +1 -0
  121. package/dist/harness/runtime/progress.d.ts +15 -0
  122. package/dist/harness/runtime/progress.d.ts.map +1 -0
  123. package/dist/harness/runtime/progress.js +66 -0
  124. package/dist/harness/runtime/progress.js.map +1 -0
  125. package/dist/harness/runtime/reducer.d.ts +5 -0
  126. package/dist/harness/runtime/reducer.d.ts.map +1 -0
  127. package/dist/harness/runtime/reducer.js +240 -0
  128. package/dist/harness/runtime/reducer.js.map +1 -0
  129. package/dist/harness/runtime/restore.d.ts +24 -0
  130. package/dist/harness/runtime/restore.d.ts.map +1 -0
  131. package/dist/harness/runtime/restore.js +117 -0
  132. package/dist/harness/runtime/restore.js.map +1 -0
  133. package/dist/harness/runtime/transcript.d.ts +21 -0
  134. package/dist/harness/runtime/transcript.d.ts.map +1 -0
  135. package/dist/harness/runtime/transcript.js +72 -0
  136. package/dist/harness/runtime/transcript.js.map +1 -0
  137. package/dist/harness/runtime/types.d.ts +105 -0
  138. package/dist/harness/runtime/types.d.ts.map +1 -0
  139. package/dist/harness/runtime/types.js +50 -0
  140. package/dist/harness/runtime/types.js.map +1 -0
  141. package/dist/harness/session/commit.d.ts +53 -0
  142. package/dist/harness/session/commit.d.ts.map +1 -0
  143. package/dist/harness/session/commit.js +57 -0
  144. package/dist/harness/session/commit.js.map +1 -0
  145. package/dist/harness/session/context.d.ts +6 -19
  146. package/dist/harness/session/context.d.ts.map +1 -1
  147. package/dist/harness/session/context.js +31 -53
  148. package/dist/harness/session/context.js.map +1 -1
  149. package/dist/harness/session/fork-policy.d.ts +21 -0
  150. package/dist/harness/session/fork-policy.d.ts.map +1 -0
  151. package/dist/harness/session/fork-policy.js +55 -0
  152. package/dist/harness/session/fork-policy.js.map +1 -0
  153. package/dist/harness/session/fork.d.ts +16 -0
  154. package/dist/harness/session/fork.d.ts.map +1 -0
  155. package/dist/harness/session/fork.js +79 -0
  156. package/dist/harness/session/fork.js.map +1 -0
  157. package/dist/harness/session/in-memory-storage-state.d.ts +39 -0
  158. package/dist/harness/session/in-memory-storage-state.d.ts.map +1 -0
  159. package/dist/harness/session/in-memory-storage-state.js +269 -0
  160. package/dist/harness/session/in-memory-storage-state.js.map +1 -0
  161. package/dist/harness/session/index.d.ts +9 -5
  162. package/dist/harness/session/index.d.ts.map +1 -1
  163. package/dist/harness/session/index.js +7 -4
  164. package/dist/harness/session/index.js.map +1 -1
  165. package/dist/harness/session/jsonl/codec.d.ts +19 -8
  166. package/dist/harness/session/jsonl/codec.d.ts.map +1 -1
  167. package/dist/harness/session/jsonl/codec.js +34 -219
  168. package/dist/harness/session/jsonl/codec.js.map +1 -1
  169. package/dist/harness/session/jsonl/fork.d.ts +38 -0
  170. package/dist/harness/session/jsonl/fork.d.ts.map +1 -0
  171. package/dist/harness/session/jsonl/fork.js +266 -0
  172. package/dist/harness/session/jsonl/fork.js.map +1 -0
  173. package/dist/harness/session/jsonl/index.d.ts +4 -0
  174. package/dist/harness/session/jsonl/index.d.ts.map +1 -0
  175. package/dist/harness/session/jsonl/index.js +4 -0
  176. package/dist/harness/session/jsonl/index.js.map +1 -0
  177. package/dist/harness/session/jsonl/io.d.ts +14 -0
  178. package/dist/harness/session/jsonl/io.d.ts.map +1 -0
  179. package/dist/harness/session/jsonl/io.js +86 -0
  180. package/dist/harness/session/jsonl/io.js.map +1 -0
  181. package/dist/harness/session/jsonl/legacy-v3.d.ts +44 -0
  182. package/dist/harness/session/jsonl/legacy-v3.d.ts.map +1 -0
  183. package/dist/harness/session/jsonl/legacy-v3.js +454 -0
  184. package/dist/harness/session/jsonl/legacy-v3.js.map +1 -0
  185. package/dist/harness/session/jsonl/repo.d.ts +27 -24
  186. package/dist/harness/session/jsonl/repo.d.ts.map +1 -1
  187. package/dist/harness/session/jsonl/repo.js +276 -151
  188. package/dist/harness/session/jsonl/repo.js.map +1 -1
  189. package/dist/harness/session/jsonl/storage.d.ts +39 -41
  190. package/dist/harness/session/jsonl/storage.d.ts.map +1 -1
  191. package/dist/harness/session/jsonl/storage.js +175 -196
  192. package/dist/harness/session/jsonl/storage.js.map +1 -1
  193. package/dist/harness/session/jsonl/types.d.ts +23 -22
  194. package/dist/harness/session/jsonl/types.d.ts.map +1 -1
  195. package/dist/harness/session/jsonl/types.js +2 -1
  196. package/dist/harness/session/jsonl/types.js.map +1 -1
  197. package/dist/harness/session/memory.d.ts +44 -40
  198. package/dist/harness/session/memory.d.ts.map +1 -1
  199. package/dist/harness/session/memory.js +333 -115
  200. package/dist/harness/session/memory.js.map +1 -1
  201. package/dist/harness/session/mutation-line.d.ts +8 -0
  202. package/dist/harness/session/mutation-line.d.ts.map +1 -0
  203. package/dist/harness/session/mutation-line.js +22 -0
  204. package/dist/harness/session/mutation-line.js.map +1 -0
  205. package/dist/harness/session/session.d.ts +78 -51
  206. package/dist/harness/session/session.d.ts.map +1 -1
  207. package/dist/harness/session/session.js +327 -207
  208. package/dist/harness/session/session.js.map +1 -1
  209. package/dist/harness/session/testing/benchmark/datasets.d.ts +12 -0
  210. package/dist/harness/session/testing/benchmark/datasets.d.ts.map +1 -0
  211. package/dist/harness/session/testing/benchmark/datasets.js +24 -0
  212. package/dist/harness/session/testing/benchmark/datasets.js.map +1 -0
  213. package/dist/harness/session/testing/benchmark/session-repo.d.ts +42 -0
  214. package/dist/harness/session/testing/benchmark/session-repo.d.ts.map +1 -0
  215. package/dist/harness/session/testing/benchmark/session-repo.js +127 -0
  216. package/dist/harness/session/testing/benchmark/session-repo.js.map +1 -0
  217. package/dist/harness/session/testing/benchmark/storage.d.ts +25 -0
  218. package/dist/harness/session/testing/benchmark/storage.d.ts.map +1 -0
  219. package/dist/harness/session/testing/benchmark/storage.js +114 -0
  220. package/dist/harness/session/testing/benchmark/storage.js.map +1 -0
  221. package/dist/harness/session/testing/conformance/session-repo.d.ts +23 -0
  222. package/dist/harness/session/testing/conformance/session-repo.d.ts.map +1 -0
  223. package/dist/harness/session/testing/conformance/session-repo.js +809 -0
  224. package/dist/harness/session/testing/conformance/session-repo.js.map +1 -0
  225. package/dist/harness/session/testing/conformance/storage.d.ts +4 -0
  226. package/dist/harness/session/testing/conformance/storage.d.ts.map +1 -0
  227. package/dist/harness/session/testing/conformance/storage.js +600 -0
  228. package/dist/harness/session/testing/conformance/storage.js.map +1 -0
  229. package/dist/harness/session/testing/gating-storage.d.ts +26 -0
  230. package/dist/harness/session/testing/gating-storage.d.ts.map +1 -0
  231. package/dist/harness/session/testing/gating-storage.js +103 -0
  232. package/dist/harness/session/testing/gating-storage.js.map +1 -0
  233. package/dist/harness/session/testing/index.d.ts +9 -2
  234. package/dist/harness/session/testing/index.d.ts.map +1 -1
  235. package/dist/harness/session/testing/index.js +8 -1
  236. package/dist/harness/session/testing/index.js.map +1 -1
  237. package/dist/harness/session/testing/instrumented-storage.d.ts +11 -0
  238. package/dist/harness/session/testing/instrumented-storage.d.ts.map +1 -0
  239. package/dist/harness/session/testing/instrumented-storage.js +19 -0
  240. package/dist/harness/session/testing/instrumented-storage.js.map +1 -0
  241. package/dist/harness/session/testing/storage-decorator.d.ts +20 -0
  242. package/dist/harness/session/testing/storage-decorator.d.ts.map +1 -0
  243. package/dist/harness/session/testing/storage-decorator.js +40 -0
  244. package/dist/harness/session/testing/storage-decorator.js.map +1 -0
  245. package/dist/harness/session/testing/types.d.ts +5 -7
  246. package/dist/harness/session/testing/types.d.ts.map +1 -1
  247. package/dist/harness/session/testing/types.js.map +1 -1
  248. package/dist/harness/session/types.d.ts +425 -265
  249. package/dist/harness/session/types.d.ts.map +1 -1
  250. package/dist/harness/session/types.js +7 -7
  251. package/dist/harness/session/types.js.map +1 -1
  252. package/dist/harness/session/values.d.ts +95 -0
  253. package/dist/harness/session/values.d.ts.map +1 -0
  254. package/dist/harness/session/values.js +81 -0
  255. package/dist/harness/session/values.js.map +1 -0
  256. package/dist/harness/skills.d.ts +3 -2
  257. package/dist/harness/skills.d.ts.map +1 -1
  258. package/dist/harness/skills.js +29 -26
  259. package/dist/harness/skills.js.map +1 -1
  260. package/dist/harness/telemetry.d.ts +64 -43
  261. package/dist/harness/telemetry.d.ts.map +1 -1
  262. package/dist/harness/telemetry.js +45 -26
  263. package/dist/harness/telemetry.js.map +1 -1
  264. package/dist/harness/tools/bash.d.ts +4 -4
  265. package/dist/harness/tools/bash.d.ts.map +1 -1
  266. package/dist/harness/tools/bash.js +66 -93
  267. package/dist/harness/tools/bash.js.map +1 -1
  268. package/dist/harness/tools/edit.d.ts.map +1 -1
  269. package/dist/harness/tools/edit.js +17 -13
  270. package/dist/harness/tools/edit.js.map +1 -1
  271. package/dist/harness/tools/file-mutation-queue.d.ts +2 -1
  272. package/dist/harness/tools/file-mutation-queue.d.ts.map +1 -1
  273. package/dist/harness/tools/file-mutation-queue.js +5 -5
  274. package/dist/harness/tools/file-mutation-queue.js.map +1 -1
  275. package/dist/harness/tools/path-utils.d.ts +3 -2
  276. package/dist/harness/tools/path-utils.d.ts.map +1 -1
  277. package/dist/harness/tools/path-utils.js +5 -5
  278. package/dist/harness/tools/path-utils.js.map +1 -1
  279. package/dist/harness/tools/read.d.ts +2 -1
  280. package/dist/harness/tools/read.d.ts.map +1 -1
  281. package/dist/harness/tools/read.js +4 -6
  282. package/dist/harness/tools/read.js.map +1 -1
  283. package/dist/harness/tools/write.d.ts.map +1 -1
  284. package/dist/harness/tools/write.js +12 -8
  285. package/dist/harness/tools/write.js.map +1 -1
  286. package/dist/harness/types.d.ts +128 -45
  287. package/dist/harness/types.d.ts.map +1 -1
  288. package/dist/harness/types.js.map +1 -1
  289. package/dist/harness/utils/adaptive-publisher.d.ts +24 -0
  290. package/dist/harness/utils/adaptive-publisher.d.ts.map +1 -0
  291. package/dist/harness/utils/adaptive-publisher.js +79 -0
  292. package/dist/harness/utils/adaptive-publisher.js.map +1 -0
  293. package/dist/harness/utils/output-capture.d.ts +31 -0
  294. package/dist/harness/utils/output-capture.d.ts.map +1 -0
  295. package/dist/harness/utils/output-capture.js +217 -0
  296. package/dist/harness/utils/output-capture.js.map +1 -0
  297. package/dist/harness/utils/shell-output.d.ts +19 -5
  298. package/dist/harness/utils/shell-output.d.ts.map +1 -1
  299. package/dist/harness/utils/shell-output.js +65 -192
  300. package/dist/harness/utils/shell-output.js.map +1 -1
  301. package/dist/harness/utils/truncate.d.ts +1 -0
  302. package/dist/harness/utils/truncate.d.ts.map +1 -1
  303. package/dist/harness/utils/truncate.js +1 -1
  304. package/dist/harness/utils/truncate.js.map +1 -1
  305. package/dist/harness/utils/usage.d.ts +4 -0
  306. package/dist/harness/utils/usage.d.ts.map +1 -0
  307. package/dist/harness/utils/usage.js +33 -0
  308. package/dist/harness/utils/usage.js.map +1 -0
  309. package/dist/index.d.ts +4 -1
  310. package/dist/index.d.ts.map +1 -1
  311. package/dist/index.js +3 -6
  312. package/dist/index.js.map +1 -1
  313. package/dist/search/index.d.ts +24 -16
  314. package/dist/search/index.d.ts.map +1 -1
  315. package/dist/search/index.js +1 -1
  316. package/dist/search/index.js.map +1 -1
  317. package/dist/types.d.ts +2 -0
  318. package/dist/types.d.ts.map +1 -1
  319. package/dist/types.js.map +1 -1
  320. package/package.json +27 -6
  321. package/dist/harness/reducer.d.ts +0 -100
  322. package/dist/harness/reducer.d.ts.map +0 -1
  323. package/dist/harness/reducer.js +0 -420
  324. package/dist/harness/reducer.js.map +0 -1
  325. package/dist/harness/session/jsonl/errors.d.ts +0 -9
  326. package/dist/harness/session/jsonl/errors.d.ts.map +0 -1
  327. package/dist/harness/session/jsonl/errors.js +0 -18
  328. package/dist/harness/session/jsonl/errors.js.map +0 -1
  329. package/dist/harness/session/jsonl.d.ts +0 -3
  330. package/dist/harness/session/jsonl.d.ts.map +0 -1
  331. package/dist/harness/session/jsonl.js +0 -2
  332. package/dist/harness/session/jsonl.js.map +0 -1
  333. package/dist/harness/session/state.d.ts +0 -65
  334. package/dist/harness/session/state.d.ts.map +0 -1
  335. package/dist/harness/session/state.js +0 -320
  336. package/dist/harness/session/state.js.map +0 -1
  337. package/dist/harness/session/testing/conformance.d.ts +0 -4
  338. package/dist/harness/session/testing/conformance.d.ts.map +0 -1
  339. package/dist/harness/session/testing/conformance.js +0 -762
  340. package/dist/harness/session/testing/conformance.js.map +0 -1
  341. package/dist/search/scanning.d.ts +0 -29
  342. package/dist/search/scanning.d.ts.map +0 -1
  343. package/dist/search/scanning.js +0 -103
  344. package/dist/search/scanning.js.map +0 -1
@@ -0,0 +1,809 @@
1
+ import { deepStrictEqual, rejects, strictEqual } from "node:assert/strict";
2
+ import { BACKGROUND_CONTEXT } from "../../../context.js";
3
+ import { insertEntry, insertUsage } from "../../commit.js";
4
+ import { appendList, branchTip, deleteList, entryLabel, laneConfig, laneState, list, operationMeta, operationPreparation, operationResult, operationState, operationToolArgs, pendingEntry, sessionName, setValue, value, } from "../../values.js";
5
+ const ROOT_ID = "00000000-0000-7000-8000-000000000001";
6
+ const CHILD_ID = "00000000-0000-7000-8000-000000000002";
7
+ const SIBLING_ID = "00000000-0000-7000-8000-000000000003";
8
+ const USAGE_ID = "00000000-0000-7000-8000-000000000004";
9
+ const OPERATION_ID = "00000000-0000-7000-8000-000000000005";
10
+ const PENDING_ID = "00000000-0000-7000-8000-000000000006";
11
+ const UNKNOWN_ID = "00000000-0000-7000-8000-000000000007";
12
+ const idleLaneState = {
13
+ currentOperationId: null,
14
+ lastOperationId: null,
15
+ inbox: [],
16
+ };
17
+ const applicationValue = value("test.application.value");
18
+ const applicationList = list("test.application.list");
19
+ const configuration = {
20
+ model: { provider: "provider", modelId: "model" },
21
+ thinkingLevel: "off",
22
+ activeToolNames: ["read"],
23
+ };
24
+ async function getBranchTip(session, name = "main") {
25
+ return (await session.branch(name, BACKGROUND_CONTEXT))?.getTipId(BACKGROUND_CONTEXT);
26
+ }
27
+ function assistantMessage(stopReason) {
28
+ return {
29
+ role: "assistant",
30
+ content: stopReason === "toolUse"
31
+ ? [{ type: "toolCall", id: "call", name: "read", arguments: {} }]
32
+ : [{ type: "text", text: stopReason }],
33
+ api: "anthropic-messages",
34
+ provider: "anthropic",
35
+ model: "claude-sonnet-4-5",
36
+ usage: {
37
+ input: 0,
38
+ output: 0,
39
+ cacheRead: 0,
40
+ cacheWrite: 0,
41
+ totalTokens: 0,
42
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
43
+ },
44
+ stopReason,
45
+ ...(stopReason === "deferred"
46
+ ? {
47
+ deferred: {
48
+ provider: "anthropic",
49
+ modelId: "claude-sonnet-4-5",
50
+ api: "anthropic-messages",
51
+ id: "job",
52
+ },
53
+ }
54
+ : {}),
55
+ timestamp: 1,
56
+ };
57
+ }
58
+ function usageRow() {
59
+ return {
60
+ id: USAGE_ID,
61
+ adjustment: true,
62
+ usage: {
63
+ input: 1,
64
+ output: 2,
65
+ cacheRead: 0,
66
+ cacheWrite: 0,
67
+ totalTokens: 3,
68
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
69
+ },
70
+ };
71
+ }
72
+ function prepareRepoCaseFactory(factory, onClose) {
73
+ return async () => ({
74
+ repo: await factory(),
75
+ ...(onClose === undefined ? {} : { close: onClose }),
76
+ });
77
+ }
78
+ function createCase(factory, group, name, test) {
79
+ return {
80
+ group,
81
+ name,
82
+ async run() {
83
+ const context = await factory();
84
+ try {
85
+ await test(context);
86
+ }
87
+ finally {
88
+ await context.close?.();
89
+ }
90
+ },
91
+ };
92
+ }
93
+ /** Creates lifecycle cases for repositories that support creation, discovery, open, and deletion. */
94
+ export function createSessionRepoLifecycleConformance(
95
+ // Require only the methods this group exercises so partial backends can run it independently.
96
+ backendFactory, onClose) {
97
+ const factory = prepareRepoCaseFactory(backendFactory, onClose);
98
+ return [
99
+ createCase(factory, "lifecycle", "creates a session with no implicit branch and rejects duplicate ids", async ({ repo }) => {
100
+ const session = await repo.create({ id: "session" }, BACKGROUND_CONTEXT);
101
+ strictEqual(session.metadata.id, "session");
102
+ strictEqual(Number.isSafeInteger(session.metadata.createdAt), true);
103
+ strictEqual(session.metadata.storageVersion, 1);
104
+ strictEqual(await session.branch("main", BACKGROUND_CONTEXT), undefined);
105
+ strictEqual(await session.getValue(laneState("main"), BACKGROUND_CONTEXT), undefined);
106
+ strictEqual(await session.getValue(laneConfig("main"), BACKGROUND_CONTEXT), undefined);
107
+ await rejects(repo.create({ id: "session" }, BACKGROUND_CONTEXT));
108
+ await session.close(BACKGROUND_CONTEXT);
109
+ }),
110
+ createCase(factory, "lifecycle", "close drains an acquired scope and rejects a queued mutation callback", async ({ repo }) => {
111
+ const session = await repo.create({ id: "session" }, BACKGROUND_CONTEXT);
112
+ const active = await session.beginMutation(BACKGROUND_CONTEXT);
113
+ let queuedStarted = false;
114
+ const queued = session.mutate(() => {
115
+ queuedStarted = true;
116
+ }, BACKGROUND_CONTEXT);
117
+ void queued.catch(() => { });
118
+ const closing = session.close(BACKGROUND_CONTEXT);
119
+ await active.end(BACKGROUND_CONTEXT);
120
+ await rejects(queued);
121
+ await closing;
122
+ strictEqual(queuedStarted, false);
123
+ }),
124
+ createCase(factory, "lifecycle", "lists metadata and preserves state across close and reopen", async ({ repo }) => {
125
+ const first = await repo.create({ id: "first" }, BACKGROUND_CONTEXT);
126
+ await first.setName("preserved", BACKGROUND_CONTEXT);
127
+ const second = await repo.create({ id: "second", parentSessionId: "parent" }, BACKGROUND_CONTEXT);
128
+ await second.close(BACKGROUND_CONTEXT);
129
+ const listed = await repo.list(undefined, BACKGROUND_CONTEXT);
130
+ deepStrictEqual(listed
131
+ .map(({ id, parentSessionId }) => ({ id, parentSessionId }))
132
+ .sort((left, right) => left.id.localeCompare(right.id)), [
133
+ { id: "first", parentSessionId: undefined },
134
+ { id: "second", parentSessionId: "parent" },
135
+ ]);
136
+ await first.close(BACKGROUND_CONTEXT);
137
+ await rejects(first.getName(BACKGROUND_CONTEXT));
138
+ const reopened = await repo.open(first.metadata, BACKGROUND_CONTEXT);
139
+ strictEqual(reopened === first, false);
140
+ strictEqual(await reopened.getName(BACKGROUND_CONTEXT), "preserved");
141
+ await reopened.close(BACKGROUND_CONTEXT);
142
+ }),
143
+ createCase(factory, "lifecycle", "deletes closed sessions without affecting other sessions", async ({ repo }) => {
144
+ const removed = await repo.create({ id: "removed" }, BACKGROUND_CONTEXT);
145
+ const retained = await repo.create({ id: "retained" }, BACKGROUND_CONTEXT);
146
+ await Promise.all([removed.close(BACKGROUND_CONTEXT), retained.close(BACKGROUND_CONTEXT)]);
147
+ await repo.delete(removed.metadata, BACKGROUND_CONTEXT);
148
+ deepStrictEqual((await repo.list(undefined, BACKGROUND_CONTEXT)).map(({ id }) => id), ["retained"]);
149
+ await rejects(repo.open(removed.metadata, BACKGROUND_CONTEXT));
150
+ await rejects(repo.delete(removed.metadata, BACKGROUND_CONTEXT));
151
+ }),
152
+ ];
153
+ }
154
+ /** Creates exclusive-open cases for repositories that own active session handles. */
155
+ export function createSessionRepoOwnershipConformance(backendFactory, onClose) {
156
+ const factory = prepareRepoCaseFactory(backendFactory, onClose);
157
+ return [
158
+ createCase(factory, "ownership", "rejects opening an already-open session", async ({ repo }) => {
159
+ const session = await repo.create({ id: "session" }, BACKGROUND_CONTEXT);
160
+ await rejects(repo.open(session.metadata, BACKGROUND_CONTEXT));
161
+ await session.close(BACKGROUND_CONTEXT);
162
+ const reopened = await repo.open(session.metadata, BACKGROUND_CONTEXT);
163
+ await rejects(repo.open(session.metadata, BACKGROUND_CONTEXT));
164
+ await reopened.close(BACKGROUND_CONTEXT);
165
+ }),
166
+ ];
167
+ }
168
+ /** Creates message cases for repositories that support session creation. */
169
+ export function createSessionRepoMessageConformance(backendFactory, onClose) {
170
+ const factory = prepareRepoCaseFactory(backendFactory, onClose);
171
+ return [
172
+ createCase(factory, "messages", "rejects pending assistant messages without changing the tree", async ({ repo }) => {
173
+ const session = await repo.create({ id: "session" }, BACKGROUND_CONTEXT);
174
+ const branch = await session.createBranch("main", null, BACKGROUND_CONTEXT);
175
+ await rejects(branch.appendMessage(assistantMessage("pending"), BACKGROUND_CONTEXT));
176
+ strictEqual(await getBranchTip(session), null);
177
+ deepStrictEqual(await session.findEntries(undefined, BACKGROUND_CONTEXT), []);
178
+ await session.close(BACKGROUND_CONTEXT);
179
+ }),
180
+ createCase(factory, "messages", "preserves every settled assistant stop reason", async ({ repo }) => {
181
+ const session = await repo.create({ id: "session" }, BACKGROUND_CONTEXT);
182
+ const messagesByStopReason = {
183
+ stop: assistantMessage("stop"),
184
+ length: assistantMessage("length"),
185
+ toolUse: assistantMessage("toolUse"),
186
+ error: assistantMessage("error"),
187
+ aborted: assistantMessage("aborted"),
188
+ deferred: assistantMessage("deferred"),
189
+ };
190
+ const branch = await session.createBranch("main", null, BACKGROUND_CONTEXT);
191
+ const messages = Object.values(messagesByStopReason);
192
+ const ids = [];
193
+ for (const message of messages)
194
+ ids.push(await branch.appendMessage(message, BACKGROUND_CONTEXT));
195
+ const entries = await session.findEntries({ order: "asc", type: "message" }, BACKGROUND_CONTEXT);
196
+ deepStrictEqual(entries.map((entry) => entry.id), ids);
197
+ for (const [index, entry] of entries.entries()) {
198
+ if (entry.type !== "message")
199
+ throw new Error("Expected message entry");
200
+ deepStrictEqual(entry.message, messages[index]);
201
+ }
202
+ strictEqual(await getBranchTip(session), ids.at(-1));
203
+ await session.close(BACKGROUND_CONTEXT);
204
+ }),
205
+ ];
206
+ }
207
+ /** Creates fork-content cases that do not require concurrent repository coordination. */
208
+ export function createSessionRepoForkBehaviorConformance(backendFactory, onClose) {
209
+ const factory = prepareRepoCaseFactory(backendFactory, onClose);
210
+ return [
211
+ createCase(factory, "forks", "tree-forks a fresh session before first attachment", async ({ repo }) => {
212
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
213
+ const fork = await repo.fork(source.metadata, { id: "fork", scope: "tree" }, BACKGROUND_CONTEXT);
214
+ strictEqual(fork.metadata.id, "fork");
215
+ strictEqual(fork.metadata.parentSessionId, "source");
216
+ strictEqual(await fork.branch("main", BACKGROUND_CONTEXT), undefined);
217
+ strictEqual(await fork.getValue(laneConfig("main"), BACKGROUND_CONTEXT), undefined);
218
+ strictEqual(await fork.getValue(laneState("main"), BACKGROUND_CONTEXT), undefined);
219
+ deepStrictEqual(await fork.findEntries(undefined, BACKGROUND_CONTEXT), []);
220
+ deepStrictEqual(await fork.getStats(BACKGROUND_CONTEXT), {
221
+ messageCount: 0,
222
+ usage: {
223
+ input: 0,
224
+ output: 0,
225
+ cacheRead: 0,
226
+ cacheWrite: 0,
227
+ totalTokens: 0,
228
+ cost: {
229
+ input: 0,
230
+ output: 0,
231
+ cacheRead: 0,
232
+ cacheWrite: 0,
233
+ total: 0,
234
+ },
235
+ },
236
+ });
237
+ await Promise.all([source.close(BACKGROUND_CONTEXT), fork.close(BACKGROUND_CONTEXT)]);
238
+ }),
239
+ createCase(factory, "forks", "rejects a data-only branch and releases its destination id", async ({ repo }) => {
240
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
241
+ await source.createBranch("data", null, BACKGROUND_CONTEXT);
242
+ await rejects(repo.fork(source.metadata, { id: "destination", scope: "branch", branch: "data" }, BACKGROUND_CONTEXT));
243
+ deepStrictEqual((await repo.list(undefined, BACKGROUND_CONTEXT)).map(({ id }) => id), ["source"]);
244
+ const destination = await repo.create({ id: "destination" }, BACKGROUND_CONTEXT);
245
+ await Promise.all([source.close(BACKGROUND_CONTEXT), destination.close(BACKGROUND_CONTEXT)]);
246
+ }),
247
+ createCase(factory, "forks", "forks one named configured branch with scoped values and a zero ledger", async ({ repo }) => {
248
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
249
+ await source.mutate((mutator) => mutator.commit([
250
+ insertEntry({
251
+ id: ROOT_ID,
252
+ parentId: null,
253
+ type: "custom",
254
+ customType: "root",
255
+ }),
256
+ insertEntry({
257
+ id: CHILD_ID,
258
+ parentId: ROOT_ID,
259
+ type: "message",
260
+ message: { role: "user", content: "child", timestamp: 1 },
261
+ }),
262
+ insertEntry({
263
+ id: SIBLING_ID,
264
+ parentId: ROOT_ID,
265
+ type: "custom",
266
+ customType: "sibling",
267
+ }),
268
+ setValue(branchTip("main"), SIBLING_ID),
269
+ setValue(branchTip("review"), CHILD_ID),
270
+ setValue(laneConfig("review"), configuration),
271
+ setValue(laneState("review"), {
272
+ currentOperationId: OPERATION_ID,
273
+ lastOperationId: "previous",
274
+ inbox: [{ entryId: PENDING_ID, kind: "write" }],
275
+ }),
276
+ setValue(operationResult("previous"), {
277
+ operationId: "previous",
278
+ kind: "navigation",
279
+ status: "completed",
280
+ fromTipId: ROOT_ID,
281
+ tipId: CHILD_ID,
282
+ startedAt: 1,
283
+ endedAt: 2,
284
+ }),
285
+ setValue(sessionName, "source name"),
286
+ setValue(applicationValue, { copied: false }),
287
+ appendList(applicationList, { copied: false }),
288
+ setValue(entryLabel(ROOT_ID), "root label"),
289
+ setValue(entryLabel(SIBLING_ID), "sibling label"),
290
+ setValue(pendingEntry(PENDING_ID), {
291
+ type: "custom",
292
+ customType: "pending",
293
+ }),
294
+ setValue(operationMeta(OPERATION_ID), {
295
+ operationId: OPERATION_ID,
296
+ lane: "review",
297
+ sourceTipId: CHILD_ID,
298
+ startedAt: 1,
299
+ intent: { kind: "compaction" },
300
+ }),
301
+ setValue(operationState(OPERATION_ID), {
302
+ at: "summary.deciding",
303
+ control: { status: "running" },
304
+ settings: {
305
+ compaction: { enabled: true, reserveTokens: 1, keepRecentTokens: 1 },
306
+ steeringMode: "all",
307
+ followUpMode: "all",
308
+ toolExecution: "sequential",
309
+ },
310
+ latestAssistantEntryId: null,
311
+ task: { taskId: OPERATION_ID, reason: "manual", boundary: { kind: "finish" } },
312
+ }),
313
+ setValue(operationToolArgs(OPERATION_ID, ROOT_ID, 0), {
314
+ argument: true,
315
+ }),
316
+ setValue(operationPreparation(OPERATION_ID, OPERATION_ID), {
317
+ kind: "compaction",
318
+ messagesToSummarize: [],
319
+ turnPrefixMessages: [],
320
+ retainedTail: [],
321
+ isSplitTurn: false,
322
+ tokensBefore: 0,
323
+ fileOps: { read: [], written: [], edited: [] },
324
+ settings: {
325
+ enabled: true,
326
+ reserveTokens: 1,
327
+ keepRecentTokens: 1,
328
+ },
329
+ }),
330
+ insertUsage(usageRow()),
331
+ ], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
332
+ const fork = await repo.fork(source.metadata, { id: "fork", scope: "branch", branch: "review", entryId: CHILD_ID, position: "at" }, BACKGROUND_CONTEXT);
333
+ deepStrictEqual((await fork.findEntries({ order: "asc" }, BACKGROUND_CONTEXT)).map(({ id }) => id), [ROOT_ID, CHILD_ID]);
334
+ strictEqual(await fork.branch("main", BACKGROUND_CONTEXT), undefined);
335
+ strictEqual(await getBranchTip(fork, "review"), CHILD_ID);
336
+ deepStrictEqual((await fork.getValue(laneConfig("review"), BACKGROUND_CONTEXT))?.value, configuration);
337
+ deepStrictEqual((await fork.getValue(laneState("review"), BACKGROUND_CONTEXT))?.value, idleLaneState);
338
+ strictEqual(await fork.getValue(laneConfig("main"), BACKGROUND_CONTEXT), undefined);
339
+ strictEqual(await fork.getValue(laneState("main"), BACKGROUND_CONTEXT), undefined);
340
+ strictEqual(await fork.getName(BACKGROUND_CONTEXT), "source name");
341
+ strictEqual(await fork.getValue(applicationValue, BACKGROUND_CONTEXT), undefined);
342
+ deepStrictEqual(await fork.readList(applicationList, undefined, BACKGROUND_CONTEXT), []);
343
+ strictEqual(await fork.getLabel(ROOT_ID, BACKGROUND_CONTEXT), "root label");
344
+ strictEqual(await fork.getLabel(SIBLING_ID, BACKGROUND_CONTEXT), undefined);
345
+ strictEqual(await fork.getValue(operationResult("previous"), BACKGROUND_CONTEXT), undefined);
346
+ strictEqual(await fork.getValue(pendingEntry(PENDING_ID), BACKGROUND_CONTEXT), undefined);
347
+ strictEqual(await fork.getValue(operationMeta(OPERATION_ID), BACKGROUND_CONTEXT), undefined);
348
+ strictEqual(await fork.getValue(operationState(OPERATION_ID), BACKGROUND_CONTEXT), undefined);
349
+ strictEqual(await fork.getValue(operationToolArgs(OPERATION_ID, ROOT_ID, 0), BACKGROUND_CONTEXT), undefined);
350
+ strictEqual(await fork.getValue(operationPreparation(OPERATION_ID, OPERATION_ID), BACKGROUND_CONTEXT), undefined);
351
+ const stats = await fork.getStats(BACKGROUND_CONTEXT);
352
+ strictEqual(stats.messageCount, 1);
353
+ deepStrictEqual(stats.usage, {
354
+ input: 0,
355
+ output: 0,
356
+ cacheRead: 0,
357
+ cacheWrite: 0,
358
+ totalTokens: 0,
359
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
360
+ });
361
+ await Promise.all([source.close(BACKGROUND_CONTEXT), fork.close(BACKGROUND_CONTEXT)]);
362
+ }),
363
+ createCase(factory, "forks", "enforces branch ancestry for at and before placement", async ({ repo }) => {
364
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
365
+ await source.mutate((mutator) => mutator.commit([
366
+ insertEntry({ id: ROOT_ID, parentId: null, type: "custom", customType: "root" }),
367
+ insertEntry({ id: CHILD_ID, parentId: ROOT_ID, type: "custom", customType: "child" }),
368
+ insertEntry({ id: SIBLING_ID, parentId: ROOT_ID, type: "custom", customType: "sibling" }),
369
+ setValue(branchTip("main"), CHILD_ID),
370
+ setValue(laneConfig("main"), configuration),
371
+ setValue(laneState("main"), idleLaneState),
372
+ setValue(branchTip("empty"), null),
373
+ setValue(laneConfig("empty"), configuration),
374
+ setValue(laneState("empty"), idleLaneState),
375
+ ], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
376
+ const before = await repo.fork(source.metadata, { id: "before", scope: "branch", branch: "main", entryId: CHILD_ID, position: "before" }, BACKGROUND_CONTEXT);
377
+ strictEqual(await getBranchTip(before), ROOT_ID);
378
+ deepStrictEqual((await before.findEntries({ order: "asc" }, BACKGROUND_CONTEXT)).map(({ id }) => id), [ROOT_ID]);
379
+ const mid = await repo.fork(source.metadata, { id: "mid", scope: "branch", branch: "main", entryId: ROOT_ID, position: "at" }, BACKGROUND_CONTEXT);
380
+ strictEqual(await getBranchTip(mid), ROOT_ID);
381
+ deepStrictEqual((await mid.findEntries({ order: "asc" }, BACKGROUND_CONTEXT)).map(({ id }) => id), [ROOT_ID]);
382
+ const beforeRoot = await repo.fork(source.metadata, { id: "before-root", scope: "branch", branch: "main", entryId: ROOT_ID, position: "before" }, BACKGROUND_CONTEXT);
383
+ strictEqual(await getBranchTip(beforeRoot), null);
384
+ deepStrictEqual(await beforeRoot.findEntries({ order: "asc" }, BACKGROUND_CONTEXT), []);
385
+ const empty = await repo.fork(source.metadata, { id: "empty", scope: "branch", branch: "empty" }, BACKGROUND_CONTEXT);
386
+ strictEqual(await getBranchTip(empty, "empty"), null);
387
+ deepStrictEqual(await empty.findEntries({ order: "asc" }, BACKGROUND_CONTEXT), []);
388
+ for (const [id, branch, entryId] of [
389
+ ["off-branch", "main", SIBLING_ID],
390
+ ["unknown", "main", UNKNOWN_ID],
391
+ ["null-tip", "empty", ROOT_ID],
392
+ ]) {
393
+ await rejects(repo.fork(source.metadata, { id, scope: "branch", branch, entryId }, BACKGROUND_CONTEXT));
394
+ }
395
+ deepStrictEqual((await repo.list(undefined, BACKGROUND_CONTEXT)).map(({ id }) => id).sort(), [
396
+ "before",
397
+ "before-root",
398
+ "empty",
399
+ "mid",
400
+ "source",
401
+ ]);
402
+ await Promise.all([
403
+ source.close(BACKGROUND_CONTEXT),
404
+ before.close(BACKGROUND_CONTEXT),
405
+ beforeRoot.close(BACKGROUND_CONTEXT),
406
+ empty.close(BACKGROUND_CONTEXT),
407
+ mid.close(BACKGROUND_CONTEXT),
408
+ ]);
409
+ }),
410
+ createCase(factory, "forks", "forks a closed source session", async ({ repo }) => {
411
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
412
+ await source.mutate((mutator) => mutator.commit([
413
+ insertEntry({
414
+ id: ROOT_ID,
415
+ parentId: null,
416
+ type: "custom",
417
+ customType: "root",
418
+ }),
419
+ setValue(branchTip("main"), ROOT_ID),
420
+ setValue(laneConfig("main"), configuration),
421
+ setValue(laneState("main"), idleLaneState),
422
+ setValue(applicationValue, "excluded"),
423
+ appendList(applicationList, "excluded"),
424
+ ], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
425
+ await source.close(BACKGROUND_CONTEXT);
426
+ const fork = await repo.fork(source.metadata, { id: "fork", scope: "branch", branch: "main" }, BACKGROUND_CONTEXT);
427
+ strictEqual(await getBranchTip(fork), ROOT_ID);
428
+ deepStrictEqual((await fork.getValue(laneConfig("main"), BACKGROUND_CONTEXT))?.value, configuration);
429
+ deepStrictEqual((await fork.getValue(laneState("main"), BACKGROUND_CONTEXT))?.value, idleLaneState);
430
+ strictEqual(await fork.getValue(applicationValue, BACKGROUND_CONTEXT), undefined);
431
+ deepStrictEqual(await fork.readList(applicationList, undefined, BACKGROUND_CONTEXT), []);
432
+ await fork.close(BACKGROUND_CONTEXT);
433
+ }),
434
+ createCase(factory, "forks", "forks the whole configured tree with fresh lane state", async ({ repo }) => {
435
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
436
+ await source.mutate((mutator) => mutator.commit([
437
+ insertEntry({
438
+ id: ROOT_ID,
439
+ parentId: null,
440
+ type: "custom",
441
+ customType: "root",
442
+ }),
443
+ insertEntry({
444
+ id: CHILD_ID,
445
+ parentId: ROOT_ID,
446
+ type: "custom",
447
+ customType: "child",
448
+ }),
449
+ insertEntry({
450
+ id: SIBLING_ID,
451
+ parentId: ROOT_ID,
452
+ type: "custom",
453
+ customType: "sibling",
454
+ }),
455
+ setValue(branchTip("main"), CHILD_ID),
456
+ setValue(laneConfig("main"), configuration),
457
+ setValue(laneState("main"), idleLaneState),
458
+ setValue(branchTip("review"), SIBLING_ID),
459
+ setValue(laneConfig("review"), configuration),
460
+ setValue(laneState("review"), idleLaneState),
461
+ setValue(branchTip("notes"), ROOT_ID),
462
+ setValue(applicationValue, { copied: true }),
463
+ ], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
464
+ const fork = await repo.fork(source.metadata, { id: "fork", scope: "tree" }, BACKGROUND_CONTEXT);
465
+ deepStrictEqual((await fork.findEntries({ order: "asc" }, BACKGROUND_CONTEXT)).map(({ id }) => id), [ROOT_ID, CHILD_ID, SIBLING_ID]);
466
+ strictEqual(await getBranchTip(fork), CHILD_ID);
467
+ strictEqual(await getBranchTip(fork, "review"), SIBLING_ID);
468
+ strictEqual(await getBranchTip(fork, "notes"), ROOT_ID);
469
+ strictEqual(await fork.getValue(laneConfig("notes"), BACKGROUND_CONTEXT), undefined);
470
+ strictEqual(await fork.getValue(laneState("notes"), BACKGROUND_CONTEXT), undefined);
471
+ deepStrictEqual((await fork.getValue(laneConfig("review"), BACKGROUND_CONTEXT))?.value, configuration);
472
+ deepStrictEqual((await fork.getValue(laneState("review"), BACKGROUND_CONTEXT))?.value, idleLaneState);
473
+ deepStrictEqual((await fork.getValue(applicationValue, BACKGROUND_CONTEXT))?.value, { copied: true });
474
+ await Promise.all([source.close(BACKGROUND_CONTEXT), fork.close(BACKGROUND_CONTEXT)]);
475
+ }),
476
+ createCase(factory, "forks", "rejects only surviving unknown reserved scalar state", async ({ repo }) => {
477
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
478
+ await source.createBranch("main", null, BACKGROUND_CONTEXT);
479
+ await source.mutate((mutator) => mutator.commit([setValue(laneConfig("main"), configuration), setValue(laneState("main"), idleLaneState)], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
480
+ for (const namespace of ["pi", "pi.unknown"]) {
481
+ const address = value(namespace);
482
+ await source.setValue(address, true, BACKGROUND_CONTEXT);
483
+ await rejects(repo.fork(source.metadata, { id: "tree", scope: "tree" }, BACKGROUND_CONTEXT));
484
+ await rejects(repo.fork(source.metadata, { id: "branch", scope: "branch", branch: "main" }, BACKGROUND_CONTEXT));
485
+ await source.deleteValue(address, BACKGROUND_CONTEXT);
486
+ }
487
+ await source.close(BACKGROUND_CONTEXT);
488
+ const tree = await repo.fork(source.metadata, { id: "tree", scope: "tree" }, BACKGROUND_CONTEXT);
489
+ const branch = await repo.fork(source.metadata, { id: "branch", scope: "branch", branch: "main" }, BACKGROUND_CONTEXT);
490
+ await Promise.all([tree.close(BACKGROUND_CONTEXT), branch.close(BACKGROUND_CONTEXT)]);
491
+ }),
492
+ ];
493
+ }
494
+ /** WP08 cases enabled for Memory and JSONL while SQLite's streaming fork implementation is pending. */
495
+ export function createSessionRepoStreamingForkConformance(backendFactory, onClose) {
496
+ // Merge into createSessionRepoForkConformance once SQLite supports these cases.
497
+ return [
498
+ ...createSessionRepoForkApplicationListConformance(backendFactory, onClose),
499
+ ...createSessionRepoBranchForkApplicationStateConformance(backendFactory, onClose),
500
+ ...createSessionRepoForkLaneValidationConformance(backendFactory, onClose),
501
+ ];
502
+ }
503
+ function createSessionRepoForkLaneValidationConformance(backendFactory, onClose) {
504
+ const factory = prepareRepoCaseFactory(backendFactory, onClose);
505
+ return [
506
+ createCase(factory, "fork lane validation", "ignores malformed unrelated lanes", async ({ repo }) => {
507
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
508
+ let tree;
509
+ let branch;
510
+ try {
511
+ await source.createBranch("main", null, BACKGROUND_CONTEXT);
512
+ await source.mutate((mutator) => mutator.commit([
513
+ setValue(laneConfig("main"), configuration),
514
+ setValue(laneState("main"), idleLaneState),
515
+ setValue(laneConfig("unrelated"), configuration),
516
+ ], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
517
+ tree = await repo.fork(source.metadata, { id: "tree", scope: "tree" }, BACKGROUND_CONTEXT);
518
+ branch = await repo.fork(source.metadata, { id: "branch", scope: "branch", branch: "main" }, BACKGROUND_CONTEXT);
519
+ strictEqual(await getBranchTip(branch), null);
520
+ }
521
+ finally {
522
+ await Promise.all([
523
+ source.close(BACKGROUND_CONTEXT),
524
+ tree?.close(BACKGROUND_CONTEXT),
525
+ branch?.close(BACKGROUND_CONTEXT),
526
+ ]);
527
+ }
528
+ }),
529
+ ];
530
+ }
531
+ /** Creates application-list fork cases. */
532
+ function createSessionRepoForkApplicationListConformance(backendFactory, onClose) {
533
+ const factory = prepareRepoCaseFactory(backendFactory, onClose);
534
+ return ["open", "closed"].flatMap((sourceState) => [
535
+ createCase(factory, `fork application lists (${sourceState} source)`, "tree fork copies lists at distinct addresses", async ({ repo }) => {
536
+ // Each list keeps its own contents: keys "" and "other" in one namespace must not merge.
537
+ // A different namespace also stays separate; pi2.events is application-owned, not reserved.
538
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
539
+ let fork;
540
+ try {
541
+ const events = list("test.application.events");
542
+ const sibling = list(events.namespace, "other");
543
+ const otherNamespace = list("pi2.events");
544
+ const absent = list(events.namespace, "absent");
545
+ await source.appendList(events, "event", BACKGROUND_CONTEXT);
546
+ await source.appendList(sibling, "sibling", BACKGROUND_CONTEXT);
547
+ await source.appendList(otherNamespace, "other namespace", BACKGROUND_CONTEXT);
548
+ if (sourceState === "closed")
549
+ await source.close(BACKGROUND_CONTEXT);
550
+ fork = await repo.fork(source.metadata, { id: "fork", scope: "tree" }, BACKGROUND_CONTEXT);
551
+ deepStrictEqual((await fork.readList(events, undefined, BACKGROUND_CONTEXT)).map(({ value }) => value), ["event"]);
552
+ deepStrictEqual((await fork.readList(sibling, undefined, BACKGROUND_CONTEXT)).map(({ value }) => value), ["sibling"]);
553
+ deepStrictEqual((await fork.readList(otherNamespace, undefined, BACKGROUND_CONTEXT)).map(({ value }) => value), ["other namespace"]);
554
+ deepStrictEqual(await fork.readList(absent, undefined, BACKGROUND_CONTEXT), []);
555
+ }
556
+ finally {
557
+ await Promise.all([source.close(BACKGROUND_CONTEXT), fork?.close(BACKGROUND_CONTEXT)]);
558
+ }
559
+ }),
560
+ createCase(factory, `fork application lists (${sourceState} source)`, "tree fork copies only survivors after list deletion and reappend", async ({ repo }) => {
561
+ // Append old -> delete -> append new must copy only new, even when changes share a transaction.
562
+ // A list deleted without a later append must remain empty in the fork.
563
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
564
+ let fork;
565
+ try {
566
+ const events = list("test.application.events");
567
+ const deleted = list(events.namespace, "deleted");
568
+ await source.appendList(events, "old", BACKGROUND_CONTEXT);
569
+ await source.appendList(deleted, "removed", BACKGROUND_CONTEXT);
570
+ await source.mutate((mutator) => mutator.commit([
571
+ deleteList(events),
572
+ appendList(events, "temporary"),
573
+ deleteList(events),
574
+ appendList(events, "first survivor"),
575
+ deleteList(deleted),
576
+ ], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
577
+ await source.appendList(events, "second survivor", BACKGROUND_CONTEXT);
578
+ if (sourceState === "closed")
579
+ await source.close(BACKGROUND_CONTEXT);
580
+ fork = await repo.fork(source.metadata, { id: "fork", scope: "tree" }, BACKGROUND_CONTEXT);
581
+ deepStrictEqual((await fork.readList(events, undefined, BACKGROUND_CONTEXT)).map(({ value }) => value), ["first survivor", "second survivor"]);
582
+ deepStrictEqual(await fork.readList(deleted, undefined, BACKGROUND_CONTEXT), []);
583
+ }
584
+ finally {
585
+ await Promise.all([source.close(BACKGROUND_CONTEXT), fork?.close(BACKGROUND_CONTEXT)]);
586
+ }
587
+ }),
588
+ createCase(factory, `fork application lists (${sourceState} source)`, "tree fork preserves list element sequences including gaps", async ({ repo }) => {
589
+ // Other writes consume sequences too. List elements at seq 2 and 4 must stay at 2 and 4
590
+ // in the fork, not be renumbered to 1 and 2.
591
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
592
+ let fork;
593
+ try {
594
+ const events = list("test.application.events");
595
+ const committed = await source.mutate((mutator) => mutator.commit([
596
+ setValue(sessionName, "before"),
597
+ appendList(events, "first"),
598
+ setValue(sessionName, "between"),
599
+ appendList(events, "second"),
600
+ ], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
601
+ if (sourceState === "closed")
602
+ await source.close(BACKGROUND_CONTEXT);
603
+ fork = await repo.fork(source.metadata, { id: "fork", scope: "tree" }, BACKGROUND_CONTEXT);
604
+ deepStrictEqual(await fork.readList(events, undefined, BACKGROUND_CONTEXT), [
605
+ { seq: committed.seqs[1], value: "first" },
606
+ { seq: committed.seqs[3], value: "second" },
607
+ ]);
608
+ }
609
+ finally {
610
+ await Promise.all([source.close(BACKGROUND_CONTEXT), fork?.close(BACKGROUND_CONTEXT)]);
611
+ }
612
+ }),
613
+ ...["asc", "desc"].map((order) => createCase(factory, `fork application lists (${sourceState} source)`, `tree fork continues ${order} pagination using source cursors`, async ({ repo }) => {
614
+ // A cursor from the source must resume at the same place in the fork. After reading
615
+ // [first, second] ascending, continue with [third]; descending does the reverse.
616
+ // Continuing after the final element must return an empty page, not repeat that element.
617
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
618
+ let fork;
619
+ try {
620
+ const events = list("test.application.events");
621
+ for (const item of ["first", "second", "third"]) {
622
+ await source.appendList(events, item, BACKGROUND_CONTEXT);
623
+ }
624
+ const firstPage = await source.readList(events, { order, limit: 2 }, BACKGROUND_CONTEXT);
625
+ strictEqual(firstPage.length, 2);
626
+ const cursor = { seq: firstPage[1].seq };
627
+ const lastPage = await source.readList(events, { order, cursor, limit: 2 }, BACKGROUND_CONTEXT);
628
+ deepStrictEqual(lastPage.map(({ value }) => value), [order === "asc" ? "third" : "first"]);
629
+ const endCursor = { seq: lastPage[0].seq };
630
+ if (sourceState === "closed")
631
+ await source.close(BACKGROUND_CONTEXT);
632
+ fork = await repo.fork(source.metadata, { id: "fork", scope: "tree" }, BACKGROUND_CONTEXT);
633
+ deepStrictEqual(await fork.readList(events, { order, limit: 2 }, BACKGROUND_CONTEXT), firstPage);
634
+ deepStrictEqual(await fork.readList(events, { order, cursor, limit: 2 }, BACKGROUND_CONTEXT), lastPage);
635
+ deepStrictEqual(await fork.readList(events, { order, cursor: endCursor, limit: 2 }, BACKGROUND_CONTEXT), []);
636
+ }
637
+ finally {
638
+ await Promise.all([source.close(BACKGROUND_CONTEXT), fork?.close(BACKGROUND_CONTEXT)]);
639
+ }
640
+ })),
641
+ ]);
642
+ }
643
+ /** Creates branch application-state cases. */
644
+ function createSessionRepoBranchForkApplicationStateConformance(backendFactory, onClose) {
645
+ const factory = prepareRepoCaseFactory(backendFactory, onClose);
646
+ return ["open", "closed"].flatMap((sourceState) => [
647
+ createCase(factory, `branch fork application state (${sourceState} source)`, "excludes overwritten and unchanged application values", async ({ repo }) => {
648
+ // WP08 §1.1: set v1 -> fork point -> overwrite with v2. A branch fork copies neither
649
+ // version: it cannot reconstruct v1 and must not copy v2. Even unchanged older values
650
+ // are excluded, so filtering current values by the fork point's sequence is not enough.
651
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
652
+ let fork;
653
+ try {
654
+ const state = value("test.application.state");
655
+ const unchanged = value("test.application.settings");
656
+ const branch = await source.createBranch("review", null, BACKGROUND_CONTEXT);
657
+ await source.mutate((mutator) => mutator.commit([
658
+ setValue(laneConfig("review"), configuration),
659
+ setValue(laneState("review"), idleLaneState),
660
+ setValue(state, "v1"),
661
+ setValue(unchanged, "predates fork point"),
662
+ ], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
663
+ const entryId = await branch.appendCustomEntry("fork-point", undefined, BACKGROUND_CONTEXT);
664
+ await source.setValue(state, "v2", BACKGROUND_CONTEXT);
665
+ strictEqual((await source.getValue(state, BACKGROUND_CONTEXT))?.value, "v2");
666
+ if (sourceState === "closed")
667
+ await source.close(BACKGROUND_CONTEXT);
668
+ fork = await repo.fork(source.metadata, { scope: "branch", branch: "review", entryId }, BACKGROUND_CONTEXT);
669
+ strictEqual(await getBranchTip(fork, "review"), entryId);
670
+ strictEqual(await fork.getValue(state, BACKGROUND_CONTEXT), undefined);
671
+ // A survivor older than the fork point must also be excluded, not copied by a seq cutoff.
672
+ strictEqual(await fork.getValue(unchanged, BACKGROUND_CONTEXT), undefined);
673
+ }
674
+ finally {
675
+ await Promise.all([source.close(BACKGROUND_CONTEXT), fork?.close(BACKGROUND_CONTEXT)]);
676
+ }
677
+ }),
678
+ createCase(factory, `branch fork application state (${sourceState} source)`, "excludes deleted/reappended and untouched application lists", async ({ repo }) => {
679
+ // WP08 §1.1: append old -> fork point -> delete -> append new. A branch fork copies
680
+ // neither the deleted elements nor the new ones. A separate untouched list is also
681
+ // excluded, even though its elements still survive from before the fork point.
682
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
683
+ let fork;
684
+ try {
685
+ const events = list("test.application.events");
686
+ const untouched = list(events.namespace, "untouched");
687
+ const branch = await source.createBranch("review", null, BACKGROUND_CONTEXT);
688
+ await source.mutate((mutator) => mutator.commit([
689
+ setValue(laneConfig("review"), configuration),
690
+ setValue(laneState("review"), idleLaneState),
691
+ appendList(events, "old first"),
692
+ appendList(events, "old second"),
693
+ appendList(untouched, "predates fork point"),
694
+ ], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
695
+ const entryId = await branch.appendCustomEntry("fork-point", undefined, BACKGROUND_CONTEXT);
696
+ await source.deleteList(events, BACKGROUND_CONTEXT);
697
+ await source.appendList(events, "new", BACKGROUND_CONTEXT);
698
+ deepStrictEqual((await source.readList(events, undefined, BACKGROUND_CONTEXT)).map(({ value }) => value), ["new"]);
699
+ if (sourceState === "closed")
700
+ await source.close(BACKGROUND_CONTEXT);
701
+ fork = await repo.fork(source.metadata, { scope: "branch", branch: "review", entryId }, BACKGROUND_CONTEXT);
702
+ strictEqual(await getBranchTip(fork, "review"), entryId);
703
+ deepStrictEqual(await fork.readList(events, undefined, BACKGROUND_CONTEXT), []);
704
+ // Even elements still surviving from before the fork point are excluded.
705
+ deepStrictEqual(await fork.readList(untouched, undefined, BACKGROUND_CONTEXT), []);
706
+ }
707
+ finally {
708
+ await Promise.all([source.close(BACKGROUND_CONTEXT), fork?.close(BACKGROUND_CONTEXT)]);
709
+ }
710
+ }),
711
+ ]);
712
+ }
713
+ /** Creates fork cases that require destination reservation across create and fork. */
714
+ export function createSessionRepoForkDestinationReservationConformance(backendFactory, onClose) {
715
+ const factory = prepareRepoCaseFactory(backendFactory, onClose);
716
+ return [
717
+ createCase(factory, "fork coordination", "publishes create when it reserves a shared destination id first", async ({ repo }) => {
718
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
719
+ const results = await Promise.allSettled([
720
+ repo.create({ id: "destination" }, BACKGROUND_CONTEXT),
721
+ repo.fork(source.metadata, { id: "destination", scope: "tree" }, BACKGROUND_CONTEXT),
722
+ ]);
723
+ strictEqual(results[0].status, "fulfilled");
724
+ strictEqual(results[1].status, "rejected");
725
+ if (results[0].status === "fulfilled")
726
+ await results[0].value.close(BACKGROUND_CONTEXT);
727
+ await source.close(BACKGROUND_CONTEXT);
728
+ }),
729
+ createCase(factory, "fork coordination", "publishes fork when it reserves a shared destination id first", async ({ repo }) => {
730
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
731
+ const results = await Promise.allSettled([
732
+ repo.fork(source.metadata, { id: "destination", scope: "tree" }, BACKGROUND_CONTEXT),
733
+ repo.create({ id: "destination" }, BACKGROUND_CONTEXT),
734
+ ]);
735
+ strictEqual(results[0].status, "fulfilled");
736
+ strictEqual(results[1].status, "rejected");
737
+ if (results[0].status === "fulfilled")
738
+ await results[0].value.close(BACKGROUND_CONTEXT);
739
+ await source.close(BACKGROUND_CONTEXT);
740
+ }),
741
+ ];
742
+ }
743
+ /** Creates fork cases that require a snapshot boundary on an active source storage queue. */
744
+ export function createSessionRepoForkSourceSnapshotConformance(backendFactory, onClose) {
745
+ const factory = prepareRepoCaseFactory(backendFactory, onClose);
746
+ return [
747
+ createCase(factory, "fork coordination", "captures one coherent boundary between source commits", async ({ repo }) => {
748
+ const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
749
+ const firstMutation = await source.beginMutation(BACKGROUND_CONTEXT);
750
+ const firstCommit = firstMutation.commit([
751
+ insertEntry({
752
+ id: ROOT_ID,
753
+ parentId: null,
754
+ type: "custom",
755
+ customType: "first",
756
+ }),
757
+ setValue(branchTip("main"), ROOT_ID),
758
+ setValue(laneConfig("main"), configuration),
759
+ setValue(laneState("main"), idleLaneState),
760
+ setValue(sessionName, "first name"),
761
+ setValue(entryLabel(ROOT_ID), "first label"),
762
+ ], BACKGROUND_CONTEXT);
763
+ const fork = repo.fork(source.metadata, { id: "fork", scope: "branch", branch: "main" }, BACKGROUND_CONTEXT);
764
+ const secondCommit = source.mutate((mutator) => mutator.commit([
765
+ insertEntry({
766
+ id: CHILD_ID,
767
+ parentId: ROOT_ID,
768
+ type: "custom",
769
+ customType: "second",
770
+ }),
771
+ setValue(branchTip("main"), CHILD_ID),
772
+ setValue(sessionName, "second name"),
773
+ setValue(entryLabel(ROOT_ID), "second label"),
774
+ ], BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
775
+ const [, forked] = await Promise.all([firstCommit, fork]);
776
+ await firstMutation.end(BACKGROUND_CONTEXT);
777
+ await secondCommit;
778
+ strictEqual(await getBranchTip(forked), ROOT_ID);
779
+ deepStrictEqual((await forked.findEntries({ order: "asc" }, BACKGROUND_CONTEXT)).map(({ id }) => id), [ROOT_ID]);
780
+ strictEqual(await forked.getName(BACKGROUND_CONTEXT), "first name");
781
+ strictEqual(await forked.getLabel(ROOT_ID, BACKGROUND_CONTEXT), "first label");
782
+ await Promise.all([source.close(BACKGROUND_CONTEXT), forked.close(BACKGROUND_CONTEXT)]);
783
+ }),
784
+ ];
785
+ }
786
+ /** Creates every fork coordination case. */
787
+ export function createSessionRepoForkCoordinationConformance(backendFactory, onClose) {
788
+ return [
789
+ ...createSessionRepoForkDestinationReservationConformance(backendFactory, onClose),
790
+ ...createSessionRepoForkSourceSnapshotConformance(backendFactory, onClose),
791
+ ];
792
+ }
793
+ /** Creates every fork conformance case. */
794
+ export function createSessionRepoForkConformance(backendFactory, onClose) {
795
+ return [
796
+ ...createSessionRepoForkBehaviorConformance(backendFactory, onClose),
797
+ ...createSessionRepoForkCoordinationConformance(backendFactory, onClose),
798
+ ];
799
+ }
800
+ /** Creates every SessionRepo conformance case. */
801
+ export function createSessionRepoConformance(factory, onClose) {
802
+ return [
803
+ ...createSessionRepoLifecycleConformance(factory, onClose),
804
+ ...createSessionRepoOwnershipConformance(factory, onClose),
805
+ ...createSessionRepoMessageConformance(factory, onClose),
806
+ ...createSessionRepoForkConformance(factory, onClose),
807
+ ];
808
+ }
809
+ //# sourceMappingURL=session-repo.js.map