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,271 @@
1
+ import crypto from 'node:crypto';
2
+ const ACTIVE = new Set(['pending', 'running']);
3
+ const TERMINAL = new Set(['succeeded', 'partial', 'failed']);
4
+ const recordOf = (value) => (value && typeof value === 'object' && !Array.isArray(value)
5
+ ? value : null);
6
+ function iso(now) {
7
+ return new Date(typeof now === 'function' ? now() : now).toISOString();
8
+ }
9
+ function errorMessage(error) {
10
+ return error instanceof Error ? error.message : String(error);
11
+ }
12
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ const unavailableLock = async () => { throw new Error('workspace operation lock is unavailable'); };
14
+ const INACTIVE_OPERATION = 'WORKSPACE_OPERATION_INACTIVE';
15
+ export function normalizeRestoreRequest(request = {}) {
16
+ const record = recordOf(request) ?? {};
17
+ const checkpointId = typeof record.checkpointId === 'string' && record.checkpointId ? record.checkpointId : 'latest';
18
+ const rawSessions = Array.isArray(record.sessions) ? record.sessions : record.sessions ? [record.sessions] : [];
19
+ const sessions = [...new Set(rawSessions.filter((name) => typeof name === 'string' && Boolean(name)))].sort();
20
+ return { checkpointId, sessions, historical: record.historical === true };
21
+ }
22
+ export function restoreRequestHash(request) {
23
+ return crypto.createHash('sha256').update(JSON.stringify(normalizeRestoreRequest(request))).digest('hex');
24
+ }
25
+ export function createOperationManager({ store, now = Date.now, randomUUID = crypto.randomUUID, pid = process.pid, tryAcquireOperationLock = unavailableLock, pendingGraceMs = 250, wait = delay, }) {
26
+ const values = new Map();
27
+ const activeByHash = new Map();
28
+ const localIds = new Set();
29
+ async function persist(operation) {
30
+ if (localIds.has(operation.id))
31
+ values.set(operation.id, operation);
32
+ await store.writeOperation(operation);
33
+ return operation;
34
+ }
35
+ async function readFresh(id) {
36
+ const result = await store.readOperation(id);
37
+ return result.status === 'ok' && result.value ? result.value : null;
38
+ }
39
+ async function assessExternal(operation, { allowPendingGrace = true } = {}) {
40
+ let current = await readFresh(operation.id);
41
+ if (!current)
42
+ return { active: false, operation: null };
43
+ if (!ACTIVE.has(current?.status))
44
+ return { active: false, operation: current };
45
+ if (current.status === 'pending' && allowPendingGrace) {
46
+ const createdAt = Date.parse(current.createdAt);
47
+ const age = Number.isFinite(createdAt) ? Math.max(0, now() - createdAt) : Number.POSITIVE_INFINITY;
48
+ const remaining = pendingGraceMs - age;
49
+ if (remaining > 0) {
50
+ await wait(remaining);
51
+ current = await readFresh(current.id);
52
+ if (!current || !ACTIVE.has(current.status))
53
+ return { active: false, operation: current };
54
+ }
55
+ }
56
+ let handle;
57
+ try {
58
+ handle = await tryAcquireOperationLock({ operationId: `assess-${current.id}` });
59
+ }
60
+ catch {
61
+ return { active: true, operation: current };
62
+ }
63
+ if (!handle)
64
+ return { active: true, operation: current };
65
+ try {
66
+ current = await readFresh(current.id);
67
+ if (!current || !ACTIVE.has(current.status))
68
+ return { active: false, operation: current };
69
+ const interrupted = {
70
+ ...current,
71
+ status: 'interrupted',
72
+ error: 'restore interrupted by process restart; retry the restore',
73
+ completedAt: iso(now),
74
+ updatedAt: iso(now),
75
+ };
76
+ await store.writeOperation(interrupted);
77
+ return { active: false, operation: interrupted, interrupted: true };
78
+ }
79
+ finally {
80
+ await handle.release();
81
+ }
82
+ }
83
+ async function findExternalActive(requestHash) {
84
+ const rows = typeof store.listOperations === 'function' ? await store.listOperations() : [];
85
+ for (const row of rows) {
86
+ const value = row.value;
87
+ if (row.status !== 'ok' || !value || localIds.has(value.id) || !ACTIVE.has(value.status))
88
+ continue;
89
+ if (value.requestHash !== requestHash || value.requestHash !== restoreRequestHash(value.request))
90
+ continue;
91
+ const assessed = await assessExternal(value);
92
+ if (assessed.active)
93
+ return assessed.operation;
94
+ }
95
+ return null;
96
+ }
97
+ async function execute(operation, runner, { deferRunning = false } = {}) {
98
+ let current = operation;
99
+ let running = false;
100
+ let terminalStatus = null;
101
+ const onRunning = async () => {
102
+ if (running)
103
+ return current;
104
+ const persisted = await readFresh(current.id);
105
+ if (!persisted || !ACTIVE.has(persisted.status)) {
106
+ if (persisted) {
107
+ current = persisted;
108
+ if (localIds.has(current.id))
109
+ values.set(current.id, current);
110
+ }
111
+ else {
112
+ localIds.delete(current.id);
113
+ values.delete(current.id);
114
+ }
115
+ const error = new Error(`restore operation ${current.id} is no longer active`);
116
+ error.code = INACTIVE_OPERATION;
117
+ error.operation = persisted;
118
+ throw error;
119
+ }
120
+ current = persisted;
121
+ current = await persist({
122
+ ...current,
123
+ status: 'running',
124
+ startedAt: iso(now),
125
+ updatedAt: iso(now),
126
+ });
127
+ running = true;
128
+ return current;
129
+ };
130
+ const onTerminal = async (value) => {
131
+ await onRunning();
132
+ const result = value instanceof Error
133
+ ? { status: 'failed', error: errorMessage(value) }
134
+ : recordOf(value) ?? { status: 'failed', error: errorMessage(value) };
135
+ const requestedStatus = typeof result.status === 'string' && TERMINAL.has(result.status) ? result.status : 'failed';
136
+ if (terminalStatus && requestedStatus !== terminalStatus)
137
+ return current;
138
+ const status = terminalStatus || requestedStatus;
139
+ const candidate = {
140
+ ...current,
141
+ ...result,
142
+ status,
143
+ progress: {
144
+ completed: Array.isArray(result.results) ? result.results.length : current.progress.completed,
145
+ total: current.progress.total,
146
+ },
147
+ completedAt: current.completedAt || iso(now),
148
+ };
149
+ if (terminalStatus && JSON.stringify(candidate) === JSON.stringify(current))
150
+ return current;
151
+ current = await persist({ ...candidate, updatedAt: iso(now) });
152
+ terminalStatus = status;
153
+ return current;
154
+ };
155
+ try {
156
+ if (!deferRunning)
157
+ await onRunning();
158
+ const result = await runner({
159
+ operationId: current.id,
160
+ request: current.request,
161
+ onRunning,
162
+ onTerminal,
163
+ onProgress: async ({ completed, total, result: row }) => {
164
+ await onRunning();
165
+ const results = row ? [...(current.results || []), row] : (current.results || []);
166
+ current = await persist({
167
+ ...current,
168
+ progress: { completed, total },
169
+ results,
170
+ updatedAt: iso(now),
171
+ });
172
+ },
173
+ });
174
+ await onTerminal(result);
175
+ }
176
+ catch (error) {
177
+ if (error && typeof error === 'object' && 'code' in error && error.code === INACTIVE_OPERATION) {
178
+ const inactive = error;
179
+ if (inactive.operation)
180
+ current = inactive.operation;
181
+ }
182
+ else {
183
+ try {
184
+ await onTerminal(error);
185
+ }
186
+ catch { /* pending/running file is interrupted on restart */ }
187
+ }
188
+ }
189
+ finally {
190
+ if (activeByHash.get(operation.requestHash) === operation.id)
191
+ activeByHash.delete(operation.requestHash);
192
+ }
193
+ return current;
194
+ }
195
+ async function createPending(request) {
196
+ const normalized = normalizeRestoreRequest(request);
197
+ const requestHash = restoreRequestHash(normalized);
198
+ const existingId = activeByHash.get(requestHash);
199
+ if (existingId) {
200
+ const existing = values.get(existingId);
201
+ if (existing)
202
+ return { reused: true, operation: existing };
203
+ activeByHash.delete(requestHash);
204
+ }
205
+ const external = await findExternalActive(requestHash);
206
+ if (external)
207
+ return { reused: true, operation: external };
208
+ const id = randomUUID();
209
+ const operation = {
210
+ id,
211
+ kind: 'workspace-restore',
212
+ status: 'pending',
213
+ request: normalized,
214
+ requestHash,
215
+ ownerPid: pid,
216
+ createdAt: iso(now),
217
+ updatedAt: iso(now),
218
+ startedAt: null,
219
+ completedAt: null,
220
+ progress: { completed: 0, total: 0 },
221
+ results: [],
222
+ mapping: null,
223
+ error: null,
224
+ };
225
+ localIds.add(id);
226
+ activeByHash.set(requestHash, id);
227
+ try {
228
+ await persist(operation);
229
+ }
230
+ catch (error) {
231
+ activeByHash.delete(requestHash);
232
+ localIds.delete(id);
233
+ values.delete(id);
234
+ throw error;
235
+ }
236
+ return { reused: false, operation };
237
+ }
238
+ async function start(request, runner, options) {
239
+ const pending = await createPending(request);
240
+ if (pending.reused) {
241
+ return { operationId: pending.operation.id, status: pending.operation.status, reused: true };
242
+ }
243
+ Promise.resolve().then(() => execute(pending.operation, runner, options)).catch(() => { });
244
+ return { operationId: pending.operation.id, status: 'pending', reused: false };
245
+ }
246
+ async function run(request, runner, options) {
247
+ const pending = await createPending(request);
248
+ if (pending.reused)
249
+ return pending.operation;
250
+ return execute(pending.operation, runner, options);
251
+ }
252
+ async function get(id) {
253
+ if (localIds.has(id) && values.has(id))
254
+ return values.get(id) ?? null;
255
+ return readFresh(id);
256
+ }
257
+ async function interruptOrphans() {
258
+ const rows = typeof store.listOperations === 'function' ? await store.listOperations() : [];
259
+ let interrupted = 0;
260
+ for (const row of rows) {
261
+ const value = row.value;
262
+ if (row.status !== 'ok' || !value || localIds.has(value.id) || !ACTIVE.has(value.status))
263
+ continue;
264
+ const assessed = await assessExternal(value);
265
+ if (assessed.interrupted)
266
+ interrupted += 1;
267
+ }
268
+ return interrupted;
269
+ }
270
+ return { start, run, get, interruptOrphans };
271
+ }
@@ -0,0 +1,15 @@
1
+ import path from 'node:path';
2
+ export function workspacePaths(home) {
3
+ const root = path.join(home, '.handmux', 'workspaces');
4
+ return {
5
+ root,
6
+ liveDir: path.join(root, 'live'),
7
+ liveCurrent: path.join(root, 'live', 'current.json'),
8
+ liveMirror: path.join(root, 'live', 'mirror.json'),
9
+ checkpointsDir: path.join(root, 'checkpoints'),
10
+ recoveryDir: path.join(root, 'recovery'),
11
+ operationsDir: path.join(root, 'operations'),
12
+ latest: path.join(root, 'latest.json'),
13
+ lockDir: path.join(root, 'restore.lock'),
14
+ };
15
+ }
@@ -0,0 +1,292 @@
1
+ const recordOf = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : null);
2
+ function deepFreeze(value, seen = new Set()) {
3
+ if (!value || typeof value !== 'object' || seen.has(value))
4
+ return value;
5
+ seen.add(value);
6
+ for (const nested of Object.values(value))
7
+ deepFreeze(nested, seen);
8
+ return Object.freeze(value);
9
+ }
10
+ function uniqueStrings(values) {
11
+ return [...new Set(values.filter((value) => typeof value === 'string' && Boolean(value)))];
12
+ }
13
+ function stringOrNull(value) {
14
+ return typeof value === 'string' ? value : null;
15
+ }
16
+ function nonEmptyStringOrNull(value) {
17
+ return typeof value === 'string' && value ? value : null;
18
+ }
19
+ function indexOrNull(value) {
20
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : null;
21
+ }
22
+ function restoredName(sourceName, names) {
23
+ if (!names.has(sourceName))
24
+ return sourceName;
25
+ const restored = `${sourceName}-restored`;
26
+ if (!names.has(restored))
27
+ return restored;
28
+ for (let suffix = 2;; suffix += 1) {
29
+ const candidate = `${restored}-${suffix}`;
30
+ if (!names.has(candidate))
31
+ return candidate;
32
+ }
33
+ }
34
+ function runtimeIdList(items, kind) {
35
+ return items.map((item) => {
36
+ const runtimeId = nonEmptyStringOrNull(recordOf(item)?.runtimeId);
37
+ if (runtimeId === null)
38
+ throw new Error(`invalid live ${kind} runtime id`);
39
+ return runtimeId;
40
+ });
41
+ }
42
+ function runtimeIds(live) {
43
+ const record = recordOf(live);
44
+ const sessions = Array.isArray(record?.sessions) ? record.sessions : [];
45
+ const windows = Array.isArray(record?.windows) ? record.windows : [];
46
+ const panes = windows.flatMap((window) => {
47
+ const panesValue = recordOf(window)?.panes;
48
+ return Array.isArray(panesValue) ? panesValue : [];
49
+ });
50
+ return {
51
+ sessions: runtimeIdList(sessions, 'session'),
52
+ windows: runtimeIdList(windows, 'window'),
53
+ panes: runtimeIdList(panes, 'pane'),
54
+ };
55
+ }
56
+ function countTopology(windows) {
57
+ const panes = windows.flatMap((window) => {
58
+ const panesValue = recordOf(window)?.panes;
59
+ return Array.isArray(panesValue) ? panesValue : [];
60
+ });
61
+ return {
62
+ windows: windows.length,
63
+ panes: panes.length,
64
+ agents: panes.filter((pane) => {
65
+ const agent = recordOf(pane)?.agent;
66
+ return agent !== null && agent !== undefined;
67
+ }).length,
68
+ };
69
+ }
70
+ function warningList(...sources) {
71
+ return uniqueStrings(sources.flatMap((source) => Array.isArray(source) ? source : [source]));
72
+ }
73
+ function unwrapCheckpoint(input) {
74
+ const record = recordOf(input);
75
+ if (record?.status === 'ok' && record.value) {
76
+ return { checkpoint: record.value, warnings: warningList(record.warnings, record.warning) };
77
+ }
78
+ return { checkpoint: input, warnings: warningList(record?.warnings, record?.warning) };
79
+ }
80
+ function linkedOwners(sessions) {
81
+ const owners = new Map();
82
+ for (const candidate of sessions) {
83
+ const session = recordOf(candidate);
84
+ if (!session || !Array.isArray(session.windowLinks))
85
+ continue;
86
+ for (const candidateLink of session.windowLinks) {
87
+ const link = recordOf(candidateLink);
88
+ if (typeof link?.windowId !== 'string')
89
+ continue;
90
+ if (!owners.has(link.windowId))
91
+ owners.set(link.windowId, new Set());
92
+ owners.get(link.windowId)?.add(session.id);
93
+ }
94
+ }
95
+ return owners;
96
+ }
97
+ function topologyIssue(session, windowsById, owners, supportsLinkedWindows) {
98
+ if (!Array.isArray(session.windowLinks) || session.windowLinks.length === 0)
99
+ return 'missing-window-links';
100
+ const ids = session.windowLinks.map((link) => link?.windowId);
101
+ const indexes = session.windowLinks.map((link) => link?.index);
102
+ if (ids.some((id) => typeof id !== 'string' || !id))
103
+ return 'invalid-window-link';
104
+ if (indexes.some((index) => typeof index !== 'number' || !Number.isInteger(index) || index < 0))
105
+ return 'invalid-window-link-index';
106
+ const validIds = ids;
107
+ const validIndexes = indexes;
108
+ if (new Set(validIds).size !== validIds.length)
109
+ return 'duplicate-window-link';
110
+ if (new Set(validIndexes).size !== validIndexes.length)
111
+ return 'duplicate-window-link-index';
112
+ if (validIds.some((id) => !windowsById.has(id)))
113
+ return 'dangling-window-link';
114
+ if (!validIds.includes(session.activeWindowId))
115
+ return 'dangling-active-window';
116
+ if (!supportsLinkedWindows && validIds.some((id) => (owners.get(id)?.size ?? 0) > 1))
117
+ return 'linked-windows-unsupported';
118
+ for (const id of validIds) {
119
+ const window = windowsById.get(id);
120
+ if (!window || !Array.isArray(window.panes) || window.panes.length === 0)
121
+ return 'missing-window-panes';
122
+ const paneIds = window.panes.map((pane) => recordOf(pane)?.id);
123
+ if (paneIds.some((paneId) => typeof paneId !== 'string' || !paneId))
124
+ return 'invalid-pane';
125
+ if (new Set(paneIds).size !== paneIds.length)
126
+ return 'duplicate-pane';
127
+ if (!paneIds.includes(window.activePaneId))
128
+ return 'dangling-active-pane';
129
+ }
130
+ return null;
131
+ }
132
+ function sourceProjection(session) {
133
+ const validSession = session && typeof session === 'object' && !Array.isArray(session);
134
+ const record = validSession ? session : null;
135
+ const rawLinks = record && Array.isArray(record.windowLinks) ? record.windowLinks : [];
136
+ const source = {
137
+ logicalId: nonEmptyStringOrNull(record?.id),
138
+ sourceName: nonEmptyStringOrNull(record?.name),
139
+ activeWindowId: nonEmptyStringOrNull(record?.activeWindowId),
140
+ windowLinks: rawLinks.map((link) => ({
141
+ windowId: nonEmptyStringOrNull(recordOf(link)?.windowId),
142
+ index: indexOrNull(recordOf(link)?.index),
143
+ })),
144
+ };
145
+ let issue = null;
146
+ if (!validSession)
147
+ issue = 'invalid-session';
148
+ else if (source.logicalId === null)
149
+ issue = 'invalid-session-id';
150
+ else if (source.sourceName === null)
151
+ issue = 'invalid-session-name';
152
+ else if (source.activeWindowId === null)
153
+ issue = 'invalid-active-window-id';
154
+ else if (!Array.isArray(record?.windowLinks))
155
+ issue = 'invalid-window-links';
156
+ else if (source.windowLinks.some((link) => link.windowId === null))
157
+ issue = 'invalid-window-link';
158
+ else if (source.windowLinks.some((link) => link.index === null))
159
+ issue = 'invalid-window-link-index';
160
+ return { source, issue };
161
+ }
162
+ function activeProjection(active) {
163
+ if (active === null || active === undefined)
164
+ return null;
165
+ const record = recordOf(active) ?? {};
166
+ return {
167
+ sessionId: stringOrNull(record.sessionId),
168
+ windowId: stringOrNull(record.windowId),
169
+ paneId: stringOrNull(record.paneId),
170
+ };
171
+ }
172
+ function selectedTopology(items, windowsById) {
173
+ const windowIds = new Set();
174
+ for (const item of items) {
175
+ if (item.action !== 'create' && item.action !== 'create-renamed')
176
+ continue;
177
+ for (const link of item.windowLinks)
178
+ windowIds.add(link.windowId);
179
+ }
180
+ return [...windowIds].flatMap((id) => {
181
+ const window = windowsById.get(id);
182
+ return window ? [window] : [];
183
+ });
184
+ }
185
+ function windowDispositions(items, live) {
186
+ const liveRecord = recordOf(live);
187
+ const liveWindows = new Map((Array.isArray(liveRecord?.windows) ? liveRecord.windows : []).map((window) => {
188
+ const record = recordOf(window);
189
+ return [record?.id, record?.runtimeId];
190
+ }));
191
+ const dispositions = new Map();
192
+ for (const item of items) {
193
+ if (item.action !== 'create' && item.action !== 'create-renamed')
194
+ continue;
195
+ for (const { windowId } of item.windowLinks) {
196
+ if (dispositions.has(windowId))
197
+ continue;
198
+ if (liveWindows.has(windowId)) {
199
+ const runtimeId = nonEmptyStringOrNull(liveWindows.get(windowId));
200
+ if (runtimeId === null)
201
+ throw new Error('invalid live window runtime id');
202
+ dispositions.set(windowId, { logicalId: nonEmptyStringOrNull(windowId), action: 'reuse', runtimeId });
203
+ }
204
+ else {
205
+ dispositions.set(windowId, {
206
+ logicalId: nonEmptyStringOrNull(windowId),
207
+ action: 'create',
208
+ ownerSessionId: nonEmptyStringOrNull(item.logicalId),
209
+ });
210
+ }
211
+ }
212
+ }
213
+ return [...dispositions.values()];
214
+ }
215
+ export function buildRestorePlan(checkpointInput, live, { sessionNames = [], recovery = null, historical = false, supportsLinkedWindows = true, warnings = [], warning = null, } = {}) {
216
+ const unwrapped = unwrapCheckpoint(checkpointInput);
217
+ const checkpoint = recordOf(unwrapped.checkpoint);
218
+ if (!checkpoint || !Array.isArray(checkpoint.sessions) || !Array.isArray(checkpoint.windows)) {
219
+ throw new Error('invalid checkpoint for restore planning');
220
+ }
221
+ const recoveryRecord = recordOf(recovery);
222
+ if (recovery !== null && recoveryRecord?.checkpointId !== checkpoint.id) {
223
+ throw new Error('recovery checkpoint id mismatch');
224
+ }
225
+ const windowsById = new Map(checkpoint.windows.flatMap((window) => {
226
+ const record = recordOf(window);
227
+ return record ? [[record.id, record]] : [];
228
+ }));
229
+ const owners = linkedOwners(checkpoint.sessions);
230
+ const pendingIds = recoveryRecord && Array.isArray(recoveryRecord.pendingSessionIds)
231
+ ? new Set(recoveryRecord.pendingSessionIds)
232
+ : null;
233
+ const requestedNames = new Set(Array.isArray(sessionNames) ? sessionNames : [sessionNames]);
234
+ const selected = checkpoint.sessions.filter((session) => {
235
+ const record = recordOf(session);
236
+ if (!historical && pendingIds && !pendingIds.has(record?.id))
237
+ return false;
238
+ return requestedNames.size === 0 || requestedNames.has(record?.name);
239
+ });
240
+ const liveRecord = recordOf(live);
241
+ const currentSessions = Array.isArray(liveRecord?.sessions) ? liveRecord.sessions : [];
242
+ const logicalIds = new Set(currentSessions.map((session) => recordOf(session)?.id));
243
+ const names = new Set(currentSessions.map((session) => recordOf(session)?.name));
244
+ const sessions = selected.map((session) => {
245
+ const { source, issue } = sourceProjection(session);
246
+ if (issue)
247
+ return { ...source, action: 'unsupported', reason: issue };
248
+ if (logicalIds.has(source.logicalId))
249
+ return { ...source, action: 'already-present' };
250
+ const reason = topologyIssue(source, windowsById, owners, supportsLinkedWindows);
251
+ if (reason)
252
+ return { ...source, action: 'unsupported', reason };
253
+ const targetName = restoredName(source.sourceName, names);
254
+ names.add(targetName);
255
+ return {
256
+ ...source,
257
+ targetName,
258
+ action: targetName === source.sourceName ? 'create' : 'create-renamed',
259
+ };
260
+ });
261
+ const actions = { create: 0, renamed: 0, alreadyPresent: 0, unsupported: 0 };
262
+ for (const session of sessions) {
263
+ if (session.action === 'create')
264
+ actions.create += 1;
265
+ else if (session.action === 'create-renamed')
266
+ actions.renamed += 1;
267
+ else if (session.action === 'already-present')
268
+ actions.alreadyPresent += 1;
269
+ else if (session.action === 'unsupported')
270
+ actions.unsupported += 1;
271
+ }
272
+ const checkpointTopology = countTopology(checkpoint.windows);
273
+ const planTopology = countTopology(selectedTopology(sessions, windowsById));
274
+ const plan = {
275
+ checkpointId: stringOrNull(checkpoint.id),
276
+ capturedAt: stringOrNull(checkpoint.capturedAt),
277
+ archivedAt: stringOrNull(checkpoint.archivedAt),
278
+ changeReason: stringOrNull(recordOf(checkpoint.environment)?.endedReason),
279
+ detectedAt: stringOrNull(recoveryRecord?.detectedAt),
280
+ expiresAt: stringOrNull(recoveryRecord?.expiresAt),
281
+ resolved: Boolean(recoveryRecord?.resolvedAt),
282
+ pendingCount: pendingIds?.size ?? null,
283
+ summary: { sessions: checkpoint.sessions.length, ...checkpointTopology },
284
+ planSummary: { ...actions, ...planTopology },
285
+ sessions,
286
+ windows: windowDispositions(sessions, live),
287
+ active: activeProjection(checkpoint.active),
288
+ preExistingRuntimeIds: runtimeIds(live),
289
+ warnings: warningList(unwrapped.warnings, checkpoint.warnings, checkpoint.warning, warnings, warning),
290
+ };
291
+ return deepFreeze(plan);
292
+ }