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,829 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { MemoryInteractionStateStore } from './interactionStore.js';
3
+ const ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/;
4
+ const TYPES = new Set(['approval', 'select', 'multi_select', 'text', 'editor', 'form', 'local_only']);
5
+ const RECEIPTS = new Set(['accepted', 'already_resolved', 'stale_run', 'rejected', 'unknown']);
6
+ const REASONS = new Set([
7
+ 'invalid_request', 'invalid_value', 'local_only', 'stale_run', 'already_resolved',
8
+ 'provider_rejected', 'temporarily_unavailable', 'stream_reset',
9
+ ]);
10
+ const INTENTS = new Set([
11
+ 'command_approval', 'file_approval', 'permission_approval', 'input_request',
12
+ ]);
13
+ const RESOLVED_RETENTION_MS = 24 * 60 * 60 * 1000;
14
+ const MAX_PRUNED_PER_MUTATION = 256;
15
+ const MAX_OPEN_BUFFER = 1024;
16
+ export class InteractionContractError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'InteractionContractError';
20
+ }
21
+ }
22
+ function isRecord(value) {
23
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
24
+ }
25
+ function bounded(value, max) {
26
+ return typeof value === 'string' && value.length > 0 && value.length <= max;
27
+ }
28
+ function copyRun(run) {
29
+ return {
30
+ agentId: run.agentId, paneId: run.paneId, runId: run.runId,
31
+ ...(run.sessionId === undefined ? {} : { sessionId: run.sessionId }),
32
+ };
33
+ }
34
+ function options(value) {
35
+ if (value === undefined)
36
+ return undefined;
37
+ if (!Array.isArray(value) || value.length === 0 || value.length > 100)
38
+ return null;
39
+ const normalized = [];
40
+ const ids = new Set();
41
+ for (const option of value) {
42
+ if (!isRecord(option) || !bounded(option.id, 256) || !bounded(option.label, 1024)
43
+ || (option.description !== undefined && !bounded(option.description, 4096))
44
+ || ids.has(option.id))
45
+ return null;
46
+ ids.add(option.id);
47
+ normalized.push({
48
+ id: option.id, label: option.label,
49
+ ...(option.description === undefined ? {} : { description: option.description }),
50
+ });
51
+ }
52
+ return normalized;
53
+ }
54
+ function details(value) {
55
+ if (value === undefined)
56
+ return undefined;
57
+ if (!Array.isArray(value) || value.length === 0 || value.length > 20)
58
+ return null;
59
+ const normalized = [];
60
+ for (const detail of value) {
61
+ if (!isRecord(detail) || !['text', 'code', 'path'].includes(String(detail.type))
62
+ || !bounded(detail.text, 64 * 1024)
63
+ || (detail.kind !== undefined
64
+ && !['reason', 'command', 'working_directory', 'context'].includes(String(detail.kind))))
65
+ return null;
66
+ normalized.push({
67
+ type: detail.type, text: detail.text,
68
+ ...(detail.kind === undefined ? {} : {
69
+ kind: detail.kind,
70
+ }),
71
+ });
72
+ }
73
+ return normalized;
74
+ }
75
+ function fields(value) {
76
+ if (value === undefined)
77
+ return undefined;
78
+ if (!Array.isArray(value) || value.length === 0 || value.length > 100)
79
+ return null;
80
+ const normalized = [];
81
+ const ids = new Set();
82
+ for (const field of value) {
83
+ if (!isRecord(field) || !bounded(field.id, 256) || !ID_RE.test(field.id)
84
+ || ids.has(field.id) || !['text', 'secret', 'select'].includes(String(field.type))
85
+ || !bounded(field.prompt, 16 * 1024)
86
+ || (field.label !== undefined && !bounded(field.label, 1024))
87
+ || (field.allowOther !== undefined && typeof field.allowOther !== 'boolean'))
88
+ return null;
89
+ const normalizedOptions = options(field.options);
90
+ if (normalizedOptions === null || ((field.type === 'select') !== (normalizedOptions !== undefined))
91
+ || (field.type !== 'select' && field.allowOther !== undefined))
92
+ return null;
93
+ ids.add(field.id);
94
+ normalized.push({
95
+ id: field.id, type: field.type, prompt: field.prompt,
96
+ ...(field.label === undefined ? {} : { label: field.label }),
97
+ ...(normalizedOptions === undefined ? {} : { options: normalizedOptions }),
98
+ ...(field.allowOther === undefined ? {} : { allowOther: field.allowOther }),
99
+ });
100
+ }
101
+ return normalized;
102
+ }
103
+ function pending(value) {
104
+ if (!isRecord(value) || !bounded(value.id, 256) || !ID_RE.test(value.id)
105
+ || !bounded(value.prompt, 16 * 1024) || typeof value.type !== 'string'
106
+ || !TYPES.has(value.type) || (value.correlationId !== undefined
107
+ && !bounded(value.correlationId, 256))
108
+ || (value.intent !== undefined && !INTENTS.has(String(value.intent))))
109
+ return null;
110
+ const normalizedOptions = options(value.options);
111
+ const normalizedDetails = details(value.details);
112
+ const normalizedFields = fields(value.fields);
113
+ if (normalizedOptions === null || normalizedDetails === null || normalizedFields === null)
114
+ return null;
115
+ const requiresOptions = ['approval', 'select', 'multi_select'].includes(value.type);
116
+ const requiresFields = value.type === 'form';
117
+ if (requiresOptions !== (normalizedOptions !== undefined)
118
+ || requiresFields !== (normalizedFields !== undefined))
119
+ return null;
120
+ return {
121
+ id: value.id,
122
+ type: value.type,
123
+ prompt: value.prompt,
124
+ ...(value.intent === undefined ? {} : {
125
+ intent: value.intent,
126
+ }),
127
+ ...(value.correlationId === undefined ? {} : { correlationId: value.correlationId }),
128
+ ...(normalizedOptions === undefined ? {} : { options: normalizedOptions }),
129
+ ...(normalizedDetails === undefined ? {} : { details: normalizedDetails }),
130
+ ...(normalizedFields === undefined ? {} : { fields: normalizedFields }),
131
+ };
132
+ }
133
+ function receipt(value) {
134
+ if (!isRecord(value) || typeof value.status !== 'string' || !RECEIPTS.has(value.status)
135
+ || (value.reason !== undefined && !REASONS.has(String(value.reason))))
136
+ return null;
137
+ return {
138
+ status: value.status,
139
+ ...(value.reason === undefined ? {} : {
140
+ reason: value.reason,
141
+ }),
142
+ };
143
+ }
144
+ function isDispatching(record) {
145
+ return record.state === 'dispatching';
146
+ }
147
+ function responseValue(value, interaction) {
148
+ if (!isRecord(value) || typeof value.type !== 'string')
149
+ return null;
150
+ const ids = new Set(interaction.options?.map((option) => option.id) ?? []);
151
+ if (interaction.type === 'approval' && value.type === 'approval'
152
+ && bounded(value.optionId, 256) && ids.has(value.optionId)) {
153
+ return { type: 'approval', optionId: value.optionId };
154
+ }
155
+ if ((interaction.type === 'select' || interaction.type === 'multi_select')
156
+ && value.type === 'selection' && Array.isArray(value.optionIds)
157
+ && value.optionIds.every((id) => bounded(id, 256) && ids.has(id))
158
+ && new Set(value.optionIds).size === value.optionIds.length
159
+ && (interaction.type === 'multi_select' || value.optionIds.length === 1)) {
160
+ return { type: 'selection', optionIds: [...value.optionIds] };
161
+ }
162
+ if ((interaction.type === 'text' || interaction.type === 'editor')
163
+ && value.type === 'text' && typeof value.text === 'string'
164
+ && value.text.length <= 256 * 1024)
165
+ return { type: 'text', text: value.text };
166
+ if (interaction.type === 'form' && value.type === 'form' && isRecord(value.answers)) {
167
+ const fieldIds = interaction.fields?.map((field) => field.id) ?? [];
168
+ const answerIds = Object.keys(value.answers);
169
+ if (answerIds.length !== fieldIds.length || answerIds.some((id) => !fieldIds.includes(id)))
170
+ return null;
171
+ const answers = {};
172
+ for (const id of fieldIds) {
173
+ const answer = value.answers[id];
174
+ if (typeof answer !== 'string' || answer.length === 0 || answer.length > 256 * 1024)
175
+ return null;
176
+ const field = interaction.fields?.find((candidate) => candidate.id === id);
177
+ if (!field || (field.type === 'select'
178
+ && !field.options?.some((option) => option.id === answer) && !field.allowOther))
179
+ return null;
180
+ answers[id] = answer;
181
+ }
182
+ return { type: 'form', answers };
183
+ }
184
+ return null;
185
+ }
186
+ function publicPending(record) {
187
+ const value = record.pending;
188
+ return {
189
+ id: record.publicInteractionId,
190
+ runId: record.run.runId,
191
+ type: value.type,
192
+ prompt: value.prompt,
193
+ resolutionToken: record.resolutionToken,
194
+ ...(value.intent === undefined ? {} : { intent: value.intent }),
195
+ ...(value.correlationId === undefined ? {} : { correlationId: value.correlationId }),
196
+ ...(value.options === undefined ? {} : { options: structuredClone(value.options) }),
197
+ ...(value.details === undefined ? {} : { details: structuredClone(value.details) }),
198
+ ...(value.fields === undefined ? {} : { fields: structuredClone(value.fields) }),
199
+ };
200
+ }
201
+ function validRun(value) {
202
+ return isRecord(value) && bounded(value.agentId, 64) && bounded(value.paneId, 256)
203
+ && bounded(value.runId, 256)
204
+ && (value.sessionId === undefined || bounded(value.sessionId, 1024));
205
+ }
206
+ function parseState(raw, now) {
207
+ if (raw === null || raw === undefined)
208
+ return { version: 1, interactions: [] };
209
+ if (!isRecord(raw) || raw.version !== 1 || !Array.isArray(raw.interactions)) {
210
+ throw new InteractionContractError('Unsupported or corrupt Interaction state');
211
+ }
212
+ const publicIds = new Set();
213
+ const sourceKeys = new Set();
214
+ const interactions = [];
215
+ for (const value of raw.interactions) {
216
+ if (!isRecord(value) || !bounded(value.agentId, 64) || !validRun(value.run)
217
+ || value.run.agentId !== value.agentId || !bounded(value.sourceInteractionId, 256)
218
+ || !bounded(value.publicInteractionId, 1024) || !bounded(value.resolutionToken, 1024)
219
+ || !['pending', 'dispatching', 'resolved'].includes(String(value.state))
220
+ || typeof value.createdAt !== 'number' || !Number.isFinite(value.createdAt)
221
+ || typeof value.updatedAt !== 'number' || !Number.isFinite(value.updatedAt)) {
222
+ throw new InteractionContractError('Corrupt Interaction record');
223
+ }
224
+ const normalizedPending = pending(value.pending);
225
+ const normalizedReceipt = value.receipt === undefined ? undefined : receipt(value.receipt);
226
+ if (!normalizedPending || normalizedReceipt === null
227
+ || (value.state === 'resolved' && !normalizedReceipt)
228
+ || (value.state !== 'resolved' && value.receipt !== undefined)) {
229
+ throw new InteractionContractError('Corrupt Interaction payload or receipt');
230
+ }
231
+ const sourceKey = `${value.agentId}\0${value.run.runId}\0${value.sourceInteractionId}`;
232
+ if (publicIds.has(value.publicInteractionId) || sourceKeys.has(sourceKey)) {
233
+ throw new InteractionContractError('Duplicate Interaction identity');
234
+ }
235
+ publicIds.add(value.publicInteractionId);
236
+ sourceKeys.add(sourceKey);
237
+ if (value.state === 'resolved' && value.updatedAt + RESOLVED_RETENTION_MS <= now)
238
+ continue;
239
+ interactions.push({
240
+ agentId: value.agentId,
241
+ run: copyRun(value.run),
242
+ sourceInteractionId: value.sourceInteractionId,
243
+ publicInteractionId: value.publicInteractionId,
244
+ resolutionToken: value.resolutionToken,
245
+ pending: normalizedPending,
246
+ state: value.state,
247
+ ...(normalizedReceipt === undefined ? {} : { receipt: normalizedReceipt }),
248
+ createdAt: value.createdAt,
249
+ updatedAt: value.updatedAt,
250
+ });
251
+ }
252
+ return { version: 1, interactions };
253
+ }
254
+ export class InteractionService {
255
+ #runs;
256
+ #adapters;
257
+ #store;
258
+ #now;
259
+ #newToken;
260
+ #open = new Map();
261
+ #issued = new Set();
262
+ #terminalPersistenceFailures = new Set();
263
+ #state;
264
+ #writeTail = Promise.resolve();
265
+ constructor({ runs, adapters, store = new MemoryInteractionStateStore(), now = Date.now, newToken = randomUUID, }) {
266
+ if (!runs || !isRecord(adapters) || Object.keys(adapters).length === 0) {
267
+ throw new TypeError('InteractionService requires a run registry and adapters');
268
+ }
269
+ const map = new Map();
270
+ for (const [agentId, adapter] of Object.entries(adapters)) {
271
+ if (!bounded(agentId, 64) || !adapter || adapter.apiVersion !== 1
272
+ || typeof adapter.observeNative !== 'function'
273
+ || typeof adapter.dispatchResponse !== 'function') {
274
+ throw new TypeError(`Invalid Interaction adapter: ${agentId}`);
275
+ }
276
+ map.set(agentId, adapter);
277
+ }
278
+ this.#runs = runs;
279
+ this.#adapters = map;
280
+ this.#store = store;
281
+ this.#now = now;
282
+ this.#newToken = newToken;
283
+ this.#state = parseState(store.load(), now());
284
+ let changed = false;
285
+ for (const record of this.#state.interactions) {
286
+ if (isDispatching(record)) {
287
+ record.state = 'resolved';
288
+ record.receipt = { status: 'unknown', reason: 'temporarily_unavailable' };
289
+ record.updatedAt = now();
290
+ changed = true;
291
+ }
292
+ }
293
+ if (changed)
294
+ this.#save();
295
+ }
296
+ async open(run, sink) {
297
+ this.#requireRun(run);
298
+ if (typeof sink !== 'function' || this.#open.has(run.ref.runId)) {
299
+ throw new InteractionContractError('Invalid or duplicate Interaction open');
300
+ }
301
+ const adapter = this.#adapter(run.ref.agentId);
302
+ let resolveClosed;
303
+ const closed = new Promise((resolve) => { resolveClosed = resolve; });
304
+ const state = {
305
+ run, adapter, sink, phase: 'buffering', revision: 0, sourceCursor: '',
306
+ seenCursors: new Set(), pendingBySource: new Map(), buffered: [],
307
+ openingFailure: undefined,
308
+ tail: Promise.resolve(), nativeHandle: undefined, detachAbort: () => { },
309
+ closed, resolveClosed, exposed: false,
310
+ };
311
+ const onAbort = () => {
312
+ void this.#enqueue(state, () => this.#cancelRevoked(state)).catch(() => { });
313
+ };
314
+ run.signal.addEventListener('abort', onAbort, { once: true });
315
+ state.detachAbort = () => run.signal.removeEventListener('abort', onAbort);
316
+ this.#open.set(run.ref.runId, state);
317
+ try {
318
+ const nativeHandle = await adapter.observeNative(run, (event) => this.#accept(state, event));
319
+ state.nativeHandle = nativeHandle;
320
+ this.#throwOpeningFailure(state);
321
+ if (run.signal.aborted || this.#runs.resolve(run.ref) !== run || state.phase === 'closed') {
322
+ throw new InteractionContractError('Interaction run was revoked during open');
323
+ }
324
+ const checkpoint = nativeHandle.checkpoint;
325
+ if (!isRecord(checkpoint) || !bounded(checkpoint.sourceCursor, 1024)
326
+ || !Array.isArray(checkpoint.pending)) {
327
+ throw new InteractionContractError('Interaction adapter returned an invalid checkpoint');
328
+ }
329
+ const baseline = checkpoint.pending.map((item) => pending(item));
330
+ if (baseline.some((item) => item === null)
331
+ || new Set(baseline.map((item) => item.id)).size !== baseline.length) {
332
+ throw new InteractionContractError('Interaction checkpoint contains invalid pending items');
333
+ }
334
+ state.sourceCursor = checkpoint.sourceCursor;
335
+ state.seenCursors.add(checkpoint.sourceCursor);
336
+ await this.#withWrite(() => {
337
+ const sourceIds = new Set(baseline.map((item) => item.id));
338
+ for (const record of this.#recordsFor(run.ref)) {
339
+ if (record.state === 'pending' && !sourceIds.has(record.sourceInteractionId)) {
340
+ record.state = 'resolved';
341
+ record.receipt = { status: 'already_resolved', reason: 'already_resolved' };
342
+ record.updatedAt = this.#now();
343
+ }
344
+ }
345
+ for (const item of baseline) {
346
+ let record = this.#recordBySource(run.ref, item.id);
347
+ if (!record) {
348
+ record = this.#createRecord(run.ref, item);
349
+ this.#state.interactions.push(record);
350
+ }
351
+ else if (record.state === 'pending') {
352
+ record.pending = item;
353
+ record.updatedAt = this.#now();
354
+ }
355
+ if (record.state === 'pending')
356
+ state.pendingBySource.set(item.id, record);
357
+ }
358
+ this.#save();
359
+ });
360
+ this.#throwOpeningFailure(state);
361
+ const exposed = Object.freeze({
362
+ revision: state.revision,
363
+ pending: [...state.pendingBySource.values()].map(publicPending),
364
+ closed: state.closed,
365
+ close: () => this.#close(state),
366
+ });
367
+ state.exposed = true;
368
+ setTimeout(() => { this.#flush(state); }, 0).unref?.();
369
+ return exposed;
370
+ }
371
+ catch (error) {
372
+ await this.#close(state);
373
+ throw error;
374
+ }
375
+ }
376
+ async respond(run, request) {
377
+ try {
378
+ this.#requireRun(run);
379
+ }
380
+ catch {
381
+ return { status: 'stale_run' };
382
+ }
383
+ if (!isRecord(request) || !bounded(request.interactionId, 1024)
384
+ || !bounded(request.resolutionToken, 1024))
385
+ return { status: 'rejected', reason: 'invalid_request' };
386
+ const record = this.#state.interactions.find((item) => (item.publicInteractionId === request.interactionId && item.run.runId === run.ref.runId
387
+ && item.agentId === run.ref.agentId));
388
+ if (!record || record.resolutionToken !== request.resolutionToken) {
389
+ return { status: 'rejected', reason: 'invalid_request' };
390
+ }
391
+ if (record.state === 'dispatching'
392
+ && this.#terminalPersistenceFailures.has(record.publicInteractionId)) {
393
+ return { status: 'unknown', reason: 'temporarily_unavailable' };
394
+ }
395
+ if (record.state !== 'dispatching') {
396
+ this.#terminalPersistenceFailures.delete(record.publicInteractionId);
397
+ }
398
+ if (record.state !== 'pending')
399
+ return record.receipt ?? { status: 'already_resolved' };
400
+ if (record.pending.type === 'local_only')
401
+ return { status: 'rejected', reason: 'local_only' };
402
+ const value = responseValue(request.value, record.pending);
403
+ if (!value)
404
+ return { status: 'rejected', reason: 'invalid_value' };
405
+ let claimed = false;
406
+ try {
407
+ claimed = await this.#withWrite(() => {
408
+ if (record.state !== 'pending')
409
+ return false;
410
+ record.state = 'dispatching';
411
+ record.updatedAt = this.#now();
412
+ try {
413
+ this.#save();
414
+ }
415
+ catch (error) {
416
+ record.state = 'pending';
417
+ throw error;
418
+ }
419
+ return true;
420
+ });
421
+ }
422
+ catch {
423
+ return { status: 'unknown', reason: 'temporarily_unavailable' };
424
+ }
425
+ if (!claimed)
426
+ return record.receipt ?? { status: 'already_resolved' };
427
+ let result;
428
+ try {
429
+ result = await this.#raceAbort(run, this.#adapter(run.ref.agentId).dispatchResponse(run, {
430
+ interactionId: record.sourceInteractionId,
431
+ value,
432
+ }));
433
+ }
434
+ catch {
435
+ result = { status: 'unknown', reason: 'temporarily_unavailable' };
436
+ }
437
+ const normalized = receipt(result) ?? { status: 'unknown', reason: 'temporarily_unavailable' };
438
+ try {
439
+ const finalized = await this.#finalizeResponse(run, record, normalized);
440
+ this.#terminalPersistenceFailures.delete(record.publicInteractionId);
441
+ return finalized;
442
+ }
443
+ catch {
444
+ if (isDispatching(record)) {
445
+ this.#terminalPersistenceFailures.add(record.publicInteractionId);
446
+ return { status: 'unknown', reason: 'temporarily_unavailable' };
447
+ }
448
+ return normalized;
449
+ }
450
+ }
451
+ async shutdown() {
452
+ const states = [...this.#open.values()];
453
+ await Promise.all(states.map(async (state) => {
454
+ if (!state.run.signal.aborted)
455
+ return this.#close(state);
456
+ await state.closed;
457
+ }));
458
+ await this.#writeTail.catch(() => { });
459
+ }
460
+ #adapter(agentId) {
461
+ const adapter = this.#adapters.get(agentId);
462
+ if (!adapter)
463
+ throw new InteractionContractError(`Interaction adapter is unavailable: ${agentId}`);
464
+ return adapter;
465
+ }
466
+ #requireRun(run) {
467
+ if (!run || run.signal.aborted || this.#runs.resolve(run.ref) !== run) {
468
+ throw new InteractionContractError('Interaction operation requires a live run lease');
469
+ }
470
+ }
471
+ #token(prefix) {
472
+ for (let attempt = 0; attempt < 8; attempt += 1) {
473
+ const token = `${prefix}:${this.#newToken()}`;
474
+ if (bounded(token, 1024) && !this.#issued.has(token)
475
+ && !this.#state.interactions.some((record) => (record.publicInteractionId === token || record.resolutionToken === token))) {
476
+ this.#issued.add(token);
477
+ return token;
478
+ }
479
+ }
480
+ throw new InteractionContractError(`Unable to allocate Interaction ${prefix}`);
481
+ }
482
+ #createRecord(run, item) {
483
+ const now = this.#now();
484
+ return {
485
+ agentId: run.agentId,
486
+ run: copyRun(run),
487
+ sourceInteractionId: item.id,
488
+ publicInteractionId: this.#token('interaction'),
489
+ resolutionToken: this.#token('resolution'),
490
+ pending: structuredClone(item),
491
+ state: 'pending',
492
+ createdAt: now,
493
+ updatedAt: now,
494
+ };
495
+ }
496
+ #recordsFor(run) {
497
+ return this.#state.interactions.filter((record) => (record.agentId === run.agentId && record.run.runId === run.runId));
498
+ }
499
+ #recordBySource(run, sourceId) {
500
+ return this.#recordsFor(run).find((record) => record.sourceInteractionId === sourceId);
501
+ }
502
+ #accept(state, event) {
503
+ if (state.phase === 'closed')
504
+ return Promise.resolve();
505
+ if (state.openingFailure)
506
+ return Promise.reject(state.openingFailure);
507
+ let cloned;
508
+ try {
509
+ cloned = structuredClone(event);
510
+ }
511
+ catch {
512
+ const failure = new InteractionContractError('Interaction event is not cloneable');
513
+ if (state.phase === 'buffering')
514
+ state.openingFailure = failure;
515
+ else
516
+ this.#triggerFailClosed(state);
517
+ return Promise.reject(failure);
518
+ }
519
+ if (state.phase === 'buffering') {
520
+ if (state.buffered.length >= MAX_OPEN_BUFFER) {
521
+ state.openingFailure = new InteractionContractError('Interaction open buffer is full');
522
+ return Promise.reject(state.openingFailure);
523
+ }
524
+ state.buffered.push(cloned);
525
+ return Promise.resolve();
526
+ }
527
+ return this.#enqueue(state, () => this.#process(state, cloned));
528
+ }
529
+ #enqueue(state, operation) {
530
+ const pendingOperation = state.tail.then(operation);
531
+ state.tail = pendingOperation.catch(async () => {
532
+ try {
533
+ await this.#failClosed(state);
534
+ }
535
+ catch { /* Callback keeps the original contract error. */ }
536
+ });
537
+ return pendingOperation;
538
+ }
539
+ #flush(state) {
540
+ if (state.phase !== 'buffering')
541
+ return;
542
+ state.phase = 'live';
543
+ if (state.openingFailure) {
544
+ state.buffered = [];
545
+ this.#triggerFailClosed(state);
546
+ return;
547
+ }
548
+ for (const event of state.buffered.splice(0)) {
549
+ void this.#accept(state, event).catch(() => { this.#triggerFailClosed(state); });
550
+ }
551
+ }
552
+ #throwOpeningFailure(state) {
553
+ if (state.openingFailure)
554
+ throw state.openingFailure;
555
+ }
556
+ #triggerFailClosed(state) {
557
+ void this.#failClosed(state).catch(() => { });
558
+ }
559
+ async #process(state, event) {
560
+ if (!isRecord(event) || !bounded(event.sourceCursor, 1024)
561
+ || state.seenCursors.has(event.sourceCursor)) {
562
+ throw new InteractionContractError('Interaction source cursor is invalid or duplicate');
563
+ }
564
+ state.seenCursors.add(event.sourceCursor);
565
+ state.sourceCursor = event.sourceCursor;
566
+ if (event.type === 'opened') {
567
+ const item = pending(event.interaction);
568
+ if (!item)
569
+ throw new InteractionContractError('Interaction opened payload is invalid');
570
+ let record = this.#recordBySource(state.run.ref, item.id);
571
+ if (record && record.state !== 'pending') {
572
+ throw new InteractionContractError('Resolved native interaction id was reused');
573
+ }
574
+ await this.#withWrite(() => {
575
+ if (!record) {
576
+ record = this.#createRecord(state.run.ref, item);
577
+ this.#state.interactions.push(record);
578
+ }
579
+ else {
580
+ record.pending = item;
581
+ record.updatedAt = this.#now();
582
+ }
583
+ this.#save();
584
+ });
585
+ state.pendingBySource.set(item.id, record);
586
+ await this.#emit(state, {
587
+ type: 'opened', revision: ++state.revision, interaction: publicPending(record),
588
+ });
589
+ return;
590
+ }
591
+ if (event.type !== 'resolved' && event.type !== 'cancelled') {
592
+ throw new InteractionContractError('Unknown Interaction adapter event');
593
+ }
594
+ if (!bounded(event.interactionId, 256)
595
+ || (event.type === 'cancelled' && event.reason !== undefined && !REASONS.has(event.reason))) {
596
+ throw new InteractionContractError('Interaction terminal payload is invalid');
597
+ }
598
+ const record = state.pendingBySource.get(event.interactionId);
599
+ if (!record)
600
+ throw new InteractionContractError('Interaction terminal event targets unknown pending item');
601
+ await this.#withWrite(() => {
602
+ record.state = 'resolved';
603
+ record.receipt = event.type === 'resolved'
604
+ ? { status: 'already_resolved', reason: 'already_resolved' }
605
+ : { status: 'rejected', reason: event.reason ?? 'provider_rejected' };
606
+ record.updatedAt = this.#now();
607
+ this.#save();
608
+ });
609
+ state.pendingBySource.delete(event.interactionId);
610
+ await this.#emit(state, event.type === 'resolved'
611
+ ? { type: 'resolved', revision: ++state.revision, interactionId: record.publicInteractionId }
612
+ : {
613
+ type: 'cancelled', revision: ++state.revision, interactionId: record.publicInteractionId,
614
+ ...(event.reason === undefined ? {} : { reason: event.reason }),
615
+ });
616
+ }
617
+ async #finalizeResponse(run, record, result) {
618
+ const live = this.#open.get(run.ref.runId);
619
+ const finish = async () => {
620
+ let finalized = false;
621
+ await this.#withWrite(() => {
622
+ if (record.state !== 'dispatching')
623
+ return;
624
+ if (result.status === 'rejected' && !run.signal.aborted
625
+ && this.#runs.resolve(run.ref) === run) {
626
+ const before = { state: record.state, updatedAt: record.updatedAt };
627
+ record.state = 'pending';
628
+ record.updatedAt = this.#now();
629
+ try {
630
+ this.#save();
631
+ }
632
+ catch (error) {
633
+ record.state = before.state;
634
+ record.updatedAt = before.updatedAt;
635
+ throw error;
636
+ }
637
+ return;
638
+ }
639
+ const before = {
640
+ state: record.state, receipt: record.receipt, updatedAt: record.updatedAt,
641
+ };
642
+ record.state = 'resolved';
643
+ record.receipt = result;
644
+ record.updatedAt = this.#now();
645
+ try {
646
+ this.#save();
647
+ }
648
+ catch (error) {
649
+ record.state = before.state;
650
+ if (before.receipt === undefined)
651
+ delete record.receipt;
652
+ else
653
+ record.receipt = before.receipt;
654
+ record.updatedAt = before.updatedAt;
655
+ throw error;
656
+ }
657
+ finalized = true;
658
+ });
659
+ if (!finalized || !live || !live.exposed || live.phase === 'closed')
660
+ return;
661
+ live.pendingBySource.delete(record.sourceInteractionId);
662
+ if (result.status === 'accepted' || result.status === 'already_resolved') {
663
+ await this.#emit(live, {
664
+ type: 'resolved', revision: ++live.revision, interactionId: record.publicInteractionId,
665
+ });
666
+ }
667
+ else {
668
+ await this.#emit(live, {
669
+ type: 'cancelled', revision: ++live.revision, interactionId: record.publicInteractionId,
670
+ reason: result.reason ?? 'temporarily_unavailable',
671
+ });
672
+ }
673
+ };
674
+ if (live)
675
+ await this.#enqueue(live, finish);
676
+ else
677
+ await finish();
678
+ return result;
679
+ }
680
+ async #raceAbort(run, operation) {
681
+ if (run.signal.aborted)
682
+ return { status: 'stale_run' };
683
+ return new Promise((resolve, reject) => {
684
+ const abort = () => resolve({ status: 'stale_run' });
685
+ run.signal.addEventListener('abort', abort, { once: true });
686
+ operation.then((value) => { run.signal.removeEventListener('abort', abort); resolve(value); }, (error) => { run.signal.removeEventListener('abort', abort); reject(error); });
687
+ });
688
+ }
689
+ async #cancelRevoked(state) {
690
+ if (state.phase === 'closed')
691
+ return;
692
+ const records = [...state.pendingBySource.values()];
693
+ try {
694
+ await this.#withWrite(() => {
695
+ const before = records.map((record) => ({
696
+ record, state: record.state, receipt: record.receipt, updatedAt: record.updatedAt,
697
+ }));
698
+ for (const record of records) {
699
+ record.state = 'resolved';
700
+ record.receipt = { status: 'stale_run', reason: 'stale_run' };
701
+ record.updatedAt = this.#now();
702
+ }
703
+ try {
704
+ if (records.length)
705
+ this.#save();
706
+ }
707
+ catch (error) {
708
+ for (const item of before) {
709
+ item.record.state = item.state;
710
+ if (item.receipt === undefined)
711
+ delete item.record.receipt;
712
+ else
713
+ item.record.receipt = item.receipt;
714
+ item.record.updatedAt = item.updatedAt;
715
+ }
716
+ throw error;
717
+ }
718
+ });
719
+ if (state.exposed) {
720
+ for (const record of records) {
721
+ await this.#emit(state, {
722
+ type: 'cancelled', revision: ++state.revision,
723
+ interactionId: record.publicInteractionId, reason: 'stale_run',
724
+ });
725
+ }
726
+ }
727
+ }
728
+ finally {
729
+ await this.#close(state);
730
+ }
731
+ }
732
+ async #failClosed(state) {
733
+ if (state.phase === 'closed')
734
+ return;
735
+ const records = [...state.pendingBySource.values()];
736
+ try {
737
+ await this.#withWrite(() => {
738
+ const before = records.map((record) => ({
739
+ record, state: record.state, receipt: record.receipt, updatedAt: record.updatedAt,
740
+ }));
741
+ for (const record of records) {
742
+ record.state = 'resolved';
743
+ record.receipt = { status: 'unknown', reason: 'stream_reset' };
744
+ record.updatedAt = this.#now();
745
+ }
746
+ try {
747
+ if (records.length)
748
+ this.#save();
749
+ }
750
+ catch (error) {
751
+ for (const item of before) {
752
+ item.record.state = item.state;
753
+ if (item.receipt === undefined)
754
+ delete item.record.receipt;
755
+ else
756
+ item.record.receipt = item.receipt;
757
+ item.record.updatedAt = item.updatedAt;
758
+ }
759
+ throw error;
760
+ }
761
+ });
762
+ if (state.exposed) {
763
+ for (const record of records) {
764
+ try {
765
+ await this.#emit(state, {
766
+ type: 'cancelled', revision: ++state.revision,
767
+ interactionId: record.publicInteractionId, reason: 'stream_reset',
768
+ });
769
+ }
770
+ catch {
771
+ break;
772
+ }
773
+ }
774
+ }
775
+ }
776
+ finally {
777
+ await this.#close(state);
778
+ }
779
+ }
780
+ async #emit(state, event) {
781
+ if (state.exposed && state.phase !== 'closed')
782
+ await state.sink(structuredClone(event));
783
+ }
784
+ async #close(state) {
785
+ if (state.phase === 'closed')
786
+ return;
787
+ state.phase = 'closed';
788
+ state.detachAbort();
789
+ state.buffered = [];
790
+ if (this.#open.get(state.run.ref.runId) === state)
791
+ this.#open.delete(state.run.ref.runId);
792
+ try {
793
+ await state.nativeHandle?.close();
794
+ }
795
+ catch { /* best effort */ }
796
+ state.resolveClosed();
797
+ }
798
+ async #withWrite(operation) {
799
+ const previous = this.#writeTail;
800
+ let release;
801
+ this.#writeTail = new Promise((resolve) => { release = resolve; });
802
+ await previous.catch(() => { });
803
+ try {
804
+ return await operation();
805
+ }
806
+ finally {
807
+ release();
808
+ }
809
+ }
810
+ #save() {
811
+ const before = this.#state.interactions;
812
+ const now = this.#now();
813
+ let pruned = 0;
814
+ this.#state.interactions = before.filter((record) => {
815
+ if (pruned >= MAX_PRUNED_PER_MUTATION || record.state !== 'resolved'
816
+ || record.updatedAt + RESOLVED_RETENTION_MS > now)
817
+ return true;
818
+ pruned += 1;
819
+ return false;
820
+ });
821
+ try {
822
+ this.#store.save(structuredClone(this.#state));
823
+ }
824
+ catch (error) {
825
+ this.#state.interactions = before;
826
+ throw error;
827
+ }
828
+ }
829
+ }