handmux 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/README.md +31 -8
  2. package/README.zh-CN.md +31 -8
  3. package/bin/handmux.js +14 -688
  4. package/bin/node-version.js +20 -0
  5. package/dist/bin/handmux.js +1015 -0
  6. package/dist/connectors/bridgeClient.js +385 -0
  7. package/dist/connectors/claude/index.js +476 -0
  8. package/dist/connectors/pi/bridgeClient.js +8 -0
  9. package/dist/connectors/pi/index.js +851 -0
  10. package/dist/hooks/handmux-notify.sh +64 -0
  11. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  12. package/dist/hooks/handmux-write.cjs +264 -0
  13. package/dist/package.json +127 -0
  14. package/dist/public/assets/index-CTrqTqtT.css +32 -0
  15. package/dist/public/assets/index-DmjeMSQA.js +346 -0
  16. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  17. package/{public → dist/public}/index.html +5 -4
  18. package/dist/src/agent-runtime/adapter.js +204 -0
  19. package/dist/src/agent-runtime/bridge.js +1006 -0
  20. package/dist/src/agent-runtime/bridgeStore.js +25 -0
  21. package/dist/src/agent-runtime/bridgeTransport.js +619 -0
  22. package/dist/src/agent-runtime/bridgeTypes.js +13 -0
  23. package/dist/src/agent-runtime/builtinRuntime.js +193 -0
  24. package/dist/src/agent-runtime/conversation.js +2179 -0
  25. package/dist/src/agent-runtime/conversationActivation.js +75 -0
  26. package/dist/src/agent-runtime/conversationActivity.js +69 -0
  27. package/dist/src/agent-runtime/conversationControls.js +206 -0
  28. package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
  29. package/dist/src/agent-runtime/conversationStore.js +352 -0
  30. package/dist/src/agent-runtime/conversationTypes.js +1 -0
  31. package/dist/src/agent-runtime/conversationValidation.js +339 -0
  32. package/dist/src/agent-runtime/inbox.js +973 -0
  33. package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
  34. package/dist/src/agent-runtime/inboxStore.js +25 -0
  35. package/dist/src/agent-runtime/inboxTypes.js +1 -0
  36. package/dist/src/agent-runtime/interaction.js +829 -0
  37. package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
  38. package/dist/src/agent-runtime/interactionStore.js +18 -0
  39. package/dist/src/agent-runtime/interactionTypes.js +1 -0
  40. package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
  41. package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
  42. package/dist/src/agent-runtime/resourceTypes.js +1 -0
  43. package/dist/src/agent-runtime/resources.js +245 -0
  44. package/dist/src/agent-runtime/run.js +328 -0
  45. package/dist/src/agent-runtime/runtime.js +793 -0
  46. package/dist/src/agent-runtime/sessionControl.js +164 -0
  47. package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
  48. package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
  49. package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
  50. package/dist/src/agents/claude.js +203 -0
  51. package/dist/src/agents/claudeConversation.js +362 -0
  52. package/dist/src/agents/claudeInteraction.js +134 -0
  53. package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
  54. package/dist/src/agents/codex.js +262 -0
  55. package/dist/src/agents/codexConversation.js +1103 -0
  56. package/dist/src/agents/codexConversationActivation.js +121 -0
  57. package/dist/src/agents/codexConversationControls.js +256 -0
  58. package/dist/src/agents/codexInteraction.js +306 -0
  59. package/dist/src/agents/codexSessionControl.js +118 -0
  60. package/dist/src/agents/codexSubscriptionUsage.js +313 -0
  61. package/dist/src/agents/codexTerminalControl.js +23 -0
  62. package/dist/src/agents/conversationProjectionSafety.js +416 -0
  63. package/dist/src/agents/index.js +39 -0
  64. package/dist/src/agents/nativeInbox.js +264 -0
  65. package/dist/src/agents/nativeInboxSources.js +115 -0
  66. package/dist/src/agents/pi.js +28 -0
  67. package/dist/src/agents/piConversation.js +523 -0
  68. package/dist/src/agents/piConversationHistory.js +502 -0
  69. package/dist/src/agents/piInboxBridge.js +368 -0
  70. package/dist/src/agents/piSessionControl.js +32 -0
  71. package/dist/src/agents/processIdentity.js +94 -0
  72. package/dist/src/agents/scanUtils.js +262 -0
  73. package/dist/src/apiAccountProviders.js +194 -0
  74. package/dist/src/apiAccounts.js +592 -0
  75. package/dist/src/apiErrors.js +86 -0
  76. package/{src → dist/src}/appName.js +12 -13
  77. package/dist/src/asr/iflyConfig.js +7 -0
  78. package/dist/src/asr/iflySign.js +14 -0
  79. package/dist/src/auth.js +27 -0
  80. package/dist/src/browser/bootstrap.js +72 -0
  81. package/dist/src/browser/cookieProfiles.js +413 -0
  82. package/dist/src/browser/coordinator.js +51 -0
  83. package/dist/src/browser/hammerheadCompat.js +104 -0
  84. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  85. package/dist/src/browser/manager.js +560 -0
  86. package/dist/src/browser/originLabel.js +15 -0
  87. package/dist/src/browser/profilePersistence.js +235 -0
  88. package/dist/src/browser/publicProxy.js +176 -0
  89. package/dist/src/browser/routes.js +159 -0
  90. package/dist/src/browser/sessionStore.js +72 -0
  91. package/dist/src/browser/siteVersion.js +121 -0
  92. package/dist/src/browser/targetPolicy.js +163 -0
  93. package/{src → dist/src}/browser/worker.js +14 -18
  94. package/dist/src/browser/workerClient.js +286 -0
  95. package/dist/src/browser/workerServer.js +91 -0
  96. package/dist/src/captureBackground.js +95 -0
  97. package/dist/src/claudeEvents.js +525 -0
  98. package/dist/src/cli/agentIntegration.js +131 -0
  99. package/dist/src/cli/agentLauncher.js +26 -0
  100. package/dist/src/cli/claudeHooks.js +290 -0
  101. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  102. package/dist/src/cli/cloudflared.js +106 -0
  103. package/dist/src/cli/codexManaged.js +80 -0
  104. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  105. package/dist/src/cli/drivers.js +98 -0
  106. package/dist/src/cli/hookScaffold.js +39 -0
  107. package/dist/src/cli/i18n/en.js +392 -0
  108. package/{src → dist/src}/cli/i18n/index.js +27 -25
  109. package/dist/src/cli/i18n/zh.js +391 -0
  110. package/dist/src/cli/legacyCodexHooks.js +52 -0
  111. package/{src → dist/src}/cli/natappUrl.js +2 -3
  112. package/dist/src/cli/options.js +245 -0
  113. package/dist/src/cli/piExtension.js +155 -0
  114. package/dist/src/cli/probe.js +16 -0
  115. package/{src → dist/src}/cli/prompt.js +4 -6
  116. package/dist/src/cli/pushCmd.js +102 -0
  117. package/{src → dist/src}/cli/qr.js +19 -19
  118. package/dist/src/cli/service.js +154 -0
  119. package/dist/src/cli/setupModel.js +194 -0
  120. package/dist/src/cli/setupWizard.js +647 -0
  121. package/dist/src/cli/shortcutEditor.js +351 -0
  122. package/dist/src/cli/sshTunnel.js +18 -0
  123. package/dist/src/cli/state.js +117 -0
  124. package/dist/src/cli/statusLine.js +115 -0
  125. package/dist/src/cli/supervisor.js +278 -0
  126. package/dist/src/cli/supervisorHealth.js +21 -0
  127. package/dist/src/cli/supervisorLaunch.js +148 -0
  128. package/dist/src/cli/supervisorProcesses.js +62 -0
  129. package/dist/src/cli/supervisorState.js +62 -0
  130. package/dist/src/cli/tmuxVersion.js +53 -0
  131. package/{src → dist/src}/cli/tunlite.js +7 -10
  132. package/dist/src/cli/tunnelClients.js +72 -0
  133. package/{src → dist/src}/cli/updateCheck.js +98 -69
  134. package/{src → dist/src}/cli/urlHost.js +8 -4
  135. package/dist/src/cli/workspaceCmd.js +347 -0
  136. package/dist/src/codexAppServer.js +3436 -0
  137. package/dist/src/codexConversationProjection.js +199 -0
  138. package/dist/src/codexDiff.js +118 -0
  139. package/dist/src/codexMessageIdentity.js +18 -0
  140. package/dist/src/codexPlan.js +30 -0
  141. package/dist/src/codexQueueProtocol.js +137 -0
  142. package/dist/src/codexStreamProtocol.js +207 -0
  143. package/dist/src/codexToolProtocol.js +68 -0
  144. package/dist/src/codexTranscriptParse.js +835 -0
  145. package/dist/src/codexUsageSnapshot.js +228 -0
  146. package/dist/src/config.js +8 -0
  147. package/{src → dist/src}/docPath.js +22 -24
  148. package/dist/src/docs.js +337 -0
  149. package/dist/src/git.js +284 -0
  150. package/dist/src/healthProtocol.js +97 -0
  151. package/dist/src/httpApi.js +96 -0
  152. package/dist/src/jsonStore.js +15 -0
  153. package/{src → dist/src}/keyNames.js +3 -3
  154. package/dist/src/notifications.js +111 -0
  155. package/dist/src/orphans.js +167 -0
  156. package/dist/src/paneInput.js +42 -0
  157. package/dist/src/pendingPrompt.js +194 -0
  158. package/dist/src/previewServer.js +202 -0
  159. package/dist/src/previews.js +179 -0
  160. package/dist/src/privateStateStore.js +124 -0
  161. package/dist/src/projectTask/backup.js +224 -0
  162. package/dist/src/projectTask/lock.js +108 -0
  163. package/dist/src/projectTask/migrations.js +90 -0
  164. package/dist/src/projectTask/routes.js +155 -0
  165. package/dist/src/projectTask/runtime.js +182 -0
  166. package/dist/src/projectTask/schema.js +56 -0
  167. package/dist/src/projectTask/store.js +457 -0
  168. package/dist/src/push.js +224 -0
  169. package/dist/src/routes/agents.js +631 -0
  170. package/dist/src/routes/apiAccounts.js +82 -0
  171. package/dist/src/routes/files.js +230 -0
  172. package/dist/src/routes/git.js +89 -0
  173. package/dist/src/routes/health.js +16 -0
  174. package/dist/src/routes/notifications.js +16 -0
  175. package/dist/src/routes/previews.js +43 -0
  176. package/dist/src/routes/push.js +145 -0
  177. package/dist/src/routes/sessions.js +244 -0
  178. package/dist/src/routes/system.js +220 -0
  179. package/dist/src/routes/terminal.js +244 -0
  180. package/dist/src/routes/workspace.js +267 -0
  181. package/dist/src/server.js +315 -0
  182. package/dist/src/shortcutConfig.js +83 -0
  183. package/{src → dist/src}/staticCache.js +5 -3
  184. package/{src → dist/src}/staticCompression.js +5 -6
  185. package/dist/src/terminalStream.js +441 -0
  186. package/{src → dist/src}/tmux/commands.js +185 -172
  187. package/dist/src/tmux/format.js +47 -0
  188. package/dist/src/transcriptParse.js +261 -0
  189. package/dist/src/transcriptReader.js +209 -0
  190. package/{src → dist/src}/trimCapture.js +11 -11
  191. package/dist/src/uploadTypes.js +31 -0
  192. package/dist/src/urlPolicy.js +18 -0
  193. package/dist/src/usagePaths.js +15 -0
  194. package/dist/src/workspace/agentRunner.js +301 -0
  195. package/dist/src/workspace/atomicJson.js +31 -0
  196. package/dist/src/workspace/capture.js +139 -0
  197. package/dist/src/workspace/checkpointer.js +228 -0
  198. package/dist/src/workspace/environment.js +71 -0
  199. package/dist/src/workspace/lock.js +137 -0
  200. package/dist/src/workspace/mapping.js +128 -0
  201. package/dist/src/workspace/operations.js +271 -0
  202. package/dist/src/workspace/paths.js +15 -0
  203. package/dist/src/workspace/planner.js +292 -0
  204. package/dist/src/workspace/restore.js +363 -0
  205. package/dist/src/workspace/runtime.js +318 -0
  206. package/dist/src/workspace/schema.js +156 -0
  207. package/dist/src/workspace/store.js +464 -0
  208. package/dist/src/workspace/tmuxAdapter.js +704 -0
  209. package/dist/src/workspaceProtocol.js +158 -0
  210. package/package.json +30 -17
  211. package/hooks/handmux-notify.sh +0 -22
  212. package/hooks/handmux-write.cjs +0 -78
  213. package/public/assets/index-C3N7OkyP.css +0 -32
  214. package/public/assets/index-D1DWcC5u.js +0 -337
  215. package/src/.gitkeep +0 -0
  216. package/src/agents/claude.js +0 -159
  217. package/src/agents/codex.js +0 -168
  218. package/src/agents/index.js +0 -21
  219. package/src/agents/processIdentity.js +0 -72
  220. package/src/agents/scanUtils.js +0 -225
  221. package/src/asr/iflyConfig.js +0 -10
  222. package/src/asr/iflySign.js +0 -16
  223. package/src/auth.js +0 -30
  224. package/src/browser/bootstrap.js +0 -68
  225. package/src/browser/cookieProfiles.js +0 -403
  226. package/src/browser/coordinator.js +0 -47
  227. package/src/browser/hammerheadCompat.js +0 -86
  228. package/src/browser/hammerheadRedirectCompat.js +0 -38
  229. package/src/browser/manager.js +0 -556
  230. package/src/browser/originLabel.js +0 -17
  231. package/src/browser/profilePersistence.js +0 -241
  232. package/src/browser/publicProxy.js +0 -175
  233. package/src/browser/routes.js +0 -154
  234. package/src/browser/sessionStore.js +0 -78
  235. package/src/browser/siteVersion.js +0 -124
  236. package/src/browser/targetPolicy.js +0 -137
  237. package/src/browser/workerClient.js +0 -253
  238. package/src/browser/workerServer.js +0 -98
  239. package/src/captureBackground.js +0 -93
  240. package/src/claudeEvents.js +0 -399
  241. package/src/cli/claudeHooks.js +0 -206
  242. package/src/cli/cloudflared.js +0 -99
  243. package/src/cli/codexManaged.js +0 -77
  244. package/src/cli/drivers.js +0 -97
  245. package/src/cli/hookScaffold.js +0 -33
  246. package/src/cli/i18n/en.js +0 -385
  247. package/src/cli/i18n/zh.js +0 -384
  248. package/src/cli/legacyCodexHooks.js +0 -49
  249. package/src/cli/options.js +0 -211
  250. package/src/cli/probe.js +0 -16
  251. package/src/cli/pushCmd.js +0 -60
  252. package/src/cli/service.js +0 -126
  253. package/src/cli/setupModel.js +0 -154
  254. package/src/cli/setupWizard.js +0 -506
  255. package/src/cli/shortcutEditor.js +0 -288
  256. package/src/cli/sshTunnel.js +0 -12
  257. package/src/cli/state.js +0 -90
  258. package/src/cli/statusLine.js +0 -90
  259. package/src/cli/supervisor.js +0 -183
  260. package/src/cli/supervisorProcesses.js +0 -57
  261. package/src/cli/tmuxVersion.js +0 -49
  262. package/src/cli/tunnelClients.js +0 -62
  263. package/src/cli/workspaceCmd.js +0 -300
  264. package/src/codexAppServer.js +0 -1620
  265. package/src/codexDiff.js +0 -108
  266. package/src/codexTranscriptParse.js +0 -413
  267. package/src/codexUsageSnapshot.cjs +0 -129
  268. package/src/config.js +0 -9
  269. package/src/docs.js +0 -228
  270. package/src/git.js +0 -208
  271. package/src/httpApi.js +0 -72
  272. package/src/jsonStore.js +0 -24
  273. package/src/notifications.js +0 -68
  274. package/src/orphans.js +0 -152
  275. package/src/pendingPrompt.js +0 -163
  276. package/src/previewServer.js +0 -168
  277. package/src/previews.js +0 -116
  278. package/src/push.js +0 -183
  279. package/src/routes/codex.js +0 -473
  280. package/src/routes/files.js +0 -172
  281. package/src/routes/git.js +0 -57
  282. package/src/routes/notifications.js +0 -19
  283. package/src/routes/previews.js +0 -35
  284. package/src/routes/push.js +0 -119
  285. package/src/routes/sessions.js +0 -168
  286. package/src/routes/system.js +0 -132
  287. package/src/routes/terminal.js +0 -210
  288. package/src/routes/transcript.js +0 -171
  289. package/src/routes/workspace.js +0 -267
  290. package/src/server.js +0 -186
  291. package/src/shortcutConfig.js +0 -75
  292. package/src/terminalStream.js +0 -391
  293. package/src/tmux/format.js +0 -45
  294. package/src/transcriptParse.js +0 -180
  295. package/src/transcriptReader.js +0 -101
  296. package/src/uploadTypes.js +0 -30
  297. package/src/urlPolicy.js +0 -14
  298. package/src/usage.js +0 -260
  299. package/src/workspace/agentRunner.js +0 -188
  300. package/src/workspace/atomicJson.js +0 -32
  301. package/src/workspace/capture.js +0 -139
  302. package/src/workspace/checkpointer.js +0 -187
  303. package/src/workspace/environment.js +0 -75
  304. package/src/workspace/lock.js +0 -128
  305. package/src/workspace/mapping.js +0 -124
  306. package/src/workspace/operations.js +0 -252
  307. package/src/workspace/paths.js +0 -16
  308. package/src/workspace/planner.js +0 -247
  309. package/src/workspace/restore.js +0 -279
  310. package/src/workspace/runtime.js +0 -314
  311. package/src/workspace/schema.js +0 -136
  312. package/src/workspace/store.js +0 -382
  313. package/src/workspace/tmuxAdapter.js +0 -554
  314. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  315. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  316. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  317. /package/{public → dist/public}/icons/badge-96.png +0 -0
  318. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  319. /package/{public → dist/public}/icons/icon-192.png +0 -0
  320. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  321. /package/{public → dist/public}/icons/icon-512.png +0 -0
  322. /package/{public → dist/public}/icons/logo.svg +0 -0
  323. /package/{public → dist/public}/manifest.webmanifest +0 -0
  324. /package/{public → dist/public}/offline.html +0 -0
  325. /package/{public → dist/public}/sw.js +0 -0
  326. /package/{src → dist/src}/browser/protocol.js +0 -0
  327. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -0,0 +1,156 @@
1
+ import crypto from 'node:crypto';
2
+ export const WORKSPACE_SCHEMA_VERSION = 1;
3
+ const byId = (a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
4
+ const byLink = (a, b) => (a.index - b.index || (a.windowId < b.windowId ? -1 : a.windowId > b.windowId ? 1 : 0));
5
+ const hash = (value) => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
6
+ function requireObject(value, label) {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value))
8
+ throw new Error(`${label} must be an object`);
9
+ }
10
+ function requireArray(value, label) {
11
+ if (!Array.isArray(value))
12
+ throw new Error(`${label} must be an array`);
13
+ }
14
+ function requireString(value, label) {
15
+ if (typeof value !== 'string' || !value)
16
+ throw new Error(`${label} must be a non-empty string`);
17
+ }
18
+ function requireIndex(value, label) {
19
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
20
+ throw new Error(`${label} must be a non-negative integer`);
21
+ }
22
+ }
23
+ function validateShape(input) {
24
+ requireString(input.capturedAt, 'capturedAt');
25
+ requireObject(input.environment, 'environment');
26
+ for (const field of ['id', 'bootIdentity'])
27
+ requireString(input.environment[field], `environment.${field}`);
28
+ if (input.environment.tmuxServerId !== null)
29
+ requireString(input.environment.tmuxServerId, 'environment.tmuxServerId');
30
+ requireString(input.tmuxVersion, 'tmuxVersion');
31
+ requireArray(input.sessions, 'sessions');
32
+ requireArray(input.windows, 'windows');
33
+ const empty = input.sessions.length === 0 && input.windows.length === 0;
34
+ if ((input.sessions.length === 0) !== (input.windows.length === 0))
35
+ throw new Error('sessions and windows must both be empty or non-empty');
36
+ if (empty) {
37
+ if (input.active !== null)
38
+ throw new Error('active must be null for an empty workspace');
39
+ }
40
+ else {
41
+ requireString(input.environment.tmuxServerId, 'environment.tmuxServerId');
42
+ requireObject(input.active, 'active');
43
+ for (const field of ['sessionId', 'windowId', 'paneId'])
44
+ requireString(input.active[field], `active.${field}`);
45
+ }
46
+ for (const [index, session] of input.sessions.entries()) {
47
+ const label = `sessions[${index}]`;
48
+ requireObject(session, label);
49
+ for (const field of ['id', 'runtimeId', 'name', 'activeWindowId'])
50
+ requireString(session[field], `${label}.${field}`);
51
+ if (session.windowIds !== undefined)
52
+ throw new Error(`${label}.windowIds is unsupported; use windowLinks`);
53
+ requireArray(session.windowLinks, `${label}.windowLinks`);
54
+ for (const [linkIndex, link] of session.windowLinks.entries()) {
55
+ const linkLabel = `${label}.windowLinks[${linkIndex}]`;
56
+ requireObject(link, linkLabel);
57
+ requireString(link.windowId, `${linkLabel}.windowId`);
58
+ requireIndex(link.index, `${linkLabel}.index`);
59
+ }
60
+ }
61
+ for (const [windowIndex, window] of input.windows.entries()) {
62
+ const label = `windows[${windowIndex}]`;
63
+ requireObject(window, label);
64
+ for (const field of ['id', 'runtimeId', 'name', 'layout', 'activePaneId'])
65
+ requireString(window[field], `${label}.${field}`);
66
+ requireIndex(window.index, `${label}.index`);
67
+ requireArray(window.panes, `${label}.panes`);
68
+ for (const [paneIndex, pane] of window.panes.entries()) {
69
+ const paneLabel = `${label}.panes[${paneIndex}]`;
70
+ requireObject(pane, paneLabel);
71
+ for (const field of ['id', 'runtimeId', 'cwd'])
72
+ requireString(pane[field], `${paneLabel}.${field}`);
73
+ requireIndex(pane.index, `${paneLabel}.index`);
74
+ if (pane.agent !== undefined && pane.agent !== null)
75
+ requireObject(pane.agent, `${paneLabel}.agent`);
76
+ }
77
+ }
78
+ }
79
+ function validateReferences(input, sessions, windows) {
80
+ if (sessions.length === 0)
81
+ return;
82
+ const windowsById = new Map(windows.map((window) => [window.id, window]));
83
+ const sessionsById = new Map(sessions.map((session) => [session.id, session]));
84
+ const active = input.active;
85
+ if (!active)
86
+ throw new Error('active must be an object');
87
+ for (const session of sessions) {
88
+ const windowIds = session.windowLinks.map((link) => link.windowId);
89
+ const windowIndexes = session.windowLinks.map((link) => link.index);
90
+ if (new Set(windowIds).size !== windowIds.length)
91
+ throw new Error(`duplicate window link in session ${session.id}`);
92
+ if (new Set(windowIndexes).size !== windowIndexes.length)
93
+ throw new Error(`duplicate window link index in session ${session.id}`);
94
+ for (const { windowId } of session.windowLinks) {
95
+ if (!windowsById.has(windowId))
96
+ throw new Error(`dangling windowLinks reference ${windowId}`);
97
+ }
98
+ if (!windowIds.includes(session.activeWindowId))
99
+ throw new Error(`dangling activeWindowId reference ${session.activeWindowId}`);
100
+ }
101
+ for (const window of windows) {
102
+ if (!window.panes.some((pane) => pane.id === window.activePaneId))
103
+ throw new Error(`dangling activePaneId reference ${window.activePaneId}`);
104
+ }
105
+ const activeSession = sessionsById.get(active.sessionId);
106
+ if (!activeSession)
107
+ throw new Error(`dangling active.sessionId reference ${active.sessionId}`);
108
+ if (!activeSession.windowLinks.some((link) => link.windowId === active.windowId))
109
+ throw new Error(`dangling active.windowId reference ${active.windowId}`);
110
+ const activeWindow = windowsById.get(active.windowId);
111
+ if (!activeWindow?.panes.some((pane) => pane.id === active.paneId))
112
+ throw new Error(`dangling active.paneId reference ${active.paneId}`);
113
+ }
114
+ export function canonicalizeSnapshot(input) {
115
+ requireObject(input, 'workspace');
116
+ if (input.schemaVersion !== WORKSPACE_SCHEMA_VERSION)
117
+ throw new Error('unsupported workspace schema');
118
+ validateShape(input);
119
+ const sessions = input.sessions.map((s) => ({ ...s, windowLinks: s.windowLinks.map((link) => ({ ...link })).sort(byLink) })).sort(byId);
120
+ const windows = input.windows.map((w) => ({ ...w, panes: [...w.panes].sort(byId) })).sort(byId);
121
+ const identitySets = [
122
+ ['session', sessions.map((x) => x.id)],
123
+ ['window', windows.map((x) => x.id)],
124
+ ['pane', windows.flatMap((w) => w.panes.map((p) => p.id))],
125
+ ];
126
+ for (const [label, ids] of identitySets) {
127
+ if (new Set(ids).size !== ids.length)
128
+ throw new Error(`duplicate ${label} id`);
129
+ }
130
+ validateReferences(input, sessions, windows);
131
+ return { ...input, sessions, windows };
132
+ }
133
+ export function fingerprintSnapshot(input) {
134
+ const { capturedAt, revision, payloadHash, ...payload } = canonicalizeSnapshot(input);
135
+ return hash(payload);
136
+ }
137
+ export function sealPayload(input) {
138
+ const payload = canonicalizeSnapshot(input);
139
+ return { ...payload, payloadHash: fingerprintSnapshot(payload) };
140
+ }
141
+ export function validateCheckpoint(input) {
142
+ try {
143
+ requireObject(input, 'checkpoint');
144
+ requireString(input.id, 'checkpoint id');
145
+ requireString(input.archivedAt, 'checkpoint archivedAt');
146
+ if (typeof input.payloadHash !== 'string' || !/^[0-9a-f]{64}$/.test(input.payloadHash))
147
+ throw new Error('invalid checkpoint payloadHash');
148
+ const expected = fingerprintSnapshot(input);
149
+ if (input.payloadHash !== expected)
150
+ throw new Error('checkpoint hash mismatch');
151
+ return { ok: true, value: canonicalizeSnapshot(input) };
152
+ }
153
+ catch (error) {
154
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
155
+ }
156
+ }
@@ -0,0 +1,464 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { ensurePrivateDir, writeJsonAtomic } from './atomicJson.js';
4
+ import { workspacePaths } from './paths.js';
5
+ import { canonicalizeSnapshot, fingerprintSnapshot, sealPayload, validateCheckpoint, } from './schema.js';
6
+ import { validateRecoveryMapping } from './mapping.js';
7
+ const DAY_MS = 24 * 60 * 60 * 1000;
8
+ const RECOVERY_WINDOW_MS = 60 * 60 * 1000;
9
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
10
+ const recordOf = (value) => (value && typeof value === 'object' && !Array.isArray(value)
11
+ ? value : null);
12
+ function hasErrorCode(error, code) {
13
+ return recordOf(error)?.code === code;
14
+ }
15
+ function requireSafeId(id) {
16
+ if (typeof id !== 'string' || !SAFE_ID.test(id) || id === '.' || id === '..')
17
+ throw new Error('workspace id must be a safe filename');
18
+ return id;
19
+ }
20
+ function errorMessage(error) {
21
+ return error instanceof Error ? error.message : String(error);
22
+ }
23
+ function projectObject(value, fields) {
24
+ const record = recordOf(value);
25
+ if (!record)
26
+ return value;
27
+ return Object.fromEntries(fields.map((field) => [field, record[field]]));
28
+ }
29
+ function projectSnapshot(input) {
30
+ const record = recordOf(input);
31
+ if (!record)
32
+ return input;
33
+ const environment = projectObject(record.environment, ['id', 'bootIdentity', 'tmuxServerId']);
34
+ const active = record.active === null ? null : projectObject(record.active, ['sessionId', 'windowId', 'paneId']);
35
+ const sessions = Array.isArray(record.sessions) ? record.sessions.map((session) => {
36
+ const projected = projectObject(session, ['id', 'runtimeId', 'name', 'windowLinks', 'activeWindowId']);
37
+ const projectedRecord = recordOf(projected);
38
+ if (!projectedRecord || !Array.isArray(projectedRecord.windowLinks))
39
+ return projected;
40
+ projectedRecord.windowLinks = projectedRecord.windowLinks.map((link) => projectObject(link, ['windowId', 'index']));
41
+ return projectedRecord;
42
+ }) : record.sessions;
43
+ const windows = Array.isArray(record.windows) ? record.windows.map((window) => {
44
+ const projected = projectObject(window, ['id', 'runtimeId', 'name', 'index', 'layout', 'activePaneId', 'panes']);
45
+ const projectedRecord = recordOf(projected);
46
+ if (!projectedRecord || !Array.isArray(projectedRecord.panes))
47
+ return projected;
48
+ projectedRecord.panes = projectedRecord.panes.map((pane) => {
49
+ const projectedPane = projectObject(pane, ['id', 'runtimeId', 'index', 'cwd', 'agent']);
50
+ const paneRecord = recordOf(projectedPane);
51
+ if (!paneRecord)
52
+ return projectedPane;
53
+ if (paneRecord.agent !== null && paneRecord.agent !== undefined) {
54
+ paneRecord.agent = projectObject(paneRecord.agent, ['id', 'sessionId', 'transcriptPath']);
55
+ }
56
+ return paneRecord;
57
+ });
58
+ return projectedRecord;
59
+ }) : record.windows;
60
+ return {
61
+ schemaVersion: record.schemaVersion,
62
+ capturedAt: record.capturedAt,
63
+ environment,
64
+ tmuxVersion: record.tmuxVersion,
65
+ active,
66
+ sessions,
67
+ windows,
68
+ };
69
+ }
70
+ function validateLive(value) {
71
+ try {
72
+ const record = recordOf(value);
73
+ if (!record || !Number.isInteger(record.revision) || record.revision < 1)
74
+ throw new Error('invalid live revision');
75
+ if (typeof record.payloadHash !== 'string' || !/^[0-9a-f]{64}$/.test(record.payloadHash))
76
+ throw new Error('invalid live payloadHash');
77
+ if (fingerprintSnapshot(value) !== record.payloadHash)
78
+ throw new Error('live hash mismatch');
79
+ const canonical = canonicalizeSnapshot(value);
80
+ return {
81
+ ok: true,
82
+ value: {
83
+ ...canonical,
84
+ revision: record.revision,
85
+ payloadHash: record.payloadHash,
86
+ },
87
+ };
88
+ }
89
+ catch (error) {
90
+ return { ok: false, error: errorMessage(error) };
91
+ }
92
+ }
93
+ async function readJson(file, fs) {
94
+ try {
95
+ return { status: 'ok', value: JSON.parse(await fs.readFile(file, 'utf8')) };
96
+ }
97
+ catch (error) {
98
+ if (hasErrorCode(error, 'ENOENT'))
99
+ return { status: 'missing' };
100
+ return { status: 'corrupt', error: errorMessage(error) };
101
+ }
102
+ }
103
+ async function readValidated(file, fs, validate) {
104
+ const result = await readJson(file, fs);
105
+ if (result.status !== 'ok')
106
+ return result;
107
+ try {
108
+ const value = validate(result.value);
109
+ return { status: 'ok', value };
110
+ }
111
+ catch (error) {
112
+ return { status: 'corrupt', error: errorMessage(error) };
113
+ }
114
+ }
115
+ function validateRecovery(value, checkpointId) {
116
+ const record = recordOf(value);
117
+ if (!record)
118
+ throw new Error('invalid recovery state');
119
+ if (record.checkpointId !== checkpointId)
120
+ throw new Error('recovery checkpoint id mismatch');
121
+ for (const field of ['detectedAt', 'expiresAt']) {
122
+ const entry = record[field];
123
+ if (typeof entry !== 'string' || Number.isNaN(Date.parse(entry)))
124
+ throw new Error(`invalid recovery ${field}`);
125
+ }
126
+ for (const field of ['initialSessionIds', 'pendingSessionIds']) {
127
+ const entry = record[field];
128
+ if (!Array.isArray(entry) || entry.some((id) => typeof id !== 'string' || !id))
129
+ throw new Error(`invalid recovery ${field}`);
130
+ if (new Set(entry).size !== entry.length)
131
+ throw new Error(`duplicate recovery ${field}`);
132
+ }
133
+ const initialSessionIds = record.initialSessionIds;
134
+ const pendingSessionIds = record.pendingSessionIds;
135
+ const initial = new Set(initialSessionIds);
136
+ if (pendingSessionIds.some((id) => !initial.has(id)))
137
+ throw new Error('recovery pending ids must be initial ids');
138
+ if (record.resolvedAt !== null && (typeof record.resolvedAt !== 'string' || Number.isNaN(Date.parse(record.resolvedAt))))
139
+ throw new Error('invalid recovery resolvedAt');
140
+ if (record.resolvedAt !== null && pendingSessionIds.length > 0)
141
+ throw new Error('resolved recovery cannot contain pending session ids');
142
+ let mapping = null;
143
+ if (record.mapping !== null) {
144
+ mapping = validateRecoveryMapping(record.mapping, checkpointId);
145
+ }
146
+ return {
147
+ checkpointId,
148
+ detectedAt: record.detectedAt,
149
+ expiresAt: record.expiresAt,
150
+ initialSessionIds,
151
+ pendingSessionIds,
152
+ resolvedAt: record.resolvedAt,
153
+ mapping,
154
+ };
155
+ }
156
+ export function selectRetainedCheckpoints(rows, now, latestId) {
157
+ const valid = rows
158
+ .filter((row) => (row.status === 'ok' && Number.isFinite(Date.parse(row.value.archivedAt))))
159
+ .sort((a, b) => Date.parse(b.value.archivedAt) - Date.parse(a.value.archivedAt));
160
+ const retained = [];
161
+ const retainedIds = new Set();
162
+ const keep = (row) => {
163
+ if (!row || retainedIds.has(row.id))
164
+ return;
165
+ retained.push(row);
166
+ retainedIds.add(row.id);
167
+ };
168
+ for (const row of valid) {
169
+ if (Date.parse(row.value.archivedAt) >= now - DAY_MS)
170
+ keep(row);
171
+ }
172
+ for (const row of valid) {
173
+ if (retained.length >= 10)
174
+ break;
175
+ keep(row);
176
+ }
177
+ keep(rows.find((row) => row.id === latestId));
178
+ return retained;
179
+ }
180
+ export function createWorkspaceStore({ home, now = Date.now, fs = fsp }) {
181
+ const paths = workspacePaths(home);
182
+ async function ensureDirectories() {
183
+ for (const dir of [path.dirname(paths.root), paths.root, paths.liveDir, paths.checkpointsDir, paths.recoveryDir, paths.operationsDir]) {
184
+ await ensurePrivateDir(dir, { fs });
185
+ }
186
+ }
187
+ async function readLiveCopy(file) {
188
+ const result = await readJson(file, fs);
189
+ if (result.status !== 'ok')
190
+ return result;
191
+ const validated = validateLive(result.value);
192
+ return validated.ok ? { status: 'ok', value: validated.value } : { status: 'corrupt', error: validated.error };
193
+ }
194
+ async function readLive() {
195
+ const copies = await Promise.all([paths.liveCurrent, paths.liveMirror].map(readLiveCopy));
196
+ const valid = copies
197
+ .map((result, index) => ({ ...result, index }))
198
+ .filter((result) => result.status === 'ok')
199
+ .sort((a, b) => b.value.revision - a.value.revision || a.index - b.index);
200
+ if (valid.length === 0) {
201
+ if (copies.every((copy) => copy.status === 'missing'))
202
+ return { status: 'empty' };
203
+ return { status: 'corrupt', errors: copies.map((copy) => copy.status === 'corrupt' ? copy.error : copy.status) };
204
+ }
205
+ const chosenCopy = valid[0];
206
+ if (!chosenCopy)
207
+ return { status: 'corrupt', errors: ['missing valid live copy'] };
208
+ const chosen = chosenCopy.value;
209
+ let repaired = false;
210
+ await ensureDirectories();
211
+ for (let index = 0; index < copies.length; index += 1) {
212
+ const copy = copies[index];
213
+ if (copy?.status === 'ok' && copy.value.revision === chosen.revision && copy.value.payloadHash === chosen.payloadHash)
214
+ continue;
215
+ await writeJsonAtomic(index === 0 ? paths.liveCurrent : paths.liveMirror, chosen, { fs });
216
+ repaired = true;
217
+ }
218
+ return { status: 'ok', value: chosen, repaired };
219
+ }
220
+ async function writeLive(snapshot) {
221
+ const payload = canonicalizeSnapshot(projectSnapshot(snapshot));
222
+ await ensureDirectories();
223
+ const copies = await Promise.all([paths.liveCurrent, paths.liveMirror].map(readLiveCopy));
224
+ const revision = Math.max(0, ...copies
225
+ .filter((copy) => copy.status === 'ok')
226
+ .map((copy) => copy.value.revision)) + 1;
227
+ const value = { ...sealPayload({ ...payload, revision }), revision };
228
+ await writeJsonAtomic(paths.liveCurrent, value, { fs });
229
+ await writeJsonAtomic(paths.liveMirror, value, { fs });
230
+ return value;
231
+ }
232
+ async function readCheckpoint(id) {
233
+ requireSafeId(id);
234
+ const result = await readJson(path.join(paths.checkpointsDir, `${id}.json`), fs);
235
+ if (result.status !== 'ok')
236
+ return result;
237
+ const validation = validateCheckpoint(result.value);
238
+ if (!validation.ok)
239
+ return { status: 'corrupt', error: validation.error };
240
+ if (validation.value.id !== id)
241
+ return { status: 'corrupt', error: 'checkpoint id does not match filename' };
242
+ if (validation.value.environment.id !== id)
243
+ return { status: 'corrupt', error: 'checkpoint environment id does not match filename' };
244
+ return { status: 'ok', value: validation.value };
245
+ }
246
+ async function listCheckpoints() {
247
+ let names;
248
+ try {
249
+ names = await fs.readdir(paths.checkpointsDir);
250
+ }
251
+ catch (error) {
252
+ if (hasErrorCode(error, 'ENOENT'))
253
+ return [];
254
+ throw error;
255
+ }
256
+ const rows = await Promise.all(names.filter((name) => name.endsWith('.json')).map(async (name) => {
257
+ const id = name.slice(0, -'.json'.length);
258
+ if (!SAFE_ID.test(id) || id === '.' || id === '..') {
259
+ return { status: 'corrupt', id, error: 'checkpoint filename is not a safe id' };
260
+ }
261
+ const result = await readCheckpoint(id);
262
+ if (result.status === 'ok')
263
+ return { status: 'ok', id, value: result.value };
264
+ return { status: 'corrupt', id, error: result.status === 'corrupt' ? result.error : result.status };
265
+ }));
266
+ return rows.sort((a, b) => {
267
+ if (a.status === 'ok' && b.status === 'ok')
268
+ return Date.parse(b.value.archivedAt) - Date.parse(a.value.archivedAt);
269
+ if (a.status === 'ok')
270
+ return -1;
271
+ if (b.status === 'ok')
272
+ return 1;
273
+ return a.id.localeCompare(b.id);
274
+ });
275
+ }
276
+ async function readLatestCheckpoint() {
277
+ const pointer = await readJson(paths.latest, fs);
278
+ let warning;
279
+ if (pointer.status === 'ok') {
280
+ try {
281
+ const pointerRecord = recordOf(pointer.value);
282
+ const checkpointId = requireSafeId(pointerRecord?.checkpointId);
283
+ if (typeof pointerRecord?.payloadHash !== 'string')
284
+ throw new Error('invalid latest payloadHash');
285
+ const pointed = await readCheckpoint(checkpointId);
286
+ if (pointed.status === 'ok' && pointed.value.payloadHash === pointerRecord.payloadHash)
287
+ return pointed;
288
+ warning = `latest checkpoint is ${pointed.status === 'ok' ? 'hash-mismatched' : pointed.status}`;
289
+ }
290
+ catch (error) {
291
+ warning = `latest pointer is corrupt: ${errorMessage(error)}`;
292
+ }
293
+ }
294
+ else if (pointer.status === 'corrupt') {
295
+ warning = `latest pointer is corrupt: ${pointer.error}`;
296
+ }
297
+ const rows = await listCheckpoints();
298
+ const fallback = rows.find((row) => row.status === 'ok');
299
+ if (fallback)
300
+ return { status: 'ok', value: fallback.value, ...(warning ? { warning } : {}) };
301
+ if (pointer.status === 'missing' && rows.length === 0)
302
+ return { status: 'missing' };
303
+ return { status: 'corrupt', error: warning ?? 'no valid checkpoint' };
304
+ }
305
+ async function readRecovery(checkpointId) {
306
+ requireSafeId(checkpointId);
307
+ return readValidated(path.join(paths.recoveryDir, `${checkpointId}.json`), fs, (value) => validateRecovery(value, checkpointId));
308
+ }
309
+ async function createRecovery(checkpointId, detectedAt) {
310
+ requireSafeId(checkpointId);
311
+ const existing = await readRecovery(checkpointId);
312
+ if (existing.status === 'ok')
313
+ return existing;
314
+ if (existing.status === 'corrupt')
315
+ throw new Error(existing.error);
316
+ const checkpoint = await readCheckpoint(checkpointId);
317
+ if (checkpoint.status !== 'ok')
318
+ return checkpoint;
319
+ if (typeof detectedAt !== 'string' || Number.isNaN(Date.parse(detectedAt)))
320
+ throw new Error('detectedAt must be an ISO timestamp');
321
+ const initialSessionIds = checkpoint.value.sessions.map((session) => session.id);
322
+ const value = {
323
+ checkpointId,
324
+ detectedAt,
325
+ expiresAt: new Date(Date.parse(detectedAt) + RECOVERY_WINDOW_MS).toISOString(),
326
+ initialSessionIds,
327
+ pendingSessionIds: [...initialSessionIds],
328
+ resolvedAt: null,
329
+ mapping: null,
330
+ };
331
+ await ensureDirectories();
332
+ await writeJsonAtomic(path.join(paths.recoveryDir, `${checkpointId}.json`), value, { fs });
333
+ return { status: 'ok', value };
334
+ }
335
+ async function resolveSessions(checkpointId, ids) {
336
+ if (!Array.isArray(ids))
337
+ throw new Error('resolved session ids must be an array');
338
+ const recovery = await readRecovery(checkpointId);
339
+ if (recovery.status !== 'ok')
340
+ return recovery;
341
+ if (recovery.value.resolvedAt !== null)
342
+ return recovery;
343
+ const resolved = new Set(ids.filter((id) => typeof id === 'string'));
344
+ const pendingSessionIds = recovery.value.pendingSessionIds.filter((id) => !resolved.has(id));
345
+ if (pendingSessionIds.length === recovery.value.pendingSessionIds.length && pendingSessionIds.length > 0)
346
+ return recovery;
347
+ const value = {
348
+ ...recovery.value,
349
+ pendingSessionIds,
350
+ resolvedAt: pendingSessionIds.length === 0 ? new Date(now()).toISOString() : null,
351
+ };
352
+ await writeJsonAtomic(path.join(paths.recoveryDir, `${checkpointId}.json`), value, { fs });
353
+ return { status: 'ok', value };
354
+ }
355
+ async function mergeRecoveryMapping(checkpointId, mapping) {
356
+ const recovery = await readRecovery(checkpointId);
357
+ if (recovery.status !== 'ok')
358
+ return recovery;
359
+ const validatedMapping = validateRecoveryMapping(mapping, checkpointId);
360
+ const value = validateRecovery({ ...recovery.value, mapping: validatedMapping }, checkpointId);
361
+ await writeJsonAtomic(path.join(paths.recoveryDir, `${checkpointId}.json`), value, { fs });
362
+ return { status: 'ok', value };
363
+ }
364
+ async function writeOperation(operation) {
365
+ requireSafeId(operation?.id);
366
+ await ensureDirectories();
367
+ await writeJsonAtomic(path.join(paths.operationsDir, `${operation.id}.json`), operation, { fs });
368
+ return operation;
369
+ }
370
+ async function readOperation(id) {
371
+ requireSafeId(id);
372
+ const result = await readJson(path.join(paths.operationsDir, `${id}.json`), fs);
373
+ return result.status === 'ok'
374
+ ? { status: 'ok', value: result.value }
375
+ : result;
376
+ }
377
+ async function listOperations() {
378
+ let names;
379
+ try {
380
+ names = await fs.readdir(paths.operationsDir);
381
+ }
382
+ catch (error) {
383
+ if (hasErrorCode(error, 'ENOENT'))
384
+ return [];
385
+ throw error;
386
+ }
387
+ return Promise.all(names.filter((name) => name.endsWith('.json')).sort().map(async (name) => {
388
+ const id = name.slice(0, -'.json'.length);
389
+ if (!SAFE_ID.test(id) || id === '.' || id === '..')
390
+ return { status: 'corrupt', id, error: 'operation filename is not a safe id' };
391
+ const result = await readOperation(id);
392
+ if (result.status !== 'ok')
393
+ return { ...result, id };
394
+ const value = recordOf(result.value);
395
+ if (value?.id !== id)
396
+ return { status: 'corrupt', id, error: 'operation id does not match filename' };
397
+ return { status: 'ok', id, value: value };
398
+ }));
399
+ }
400
+ async function prune() {
401
+ const pointer = await readJson(paths.latest, fs);
402
+ const pointerRecord = pointer.status === 'ok' ? recordOf(pointer.value) : null;
403
+ const latestId = typeof pointerRecord?.checkpointId === 'string' ? pointerRecord.checkpointId : undefined;
404
+ const rows = await listCheckpoints();
405
+ const retained = new Set(selectRetainedCheckpoints(rows, now(), latestId).map((row) => row.id));
406
+ const canDeleteCorrupt = pointer.status === 'ok' && SAFE_ID.test(latestId ?? '');
407
+ await Promise.all(rows.map(async (row) => {
408
+ if (retained.has(row.id) || (row.status === 'corrupt' && !canDeleteCorrupt))
409
+ return;
410
+ await fs.unlink(path.join(paths.checkpointsDir, `${row.id}.json`));
411
+ }));
412
+ return rows.filter((row) => retained.has(row.id));
413
+ }
414
+ async function archiveEnvironment({ endedReason, detectedAt }) {
415
+ if (typeof endedReason !== 'string' || !endedReason)
416
+ throw new Error('endedReason must be a non-empty string');
417
+ if (typeof detectedAt !== 'string' || Number.isNaN(Date.parse(detectedAt)))
418
+ throw new Error('detectedAt must be an ISO timestamp');
419
+ const live = await readLive();
420
+ if (live.status !== 'ok')
421
+ return live;
422
+ if (live.value.sessions.length === 0)
423
+ return { status: 'empty' };
424
+ const id = requireSafeId(live.value.environment.id);
425
+ await ensureDirectories();
426
+ let checkpoint = await readCheckpoint(id);
427
+ if (checkpoint.status !== 'ok') {
428
+ const { payloadHash: ignoredHash, revision: ignoredRevision, ...unsealed } = live.value;
429
+ const sealed = sealPayload({
430
+ ...unsealed,
431
+ id,
432
+ archivedAt: new Date(now()).toISOString(),
433
+ environment: { ...unsealed.environment, endedReason },
434
+ });
435
+ const validation = validateCheckpoint(sealed);
436
+ if (!validation.ok)
437
+ throw new Error(validation.error);
438
+ const value = validation.value;
439
+ await writeJsonAtomic(path.join(paths.checkpointsDir, `${id}.json`), value, { fs });
440
+ checkpoint = { status: 'ok', value };
441
+ }
442
+ await writeJsonAtomic(paths.latest, { checkpointId: id, payloadHash: checkpoint.value.payloadHash }, { fs });
443
+ await createRecovery(id, detectedAt);
444
+ await prune().catch(() => { });
445
+ return checkpoint;
446
+ }
447
+ return {
448
+ paths,
449
+ readLive,
450
+ writeLive,
451
+ archiveEnvironment,
452
+ listCheckpoints,
453
+ readCheckpoint,
454
+ readLatestCheckpoint,
455
+ readRecovery,
456
+ createRecovery,
457
+ resolveSessions,
458
+ mergeRecoveryMapping,
459
+ writeOperation,
460
+ readOperation,
461
+ listOperations,
462
+ prune,
463
+ };
464
+ }