@visual-z/xeno 0.8.4 → 0.9.1

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 (350) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
package/src/store/log.ts DELETED
@@ -1,725 +0,0 @@
1
- /**
2
- * The session tree on disk (card: refactor-log-tree).
3
- *
4
- * The only module in the project that does I/O, so ordering, durability and
5
- * corruption are handled here and nowhere else.
6
- *
7
- * Layout:
8
- * log/node-0000.jsonl header line, then one JSON event per line
9
- * log/handoff-0000.yaml the handoff derived when that node closed
10
- * log/manifest.json the index: one fixed-size record per closed node
11
- *
12
- * NNNN is the node's ALLOCATION order, not its position in the tree. Structure
13
- * lives only in parent pointers -- the same split git makes between a commit's
14
- * identity and its shape. A linear session is the degenerate tree where every
15
- * node has one child, which is why this refactor leaves behaviour unchanged.
16
- *
17
- * One file per node, one writer per file. That is what makes parallel branches
18
- * free of contention: no locks, no interleaved half-written lines, and D14's
19
- * "a subagent writes its own branch file" becomes automatic rather than an
20
- * extra mechanism.
21
- *
22
- * JSONL rather than SQLite so the agent can recover anything a handoff omits
23
- * with the tools it already has -- grep, sed, jq -- and no recall tool has to
24
- * exist. What that gives up is a real index, so cross-node queries by resource
25
- * are linear scans; per-node files keep each one small enough for that to be
26
- * fine and, unlike a single rollout file, nothing grows without bound.
27
- */
28
-
29
- import { access, mkdir, readdir, readFile, open } from "node:fs/promises";
30
- import { createHash } from "node:crypto";
31
- import { join } from "node:path";
32
- import type { LogEvent, NodeRef, IsoTime, EventId } from "../core/events.ts";
33
- import { assertNever } from "../core/exhaustive.ts";
34
- import type { Handoff } from "../core/handoff.ts";
35
- import { isLogEvent, isRecord, validNodeRef } from "../core/event-validation.ts";
36
- import { emitHandoff } from "../core/handoff.ts";
37
- import { atomicWrite, durableAppend } from "./atomic.ts";
38
- import { serialized } from "./serial.ts";
39
- import { stateAtPosition } from "../core/session-position.ts";
40
- import { SESSION_FORMAT, SessionFormatError } from "../core/session-format.ts";
41
-
42
- /** Allocation number. Not an ordering over the tree. */
43
- export type NodeId = number;
44
-
45
- /**
46
- * The first line of every node file.
47
- *
48
- * It shares the file with events and is shaped like one, but it is not an
49
- * event: it describes the node rather than something that happened inside it,
50
- * so readNode returns it separately and it never reaches the projection.
51
- */
52
- /**
53
- * What a node takes from its parent.
54
- *
55
- * inherit the parent's events, so the prefix is shared and the cache with
56
- * it -- a fork exploring an alternative
57
- * handoff the parent's derived handoff instead of its events -- a cut,
58
- * taken when the context filled
59
- * none nothing; the parent pointer records provenance only -- a
60
- * subagent working in isolation
61
- *
62
- * Declared by the child rather than inferred from the parent, because the
63
- * same parent can have children of every kind at once. Inferring it from
64
- * whether the parent had a handoff is right for a linear session and wrong
65
- * for a fork, whose parent is almost always closed.
66
- */
67
- export type Basis = "inherit" | "handoff" | "none";
68
-
69
- export interface NodeHeader {
70
- t: "header";
71
- node: NodeId;
72
- parent: NodeRef | null;
73
- basis: Basis;
74
- created_at: IsoTime;
75
- adopted_from?: NodeRef;
76
- }
77
-
78
- export interface NodeRecord {
79
- node: NodeId;
80
- parent: NodeRef | null;
81
- log_ref: string;
82
- sha256: string;
83
- handoff_ref: string | null;
84
- events: number;
85
- closed_at: IsoTime;
86
- }
87
-
88
- /**
89
- * Where the session was when it last stopped.
90
- *
91
- * Written down rather than inferred. The manifest indexes only CLOSED nodes,
92
- * so the open one is not in it -- and the tree shape cannot be read backwards
93
- * to find the cursor, because three legal shapes are indistinguishable: a
94
- * node abandoned by a rewind stays open, a resolved branch holds the highest
95
- * id while the cursor sits in its parent, and both can be true at once. This
96
- * project already settled the same question once, for `basis`, and settled it
97
- * the same way: declare it instead of deducing it.
98
- */
99
- export interface Cursor {
100
- /** The open node the session was working in. */
101
- node: NodeId;
102
- /**
103
- * The allocator's state, not `max(id) + 1`. Dead limbs and resolved
104
- * branches both hold ids, and recomputing the next one is exactly the
105
- * shape of the bug that once had a cut and a branch agree on id 1 and
106
- * silently truncate a node's whole log.
107
- */
108
- next_node: NodeId;
109
- /**
110
- * The model this session ran under. Its thinking blocks are that model's:
111
- * replaying them to another provider is a bad request, so resume checks
112
- * this rather than discovering it on the wire.
113
- */
114
- model: string;
115
- /** New cursors disambiguate identical model IDs served by different providers. */
116
- provider?: string;
117
- /** Configuration takes effect after this committed event; absent means node start. */
118
- at_event?: string;
119
- thinking?: string;
120
- blob_dir?: string;
121
- /** Legacy historical positions with unknown model provenance must cut before dispatch. */
122
- needs_model_checkpoint?: boolean;
123
- updated_at: IsoTime;
124
- /** Active checkout, which changes when a writing branch becomes mainline. */
125
- workspace?: string;
126
- /** Content-addressed frozen context; credentials and approvals are excluded. */
127
- context_ref?: string;
128
- /**
129
- * The xeno release that wrote this cursor. Every start writes one, so the
130
- * append-only metadata journal is a timeline of the versions a session ran under.
131
- */
132
- xeno_version?: string;
133
- /**
134
- * A human name. Optional because most sessions never get one, and a log
135
- * written before names existed has none. Absent rather than empty: an
136
- * empty string would still show up in a listing as a name that isn't.
137
- */
138
- name?: string;
139
- }
140
-
141
- export interface Manifest {
142
- version: 1;
143
- nodes: NodeRecord[];
144
- /** An import keeps historical bytes and relocates their storage through journal metadata. */
145
- relocated?: true;
146
- /** Absent in a log written before resume existed, and in an empty one. */
147
- cursor?: Cursor;
148
- parent_session?: string;
149
- labels?: Record<string, { text: string; at: string }>;
150
- }
151
-
152
- /**
153
- * A chain longer than this is a bug rather than a deep session: it would mean
154
- * thousands of boundaries with no progress, which the stop-and-ask rule (D9)
155
- * catches long before. The cap exists so a malformed tree fails fast instead
156
- * of consuming the process -- unbounded traversal is not allowed here any more
157
- * than an unbounded buffer would be.
158
- */
159
- export const MAX_ANCESTRY_DEPTH = 1000;
160
-
161
- const pad = (n: number) => String(n).padStart(4, "0");
162
-
163
- export function nodePath(dir: string, node: NodeId): string {
164
- return join(dir, `node-${pad(node)}.jsonl`);
165
- }
166
-
167
- export function handoffPath(dir: string, node: NodeId): string {
168
- return join(dir, `handoff-${pad(node)}.yaml`);
169
- }
170
-
171
- export function manifestPath(dir: string): string {
172
- return join(dir, "manifest.json");
173
- }
174
-
175
- export async function initLog(dir: string): Promise<void> {
176
- await mkdir(dir, { recursive: true });
177
- }
178
-
179
- /**
180
- * Every node in the log, in allocation order.
181
- *
182
- * From the directory rather than the manifest: the manifest holds only CLOSED
183
- * nodes, and an open node -- including the one being written right now -- is
184
- * exactly the one a reader is most likely to be asking about.
185
- */
186
- export async function listNodes(dir: string): Promise<NodeId[]> {
187
- let names: string[];
188
- try {
189
- names = await readdir(dir);
190
- } catch (err) {
191
- if ((err as { code?: string }).code === "ENOENT") return [];
192
- throw err;
193
- }
194
- return names
195
- .map((n) => /^node-(\d+)\.jsonl$/.exec(n))
196
- .filter((m): m is RegExpExecArray => m !== null)
197
- .map((m) => Number(m[1]))
198
- .sort((a, b) => a - b);
199
- }
200
-
201
- /** Creates a node file by writing its header. Must precede any appendEvent. */
202
- export async function initNode(
203
- dir: string,
204
- node: NodeId,
205
- parent: NodeRef | null,
206
- createdAt: IsoTime,
207
- // Defaults to inherit, which is the only safe choice for an unstated one:
208
- // inheriting too much shows the model work it has already seen, while
209
- // truncating too early silently hides work it needed. A caller that means
210
- // to cut says so.
211
- basis: Basis = parent === null ? "none" : "inherit",
212
- metadata: Pick<NodeHeader, "adopted_from"> = {},
213
- ): Promise<NodeHeader> {
214
- const header: NodeHeader = { t: "header", node, parent, basis, created_at: createdAt, ...metadata };
215
- try {
216
- // Exclusive create. A node log is append-only, so a second init is always
217
- // a bug -- and writeFile would truncate, taking every event in the node
218
- // with it while every ref pointing at it still resolves. Two allocators
219
- // over one id namespace once did exactly that here, silently.
220
- const file = await open(nodePath(dir, node), "wx", 0o600);
221
- try { await file.writeFile(JSON.stringify(header) + "\n"); await file.sync(); }
222
- finally { await file.close(); }
223
- const directory = await open(dir, "r");
224
- try { await directory.sync(); } finally { await directory.close(); }
225
- } catch (err) {
226
- if ((err as { code?: string }).code === "EEXIST") {
227
- throw new Error(`node ${node} already exists at ${nodePath(dir, node)} -- refusing to overwrite it`);
228
- }
229
- throw err;
230
- }
231
- return header;
232
- }
233
-
234
- /**
235
- * Appends one event as a single line.
236
- *
237
- * JSON.stringify never emits a raw newline inside a string, so one event is
238
- * always exactly one line and a partially written tail can only ever damage
239
- * the last line -- which readNode reports rather than silently dropping.
240
- */
241
- export async function appendEvent(
242
- dir: string,
243
- node: NodeId,
244
- event: LogEvent,
245
- ): Promise<void> {
246
- if ((await readManifest(dir)).nodes.some((entry) => entry.node === node)) {
247
- throw new Error(`INTEGRITY: cannot append to sealed node ${node}`);
248
- }
249
- await durableAppend(nodePath(dir, node), JSON.stringify(event) + "\n");
250
- }
251
-
252
- export interface ReadResult {
253
- /** Null when the file has no header line, which is itself reported as corrupt. */
254
- header: NodeHeader | null;
255
- events: LogEvent[];
256
- /** Lines that would not parse. Surfaced, never skipped quietly. */
257
- corrupt: { line: number; text: string }[];
258
- }
259
-
260
- export async function readNode(dir: string, node: NodeId): Promise<ReadResult> {
261
- let text: string;
262
- try {
263
- text = await readFile(nodePath(dir, node), "utf8");
264
- } catch (err) {
265
- if ((err as { code?: string }).code === "ENOENT") {
266
- return { header: null, events: [], corrupt: [] };
267
- }
268
- throw err;
269
- }
270
-
271
- let header: NodeHeader | null = null;
272
- const events: LogEvent[] = [];
273
- const corrupt: ReadResult["corrupt"] = [];
274
- const lines = text.split("\n");
275
- const ids = new Set<string>();
276
-
277
- for (let i = 0; i < lines.length; i++) {
278
- const line = lines[i]!;
279
- if (line.length === 0) continue;
280
- let parsed: unknown;
281
- try {
282
- parsed = JSON.parse(line);
283
- } catch {
284
- corrupt.push({ line: i + 1, text: line.slice(0, 200) });
285
- continue;
286
- }
287
- // The header describes the node; it is not something that happened in it.
288
- if (isRecord(parsed) && parsed["t"] === "header") {
289
- if (i !== 0 || header !== null || parsed["node"] !== node || typeof parsed["created_at"] !== "string" ||
290
- !(parsed["parent"] === null || validNodeRef(parsed["parent"])) ||
291
- !(parsed["basis"] === undefined || ["inherit", "none", "handoff"].includes(String(parsed["basis"])))) {
292
- corrupt.push({ line: i + 1, text: line.slice(0, 200) }); continue;
293
- }
294
- header = {
295
- t: "header", node, parent: parsed["parent"], created_at: parsed["created_at"],
296
- basis: parsed["basis"] === undefined ? (parsed["parent"] === null ? "none" : "inherit") : parsed["basis"] as Basis,
297
- ...(validNodeRef(parsed["adopted_from"]) ? { adopted_from: parsed["adopted_from"] } : {}),
298
- };
299
- continue;
300
- }
301
- if (!isLogEvent(parsed) || ids.has(parsed.id) || i === lines.length - 1) {
302
- corrupt.push({ line: i + 1, text: line.slice(0, 200) }); continue;
303
- }
304
- ids.add(parsed.id);
305
- events.push(parsed);
306
- }
307
-
308
- return { header, events, corrupt };
309
- }
310
-
311
- // ---------------------------------------------------------------------------
312
- // Ancestry
313
- // ---------------------------------------------------------------------------
314
-
315
- export interface AncestryError {
316
- code:
317
- | "CYCLE"
318
- | "MISSING_PARENT"
319
- | "TOO_DEEP"
320
- | "MISSING_HEADER"
321
- | "CORRUPT"
322
- | "INTEGRITY"
323
- | "MISSING_BASIS_HANDOFF"
324
- | "MISSING_FORK_POINT";
325
- detail: string;
326
- }
327
-
328
- export type AncestryResult =
329
- | {
330
- ok: true;
331
- /**
332
- * The ancestor whose handoff is the context basis, set when a node on
333
- * the path declared `basis: "handoff"`. Its events do not replay. Null
334
- * when the walk reached a root or a `none` boundary instead.
335
- */
336
- basis: { node: NodeId; handoff_ref: string } | null;
337
- /** Root-to-leaf order, ready to hand to projectContext. */
338
- events: LogEvent[];
339
- /** Nodes contributing events, root-to-leaf. Excludes the basis node. */
340
- visited: NodeId[];
341
- }
342
- | { ok: false; error: AncestryError };
343
-
344
- /**
345
- * Walks the parent chain from `node` upward, collecting events, and stops at
346
- * the first ancestor that closed with a handoff.
347
- *
348
- * Returns an error rather than partial results: a chain that cannot be walked
349
- * means the caller's picture of the tree is wrong, and half an ancestry would
350
- * project a context that silently omits work.
351
- */
352
- export async function readAncestry(
353
- dir: string,
354
- node: NodeId,
355
- manifest?: Manifest,
356
- ): Promise<AncestryResult> {
357
- const index = new Map(((manifest ?? await readManifest(dir)).nodes).map((r) => [r.node, r]));
358
- const chain: { node: NodeId; events: LogEvent[] }[] = [];
359
- // Revisiting a node means the parent pointers form a loop. The depth cap
360
- // alone would stop the walk, but it would report exhaustion -- a reader
361
- // needs to know the tree is malformed, not that the chain was long.
362
- const seen = new Set<NodeId>();
363
-
364
- let current: NodeId | null = node;
365
- // Set when a child declared a fork point: the parent contributes only the
366
- // events up to and including that one.
367
- let cutoff: EventId | undefined;
368
- let depth = 0;
369
- let basis: { node: NodeId; handoff_ref: string } | null = null;
370
-
371
- while (current !== null) {
372
- if (seen.has(current)) {
373
- return {
374
- ok: false,
375
- error: {
376
- code: "CYCLE",
377
- detail:
378
- `parent chain from node ${node} revisits node ${current} ` +
379
- `(walked ${[...seen].join(" -> ")})`,
380
- },
381
- };
382
- }
383
- seen.add(current);
384
-
385
- if (depth++ >= MAX_ANCESTRY_DEPTH) {
386
- return {
387
- ok: false,
388
- error: {
389
- code: "TOO_DEEP",
390
- detail: `ancestry of node ${node} exceeded ${MAX_ANCESTRY_DEPTH} nodes`,
391
- },
392
- };
393
- }
394
-
395
- const { header, events, corrupt } = await readNode(dir, current);
396
- const sealed = index.get(current);
397
- if (sealed && sealed.sha256 !== await digestNode(dir, current)) {
398
- return { ok: false, error: { code: "INTEGRITY", detail: `node ${current} content digest does not match its sealed record` } };
399
- }
400
- if (header === null) {
401
- return {
402
- ok: false,
403
- error: {
404
- code: "MISSING_HEADER",
405
- detail: `node ${current} has no header line (reached from ${node})`,
406
- },
407
- };
408
- }
409
- if (corrupt.length > 0) {
410
- return {
411
- ok: false,
412
- error: {
413
- code: "CORRUPT",
414
- detail: `node ${current} has ${corrupt.length} unparsable line(s), first at ${corrupt[0]!.line}`,
415
- },
416
- };
417
- }
418
-
419
- // A fork point truncates this node's contribution. Naming an event the
420
- // node does not have is an error rather than a silent fall back to all of
421
- // them: the caller's picture of the tree is wrong either way, and half an
422
- // ancestry projects a context that quietly omits work.
423
- let contributed = events;
424
- if (cutoff !== undefined) {
425
- const at = events.findIndex((e) => e.id === cutoff);
426
- if (at < 0) {
427
- return {
428
- ok: false,
429
- error: {
430
- code: "MISSING_FORK_POINT",
431
- detail: `node ${current} has no event ${cutoff} (fork point reached from ${node})`,
432
- },
433
- };
434
- }
435
- contributed = events.slice(0, at + 1);
436
- }
437
- chain.push({ node: current, events: contributed });
438
-
439
- const parent: NodeRef | null = header.parent;
440
- if (parent === null) break;
441
-
442
- // What this node takes from its parent is declared here, not inferred
443
- // from the parent. The same parent can have a fork, a cut and a subagent
444
- // as children at once, so no property of the parent could decide it.
445
- switch (header.basis) {
446
- case "none":
447
- // The parent pointer is provenance; it contributes no context.
448
- current = null;
449
- break;
450
- case "handoff": {
451
- const record = index.get(parent.node);
452
- if (!record?.handoff_ref) {
453
- return {
454
- ok: false,
455
- error: {
456
- code: "MISSING_BASIS_HANDOFF",
457
- detail:
458
- `node ${current} declares a handoff basis but node ${parent.node} has none ` +
459
- `(it may not be closed yet, or the manifest was not passed)`,
460
- },
461
- };
462
- }
463
- basis = { node: parent.node, handoff_ref: record.handoff_ref };
464
- current = null;
465
- break;
466
- }
467
- case "inherit":
468
- cutoff = parent.at_event;
469
- current = parent.node;
470
- break;
471
- default:
472
- return assertNever(header.basis, "readAncestry");
473
- }
474
- }
475
-
476
- chain.reverse();
477
- return {
478
- ok: true,
479
- basis,
480
- events: chain.flatMap((c) => c.events),
481
- visited: chain.map((c) => c.node),
482
- };
483
- }
484
-
485
- // ---------------------------------------------------------------------------
486
- // Digest, handoff, manifest
487
- // ---------------------------------------------------------------------------
488
-
489
- export async function digestNode(dir: string, node: NodeId): Promise<string> {
490
- try {
491
- const buf = await readFile(nodePath(dir, node));
492
- return createHash("sha256").update(buf).digest("hex");
493
- } catch (err) {
494
- if ((err as { code?: string }).code === "ENOENT") {
495
- return createHash("sha256").update("").digest("hex");
496
- }
497
- throw err;
498
- }
499
- }
500
-
501
- export async function writeHandoff(
502
- dir: string,
503
- node: NodeId,
504
- handoff: Handoff,
505
- ): Promise<string> {
506
- const text = emitHandoff(handoff);
507
- await atomicWrite(handoffPath(dir, node), text);
508
- return text;
509
- }
510
-
511
- type MetadataRecord =
512
- | { kind: "format"; version: number }
513
- | { kind: "relocate"; workspace: string }
514
- | { kind: "cursor"; cursor: Cursor }
515
- | { kind: "parent_session"; path: string }
516
- | { kind: "label"; target: string; text: string; at: string }
517
- | { kind: "node"; record: NodeRecord };
518
-
519
- function metadataPath(dir: string): string { return join(dir, "metadata.jsonl"); }
520
-
521
- /**
522
- * S.3: the journal's first record declares the session format. A directory in
523
- * any other format -- a journal opening with something else, or a manifest
524
- * with no journal at all -- is refused before anything reads or writes it.
525
- * Null means a directory no session has written yet.
526
- */
527
- async function readMetadata(dir: string): Promise<MetadataRecord[] | null> {
528
- let text: string;
529
- try { text = await readFile(metadataPath(dir), "utf8"); }
530
- catch (error) {
531
- if ((error as { code?: string }).code !== "ENOENT") throw error;
532
- if (await access(manifestPath(dir)).then(() => true, () => false)) throw new SessionFormatError(dir, "undeclared");
533
- return null;
534
- }
535
- if (text.length > 0 && !text.endsWith("\n")) throw new Error("INTEGRITY: incomplete metadata journal tail; no cursor is guessed");
536
- const records = text.split("\n").filter(Boolean).map((line) => {
537
- const envelope = JSON.parse(line) as { record: MetadataRecord; sha256: string };
538
- const digest = createHash("sha256").update(JSON.stringify(envelope.record)).digest("hex");
539
- if (envelope.sha256 !== digest) throw new Error("INTEGRITY: metadata journal digest mismatch");
540
- return envelope.record;
541
- });
542
- const first = records[0];
543
- if (first?.kind !== "format") throw new SessionFormatError(dir, "undeclared");
544
- if (first.version !== SESSION_FORMAT) throw new SessionFormatError(dir, first.version);
545
- return records;
546
- }
547
-
548
- /** The metadata journal already owns configuration history; no second state log is needed. */
549
- export async function readCursorAt(dir: string, node: number, atEvent?: string): Promise<Cursor | null> {
550
- const records = await readMetadata(dir);
551
- const states: Cursor[] = [];
552
- let relocated = false;
553
- for (const record of records ?? []) {
554
- if (record.kind === "cursor") states.push({ ...record.cursor });
555
- else if (record.kind === "relocate") { relocated = true; for (const state of states) state.workspace = record.workspace; }
556
- }
557
- if (relocated) for (const state of states) state.blob_dir = join(dir, "blobs");
558
- const { events } = await readNode(dir, node);
559
- return stateAtPosition(states, node, events.map(event => event.id), atEvent);
560
- }
561
-
562
- /**
563
- * When this session was forked, or null for an original (fix-fork-start-time).
564
- * copyAncestryTo keeps the parent's nodes and their timestamps, then writes
565
- * the fork cursor and, right after it, the parent_session record; that
566
- * cursor's updated_at is the fork moment. The last parent_session record is
567
- * this session's own: a copied parent that was itself a fork brings its
568
- * record along in the copied journal.
569
- */
570
- export async function forkedAt(dir: string): Promise<string | null> {
571
- const records = await readMetadata(dir) ?? [];
572
- const own = records.findLastIndex(record => record.kind === "parent_session");
573
- const cursor = records.slice(0, Math.max(0, own)).findLast(record => record.kind === "cursor");
574
- return own >= 0 && cursor?.kind === "cursor" ? cursor.cursor.updated_at : null;
575
- }
576
-
577
- /** D3: the replaceable manifest is a projection of committed metadata events. */
578
- export async function readManifest(dir: string): Promise<Manifest> {
579
- return serialized(dir, () => readManifestUnlocked(dir));
580
- }
581
-
582
- async function readManifestUnlocked(dir: string): Promise<Manifest> {
583
- const records = await readMetadata(dir);
584
- const manifest: Manifest = { version: 1, nodes: [] };
585
- for (const event of records ?? []) {
586
- switch (event.kind) {
587
- case "format": break;
588
- case "cursor": manifest.cursor = event.cursor; break;
589
- case "relocate":
590
- manifest.relocated = true;
591
- if (manifest.cursor) manifest.cursor = { ...manifest.cursor, workspace: event.workspace };
592
- break;
593
- case "parent_session": manifest.parent_session = event.path; break;
594
- case "label":
595
- manifest.labels ??= {};
596
- if (event.text) Object.defineProperty(manifest.labels, event.target, { value: { text: event.text, at: event.at }, enumerable: true, configurable: true, writable: true });
597
- else delete manifest.labels[event.target];
598
- break;
599
- case "node": {
600
- const i = manifest.nodes.findIndex((n) => n.node === event.record.node);
601
- if (i < 0) manifest.nodes.push(event.record);
602
- else manifest.nodes[i] = event.record;
603
- break;
604
- }
605
- default: return assertNever(event, "metadata journal");
606
- }
607
- }
608
- manifest.nodes.sort((a, b) => a.node - b.node);
609
- if (manifest.relocated && manifest.cursor) manifest.cursor = { ...manifest.cursor, blob_dir: join(dir, "blobs") };
610
- return manifest;
611
- }
612
-
613
- /** Relocation changes recovery locations, not historical events or evidence (D3/D8). */
614
- export async function relocateSession(dir: string, workspace: string): Promise<void> {
615
- return serialized(dir, async () => {
616
- await appendMetadata(dir, { kind: "relocate", workspace });
617
- await atomicWrite(manifestPath(dir), JSON.stringify(await readManifestUnlocked(dir), null, 2) + "\n");
618
- });
619
- }
620
-
621
- async function appendMetadata(dir: string, event: MetadataRecord): Promise<void> {
622
- await mkdir(dir, { recursive: true });
623
- const encode = (record: MetadataRecord) => JSON.stringify({
624
- record, sha256: createHash("sha256").update(JSON.stringify(record)).digest("hex"),
625
- }) + "\n";
626
- // The first write declares the format; readMetadata has already refused a
627
- // directory in any other format, so nothing here appends to an old log.
628
- if (await readMetadata(dir) === null) await durableAppend(metadataPath(dir), encode({ kind: "format", version: SESSION_FORMAT }));
629
- await durableAppend(metadataPath(dir), encode(event));
630
- }
631
-
632
- /**
633
- * Moves the cursor. Called at every point the session changes node, so that
634
- * "where was I" is a recorded fact rather than a reconstruction.
635
- *
636
- * Rewrites the whole manifest, like `recordNode`, so the index and the cursor
637
- * cannot disagree -- two files could say node 7 is both closed and current.
638
- *
639
- * A name already on the cursor is kept unless the caller set one. The loop
640
- * writes the cursor at every move and does not know about names; replacing
641
- * the whole record would drop a name the moment a session cuts or starts,
642
- * making `/name` a lie.
643
- */
644
- export async function writeCursor(dir: string, cursor: Cursor): Promise<void> {
645
- return serialized(dir, async () => {
646
- const manifest = await readManifestUnlocked(dir);
647
- const previousName = manifest.cursor?.name;
648
- manifest.cursor =
649
- previousName !== undefined && !("name" in cursor)
650
- ? { ...cursor, name: previousName }
651
- : cursor;
652
- await appendMetadata(dir, { kind: "cursor", cursor: manifest.cursor });
653
- await atomicWrite(manifestPath(dir), JSON.stringify(manifest, null, 2) + "\n");
654
- });
655
- }
656
-
657
- export async function readCursor(dir: string): Promise<Cursor | null> {
658
- return (await readManifest(dir)).cursor ?? null;
659
- }
660
-
661
- /** Labels are display metadata, never model evidence or rewritten conversation events. */
662
- export async function setSessionLabel(dir: string, target: string, text: string, at: string): Promise<void> {
663
- return serialized(dir, async () => {
664
- await appendMetadata(dir, { kind: "label", target, text: text.trim(), at });
665
- await atomicWrite(manifestPath(dir), JSON.stringify(await readManifestUnlocked(dir), null, 2) + "\n");
666
- });
667
- }
668
- export async function setParentSession(dir: string, path: string): Promise<void> {
669
- return serialized(dir, async () => {
670
- await appendMetadata(dir, { kind: "parent_session", path });
671
- await atomicWrite(manifestPath(dir), JSON.stringify(await readManifestUnlocked(dir), null, 2) + "\n");
672
- });
673
- }
674
-
675
- /** Direct children of `node`, in allocation order. */
676
- export function childrenOf(manifest: Manifest, node: NodeId): NodeRecord[] {
677
- return manifest.nodes.filter((r) => r.parent?.node === node);
678
- }
679
-
680
- /** Records with no parent. A well-formed tree has exactly one. */
681
- export function rootsOf(manifest: Manifest): NodeRecord[] {
682
- return manifest.nodes.filter((r) => r.parent === null);
683
- }
684
-
685
- /**
686
- * Records a closed node. Rewrites the manifest whole, which is fine because it
687
- * holds one small record per node -- and makes the file atomically consistent
688
- * rather than an append log that can end mid-record.
689
- */
690
- export async function recordNode(dir: string, record: NodeRecord): Promise<Manifest> {
691
- return serialized(dir, async () => {
692
- const manifest = await readManifestUnlocked(dir);
693
- const existing = manifest.nodes.findIndex((r) => r.node === record.node);
694
- if (existing >= 0) manifest.nodes[existing] = record;
695
- else manifest.nodes.push(record);
696
- manifest.nodes.sort((a, b) => a.node - b.node);
697
- await appendMetadata(dir, { kind: "node", record });
698
- await atomicWrite(manifestPath(dir), JSON.stringify(manifest, null, 2) + "\n");
699
- return manifest;
700
- });
701
- }
702
-
703
- /** Closes a node: fingerprint it, write its handoff, index it. */
704
- export async function closeNode(
705
- dir: string,
706
- node: NodeId,
707
- handoff: Handoff,
708
- eventCount: number,
709
- closedAt: IsoTime,
710
- ): Promise<NodeRecord> {
711
- const { header } = await readNode(dir, node);
712
- const sha256 = await digestNode(dir, node);
713
- await writeHandoff(dir, node, handoff);
714
- const record: NodeRecord = {
715
- node,
716
- parent: header?.parent ?? null,
717
- log_ref: nodePath(dir, node),
718
- sha256,
719
- handoff_ref: handoffPath(dir, node),
720
- events: eventCount,
721
- closed_at: closedAt,
722
- };
723
- await recordNode(dir, record);
724
- return record;
725
- }