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,973 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { MemoryInboxStateStore } from './inboxStore.js';
3
+ const SOURCE_RE = /^[a-z][a-z0-9-]{0,63}\.[a-z][a-z0-9._-]{0,63}$/;
4
+ const EVENT_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/;
5
+ const HASH_RE = /^[0-9a-f]{64}$/;
6
+ const TERMINAL_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
7
+ const STATES = new Set(['working', 'waiting', 'done', 'error']);
8
+ const AVAILABILITIES = new Set(['ready', 'degraded', 'unavailable']);
9
+ export class InboxContractError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'InboxContractError';
13
+ }
14
+ }
15
+ function isRecord(value) {
16
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
17
+ }
18
+ function validRunRef(value) {
19
+ return isRecord(value)
20
+ && typeof value.agentId === 'string' && value.agentId.length > 0
21
+ && typeof value.paneId === 'string' && value.paneId.length > 0
22
+ && typeof value.runId === 'string' && value.runId.length > 0
23
+ && (value.sessionId === undefined || (typeof value.sessionId === 'string' && value.sessionId.length > 0));
24
+ }
25
+ function copyRunRef(ref) {
26
+ return Object.freeze({
27
+ agentId: ref.agentId,
28
+ paneId: ref.paneId,
29
+ runId: ref.runId,
30
+ ...(ref.sessionId === undefined ? {} : { sessionId: ref.sessionId }),
31
+ });
32
+ }
33
+ function sameTerminalEvent(left, right) {
34
+ if (left.agentId !== right.agentId || left.eventId !== right.eventId)
35
+ return false;
36
+ if (left.sessionId !== undefined && right.sessionId !== undefined) {
37
+ return left.sessionId === right.sessionId;
38
+ }
39
+ return left.runId === right.runId && left.paneId === right.paneId;
40
+ }
41
+ function boundedString(value, max) {
42
+ return typeof value === 'string' && value.length > 0 && value.length <= max;
43
+ }
44
+ function optionalText(value, max, nullable = false) {
45
+ return value === undefined || (nullable && value === null) || boundedString(value, max);
46
+ }
47
+ function validSource(value, agentId) {
48
+ return isRecord(value)
49
+ && typeof value.sourceId === 'string'
50
+ && SOURCE_RE.test(value.sourceId)
51
+ && value.sourceId.startsWith(`${agentId}.`)
52
+ && (value.cursor === undefined || boundedString(value.cursor, 1024));
53
+ }
54
+ function copySource(source) {
55
+ return Object.freeze({
56
+ sourceId: source.sourceId,
57
+ ...(source.cursor === undefined ? {} : { cursor: source.cursor }),
58
+ });
59
+ }
60
+ function semanticHash(value) {
61
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
62
+ }
63
+ function normalizeOperation(raw, agentId) {
64
+ if (!isRecord(raw) || (raw.kind !== 'set' && raw.kind !== 'clear')
65
+ || !validSource(raw.source, agentId)
66
+ || !optionalText(raw.message, 4096, true)
67
+ || !optionalText(raw.reason, 1024, true)
68
+ || !optionalText(raw.correlationId, 256)
69
+ || (raw.eventId !== undefined
70
+ && (typeof raw.eventId !== 'string' || !EVENT_ID_RE.test(raw.eventId)))
71
+ || (raw.sourceOccurredAt !== undefined
72
+ && (typeof raw.sourceOccurredAt !== 'number' || !Number.isFinite(raw.sourceOccurredAt))))
73
+ return null;
74
+ if (raw.kind === 'set' && (typeof raw.state !== 'string' || !STATES.has(raw.state)))
75
+ return null;
76
+ const operation = raw.kind === 'set' ? {
77
+ kind: 'set',
78
+ state: raw.state,
79
+ source: copySource(raw.source),
80
+ ...(raw.message === undefined ? {} : { message: raw.message }),
81
+ ...(raw.reason === undefined ? {} : { reason: raw.reason }),
82
+ ...(raw.correlationId === undefined ? {} : { correlationId: raw.correlationId }),
83
+ ...(raw.eventId === undefined ? {} : { eventId: raw.eventId }),
84
+ ...(raw.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: raw.sourceOccurredAt }),
85
+ } : {
86
+ kind: 'clear',
87
+ source: copySource(raw.source),
88
+ ...(raw.message === undefined ? {} : { message: raw.message }),
89
+ ...(raw.reason === undefined ? {} : { reason: raw.reason }),
90
+ ...(raw.correlationId === undefined ? {} : { correlationId: raw.correlationId }),
91
+ ...(raw.eventId === undefined ? {} : { eventId: raw.eventId }),
92
+ ...(raw.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: raw.sourceOccurredAt }),
93
+ };
94
+ return {
95
+ operation,
96
+ hash: semanticHash({
97
+ kind: operation.kind,
98
+ ...(operation.kind === 'set' ? { state: operation.state } : {}),
99
+ message: operation.message,
100
+ reason: operation.reason,
101
+ correlationId: operation.correlationId,
102
+ eventId: operation.eventId,
103
+ }),
104
+ };
105
+ }
106
+ function baselineAsOperation(baseline) {
107
+ return {
108
+ kind: 'set',
109
+ state: baseline.state,
110
+ source: baseline.source,
111
+ ...(baseline.message === undefined ? {} : { message: baseline.message }),
112
+ ...(baseline.reason === undefined ? {} : { reason: baseline.reason }),
113
+ ...(baseline.correlationId === undefined ? {} : { correlationId: baseline.correlationId }),
114
+ ...(baseline.eventId === undefined ? {} : { eventId: baseline.eventId }),
115
+ ...(baseline.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: baseline.sourceOccurredAt }),
116
+ };
117
+ }
118
+ function normalizeBaseline(raw, agentId) {
119
+ if (!isRecord(raw) || !validRunRef(raw.run) || raw.run.agentId !== agentId
120
+ || !validSource(raw.source, agentId)
121
+ || typeof raw.state !== 'string' || !STATES.has(raw.state)
122
+ || !optionalText(raw.message, 4096)
123
+ || !optionalText(raw.reason, 1024)
124
+ || !optionalText(raw.correlationId, 256)
125
+ || (raw.eventId !== undefined
126
+ && (typeof raw.eventId !== 'string' || !EVENT_ID_RE.test(raw.eventId)))
127
+ || (raw.sourceOccurredAt !== undefined
128
+ && (typeof raw.sourceOccurredAt !== 'number' || !Number.isFinite(raw.sourceOccurredAt))))
129
+ return null;
130
+ const baseline = {
131
+ run: copyRunRef(raw.run),
132
+ source: copySource(raw.source),
133
+ state: raw.state,
134
+ ...(raw.message === undefined ? {} : { message: raw.message }),
135
+ ...(raw.reason === undefined ? {} : { reason: raw.reason }),
136
+ ...(raw.correlationId === undefined ? {} : { correlationId: raw.correlationId }),
137
+ ...(raw.eventId === undefined ? {} : { eventId: raw.eventId }),
138
+ ...(raw.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: raw.sourceOccurredAt }),
139
+ };
140
+ const normalized = normalizeOperation(baselineAsOperation(baseline), agentId);
141
+ return normalized ? { baseline, hash: normalized.hash } : null;
142
+ }
143
+ function parseInboxRecord(value, run) {
144
+ if (!isRecord(value) || !validSource(value.source, run.agentId)
145
+ || typeof value.state !== 'string' || !STATES.has(value.state)
146
+ || !optionalText(value.message, 4096) || !optionalText(value.reason, 1024)
147
+ || !optionalText(value.correlationId, 256)
148
+ || (value.eventId !== undefined
149
+ && (typeof value.eventId !== 'string' || !EVENT_ID_RE.test(value.eventId)))
150
+ || (value.inboxSequence !== undefined
151
+ && (!Number.isSafeInteger(value.inboxSequence) || Number(value.inboxSequence) <= 0))
152
+ || (value.acceptedAt !== undefined
153
+ && (typeof value.acceptedAt !== 'number' || !Number.isFinite(value.acceptedAt)))
154
+ || typeof value.receivedAt !== 'number' || !Number.isFinite(value.receivedAt)
155
+ || (value.sourceOccurredAt !== undefined
156
+ && (typeof value.sourceOccurredAt !== 'number' || !Number.isFinite(value.sourceOccurredAt)))) {
157
+ throw new InboxContractError('Corrupt persisted Inbox latest record');
158
+ }
159
+ return {
160
+ run: copyRunRef(run),
161
+ source: copySource(value.source),
162
+ state: value.state,
163
+ ...(value.message === undefined ? {} : { message: value.message }),
164
+ ...(value.reason === undefined ? {} : { reason: value.reason }),
165
+ ...(value.correlationId === undefined ? {} : { correlationId: value.correlationId }),
166
+ ...(value.eventId === undefined ? {} : { eventId: value.eventId }),
167
+ ...(value.inboxSequence === undefined ? {} : { inboxSequence: value.inboxSequence }),
168
+ ...(value.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: value.sourceOccurredAt }),
169
+ ...(value.acceptedAt === undefined ? {} : { acceptedAt: value.acceptedAt }),
170
+ receivedAt: value.receivedAt,
171
+ };
172
+ }
173
+ function parsePersistedState(raw) {
174
+ if (raw === null || raw === undefined) {
175
+ return { version: 1, runs: [], availability: [], terminalNotifications: [] };
176
+ }
177
+ if (!isRecord(raw) || raw.version !== 1 || !Array.isArray(raw.runs)
178
+ || !Array.isArray(raw.availability) || !Array.isArray(raw.terminalNotifications)) {
179
+ throw new InboxContractError('Unsupported or corrupt Inbox state');
180
+ }
181
+ const runIds = new Set();
182
+ const runs = raw.runs.map((value) => {
183
+ if (!isRecord(value) || !validRunRef(value.run)
184
+ || !Number.isSafeInteger(value.highWatermark) || Number(value.highWatermark) < 0
185
+ || !Array.isArray(value.sources) || !Array.isArray(value.events)) {
186
+ throw new InboxContractError('Corrupt persisted Inbox run');
187
+ }
188
+ if (runIds.has(value.run.runId))
189
+ throw new InboxContractError('Duplicate persisted Inbox runId');
190
+ runIds.add(value.run.runId);
191
+ const runRef = value.run;
192
+ const highWatermark = value.highWatermark;
193
+ const sources = value.sources.map((source) => {
194
+ if (!isRecord(source) || !validSource(source, runRef.agentId)
195
+ || typeof source.cursor !== 'string' || source.cursor.length === 0 || source.cursor.length > 1024
196
+ || typeof source.operationHash !== 'string' || !HASH_RE.test(source.operationHash)
197
+ || (source.inboxSequence !== undefined
198
+ && (!Number.isSafeInteger(source.inboxSequence) || Number(source.inboxSequence) <= 0
199
+ || Number(source.inboxSequence) > highWatermark))
200
+ || (source.acceptedAt !== undefined
201
+ && (typeof source.acceptedAt !== 'number' || !Number.isFinite(source.acceptedAt)))
202
+ || (source.eventId !== undefined
203
+ && (typeof source.eventId !== 'string' || !EVENT_ID_RE.test(source.eventId)))
204
+ || typeof source.lastReceivedAt !== 'number' || !Number.isFinite(source.lastReceivedAt)) {
205
+ throw new InboxContractError('Corrupt persisted Inbox source receipt');
206
+ }
207
+ return {
208
+ sourceId: source.sourceId,
209
+ cursor: source.cursor,
210
+ operationHash: source.operationHash,
211
+ ...(source.inboxSequence === undefined ? {} : { inboxSequence: source.inboxSequence }),
212
+ ...(source.acceptedAt === undefined ? {} : { acceptedAt: source.acceptedAt }),
213
+ ...(source.eventId === undefined ? {} : { eventId: source.eventId }),
214
+ lastReceivedAt: source.lastReceivedAt,
215
+ };
216
+ });
217
+ const events = value.events.map((event) => {
218
+ if (!isRecord(event) || typeof event.eventId !== 'string' || !EVENT_ID_RE.test(event.eventId)
219
+ || typeof event.operationHash !== 'string' || !HASH_RE.test(event.operationHash)
220
+ || !Number.isSafeInteger(event.inboxSequence) || Number(event.inboxSequence) <= 0
221
+ || Number(event.inboxSequence) > highWatermark
222
+ || typeof event.acceptedAt !== 'number' || !Number.isFinite(event.acceptedAt)
223
+ || typeof event.lastReceivedAt !== 'number' || !Number.isFinite(event.lastReceivedAt)) {
224
+ throw new InboxContractError('Corrupt persisted Inbox event receipt');
225
+ }
226
+ return {
227
+ eventId: event.eventId,
228
+ operationHash: event.operationHash,
229
+ inboxSequence: event.inboxSequence,
230
+ acceptedAt: event.acceptedAt,
231
+ lastReceivedAt: event.lastReceivedAt,
232
+ };
233
+ });
234
+ return {
235
+ run: copyRunRef(runRef),
236
+ highWatermark,
237
+ latest: value.latest === undefined ? undefined : parseInboxRecord(value.latest, runRef),
238
+ sources,
239
+ events,
240
+ };
241
+ });
242
+ const availability = raw.availability.map((value) => {
243
+ if (!isRecord(value) || typeof value.agentId !== 'string' || !AVAILABILITIES.has(value.availability)
244
+ || !optionalText(value.message, 1024))
245
+ throw new InboxContractError('Corrupt Inbox availability');
246
+ return {
247
+ agentId: value.agentId,
248
+ availability: value.availability,
249
+ ...(value.message === undefined ? {} : { message: value.message }),
250
+ };
251
+ });
252
+ const terminalNotifications = raw.terminalNotifications.map((value) => {
253
+ if (!isRecord(value) || typeof value.id !== 'string' || !validRunRef({
254
+ agentId: value.agentId, paneId: value.paneId, runId: value.runId, sessionId: value.sessionId,
255
+ }) || typeof value.eventId !== 'string' || !EVENT_ID_RE.test(value.eventId)
256
+ || (value.state !== 'done' && value.state !== 'error')
257
+ || !optionalText(value.message, 4096) || !optionalText(value.reason, 1024)
258
+ || !optionalText(value.correlationId, 256)
259
+ || typeof value.acceptedAt !== 'number' || !Number.isFinite(value.acceptedAt)
260
+ || (value.readAt !== undefined && (typeof value.readAt !== 'number' || !Number.isFinite(value.readAt)))
261
+ || typeof value.expiresAt !== 'number' || !Number.isFinite(value.expiresAt)) {
262
+ throw new InboxContractError('Corrupt terminal notification record');
263
+ }
264
+ return {
265
+ id: value.id,
266
+ agentId: value.agentId,
267
+ runId: value.runId,
268
+ ...(value.sessionId === undefined ? {} : { sessionId: value.sessionId }),
269
+ paneId: value.paneId,
270
+ eventId: value.eventId,
271
+ state: value.state,
272
+ ...(value.message === undefined ? {} : { message: value.message }),
273
+ ...(value.reason === undefined ? {} : { reason: value.reason }),
274
+ ...(value.correlationId === undefined ? {} : { correlationId: value.correlationId }),
275
+ acceptedAt: value.acceptedAt,
276
+ ...(value.readAt === undefined ? {} : { readAt: value.readAt }),
277
+ expiresAt: value.expiresAt,
278
+ };
279
+ });
280
+ return { version: 1, runs, availability, terminalNotifications };
281
+ }
282
+ export class InboxService {
283
+ serviceEpoch;
284
+ #runs;
285
+ #adapterIds;
286
+ #store;
287
+ #now;
288
+ #newNotificationId;
289
+ #boundLeases = new WeakSet();
290
+ #notificationListeners = new Set();
291
+ #state;
292
+ #runsById = new Map();
293
+ #writeTail = Promise.resolve();
294
+ #notificationTail = Promise.resolve();
295
+ #revision = 0;
296
+ constructor({ runs, adapterIds, store = new MemoryInboxStateStore(), now = Date.now, newServiceEpoch = randomUUID, newNotificationId = randomUUID, }) {
297
+ if (!runs || !Array.isArray(adapterIds) || adapterIds.length === 0) {
298
+ throw new TypeError('InboxService requires a run registry and static adapter ids');
299
+ }
300
+ this.#runs = runs;
301
+ this.#adapterIds = new Set(adapterIds);
302
+ if (this.#adapterIds.size !== adapterIds.length)
303
+ throw new TypeError('Inbox adapter ids must be unique');
304
+ this.#store = store;
305
+ this.#now = now;
306
+ this.#newNotificationId = newNotificationId;
307
+ this.serviceEpoch = newServiceEpoch();
308
+ this.#state = parsePersistedState(this.#store.load());
309
+ this.#rebuildRunIndex();
310
+ if (this.#pruneExpired(this.#now()))
311
+ this.#save();
312
+ }
313
+ projectorFor(agentId) {
314
+ this.#assertAdapter(agentId);
315
+ return Object.freeze({
316
+ forRun: (run) => this.#forRun(agentId, run),
317
+ restore: (result) => this.#restore(agentId, result),
318
+ submitTerminalReplay: (replay) => this.#submitTerminalReplay(agentId, replay),
319
+ });
320
+ }
321
+ subscribeNotifications(listener) {
322
+ if (typeof listener !== 'function')
323
+ throw new TypeError('Inbox notification listener is required');
324
+ this.#notificationListeners.add(listener);
325
+ return () => { this.#notificationListeners.delete(listener); };
326
+ }
327
+ read() {
328
+ const before = structuredClone(this.#state);
329
+ if (this.#pruneExpired(this.#now())) {
330
+ try {
331
+ this.#save();
332
+ this.#revision += 1;
333
+ }
334
+ catch {
335
+ this.#state = before;
336
+ this.#rebuildRunIndex();
337
+ throw new InboxContractError('Terminal notification expiry persistence failed');
338
+ }
339
+ }
340
+ const availability = {};
341
+ for (const entry of this.#state.availability) {
342
+ availability[entry.agentId] = {
343
+ availability: entry.availability,
344
+ ...(entry.message === undefined ? {} : { message: entry.message }),
345
+ };
346
+ }
347
+ return {
348
+ serviceEpoch: this.serviceEpoch,
349
+ revision: this.#revision,
350
+ availability,
351
+ records: this.#state.runs.flatMap((run) => run.latest ? [structuredClone(run.latest)] : []),
352
+ terminalNotifications: structuredClone(this.#state.terminalNotifications),
353
+ };
354
+ }
355
+ async markTerminalRead(notificationIds) {
356
+ if (!Array.isArray(notificationIds) || notificationIds.length === 0 || notificationIds.length > 256
357
+ || notificationIds.some((id) => !boundedString(id, 256))) {
358
+ throw new InboxContractError('Terminal notification ids must be a non-empty bounded list');
359
+ }
360
+ const ids = new Set(notificationIds);
361
+ const readAt = this.#now();
362
+ return this.#withWrite(async () => {
363
+ const before = structuredClone(this.#state);
364
+ try {
365
+ const pruned = this.#pruneExpired(readAt);
366
+ const targets = this.#state.terminalNotifications.filter((record) => ids.has(record.id));
367
+ const markedIds = [];
368
+ for (const record of this.#state.terminalNotifications) {
369
+ if (record.readAt !== undefined
370
+ || !targets.some((target) => sameTerminalEvent(target, record)))
371
+ continue;
372
+ record.readAt = readAt;
373
+ markedIds.push(record.id);
374
+ }
375
+ if (pruned || markedIds.length > 0) {
376
+ this.#save();
377
+ this.#revision += 1;
378
+ }
379
+ return {
380
+ serviceEpoch: this.serviceEpoch,
381
+ revision: this.#revision,
382
+ markedIds,
383
+ ...(markedIds.length === 0 ? {} : { readAt }),
384
+ };
385
+ }
386
+ catch {
387
+ this.#state = before;
388
+ this.#rebuildRunIndex();
389
+ throw new InboxContractError('Terminal notification read state persistence failed');
390
+ }
391
+ });
392
+ }
393
+ #assertAdapter(agentId) {
394
+ if (!this.#adapterIds.has(agentId))
395
+ throw new InboxContractError(`Unknown Inbox adapter: ${agentId}`);
396
+ }
397
+ #forRun(agentId, run) {
398
+ if (run.ref.agentId !== agentId || this.#runs.resolve(run.ref) !== run || run.signal.aborted) {
399
+ throw new InboxContractError('Inbox projector requires its scoped live Agent run lease');
400
+ }
401
+ if (!this.#boundLeases.has(run)) {
402
+ this.#boundLeases.add(run);
403
+ run.signal.addEventListener('abort', () => { void this.#invalidateRevokedRun(run.ref); }, { once: true });
404
+ }
405
+ return Object.freeze({ submit: (operation) => this.#submit(agentId, run, operation) });
406
+ }
407
+ async #withWrite(operation) {
408
+ const previous = this.#writeTail;
409
+ let release;
410
+ this.#writeTail = new Promise((resolve) => { release = resolve; });
411
+ await previous.catch(() => { });
412
+ try {
413
+ return await operation();
414
+ }
415
+ finally {
416
+ release();
417
+ }
418
+ }
419
+ #rebuildRunIndex() {
420
+ this.#runsById = new Map(this.#state.runs.map((run) => [run.run.runId, run]));
421
+ }
422
+ #save() {
423
+ this.#store.save(structuredClone(this.#state));
424
+ }
425
+ #pruneExpired(now) {
426
+ const before = this.#state.terminalNotifications.length;
427
+ this.#state.terminalNotifications = this.#state.terminalNotifications
428
+ .filter((record) => record.expiresAt > now);
429
+ return this.#state.terminalNotifications.length !== before;
430
+ }
431
+ #ensureRun(ref) {
432
+ let run = this.#runsById.get(ref.runId);
433
+ if (!run) {
434
+ run = { run: copyRunRef(ref), highWatermark: 0, latest: undefined, sources: [], events: [] };
435
+ this.#state.runs.push(run);
436
+ this.#runsById.set(ref.runId, run);
437
+ }
438
+ else {
439
+ run.run = copyRunRef(ref);
440
+ if (run.latest)
441
+ run.latest.run = copyRunRef(ref);
442
+ }
443
+ return run;
444
+ }
445
+ #nextSequence(run) {
446
+ if (run.highWatermark >= Number.MAX_SAFE_INTEGER)
447
+ throw new InboxContractError('Inbox sequence exhausted');
448
+ run.highWatermark += 1;
449
+ return run.highWatermark;
450
+ }
451
+ #queueNotification(event) {
452
+ const immutable = Object.freeze(structuredClone(event));
453
+ this.#notificationTail = this.#notificationTail.then(async () => {
454
+ for (const listener of this.#notificationListeners) {
455
+ try {
456
+ await listener(immutable);
457
+ }
458
+ catch { /* delivery is isolated from canonical persistence */ }
459
+ }
460
+ });
461
+ }
462
+ #result(receivedAt, accepted, details = {}) {
463
+ return {
464
+ accepted,
465
+ ...details,
466
+ serviceEpoch: this.serviceEpoch,
467
+ revision: this.#revision,
468
+ receivedAt,
469
+ };
470
+ }
471
+ #sourceReceipt(run, source) {
472
+ return source.cursor === undefined ? undefined : run.sources.find((receipt) => (receipt.sourceId === source.sourceId && receipt.cursor === source.cursor));
473
+ }
474
+ #crossRunEventReceipts(ref, eventId) {
475
+ if (ref.sessionId === undefined)
476
+ return [];
477
+ return this.#state.runs.flatMap((run) => (run.run.runId !== ref.runId
478
+ && run.run.agentId === ref.agentId
479
+ && run.run.sessionId === ref.sessionId
480
+ ? run.events.filter((receipt) => receipt.eventId === eventId)
481
+ : []));
482
+ }
483
+ #addSourceReceipt(run, source, hash, receivedAt, details) {
484
+ if (source.cursor === undefined)
485
+ return;
486
+ run.sources.push({
487
+ sourceId: source.sourceId,
488
+ cursor: source.cursor,
489
+ operationHash: hash,
490
+ ...(details.inboxSequence === undefined ? {} : { inboxSequence: details.inboxSequence }),
491
+ ...(details.acceptedAt === undefined ? {} : { acceptedAt: details.acceptedAt }),
492
+ ...(details.eventId === undefined ? {} : { eventId: details.eventId }),
493
+ lastReceivedAt: receivedAt,
494
+ });
495
+ }
496
+ #terminalNotification(ref, operation, acceptedAt) {
497
+ if ((operation.state !== 'done' && operation.state !== 'error') || !operation.eventId)
498
+ return null;
499
+ return {
500
+ id: this.#newNotificationId(),
501
+ agentId: ref.agentId,
502
+ runId: ref.runId,
503
+ ...(ref.sessionId === undefined ? {} : { sessionId: ref.sessionId }),
504
+ paneId: ref.paneId,
505
+ eventId: operation.eventId,
506
+ state: operation.state,
507
+ ...(typeof operation.message === 'string' ? { message: operation.message } : {}),
508
+ ...(typeof operation.reason === 'string' ? { reason: operation.reason } : {}),
509
+ ...(operation.correlationId === undefined ? {} : { correlationId: operation.correlationId }),
510
+ acceptedAt,
511
+ expiresAt: acceptedAt + TERMINAL_RETENTION_MS,
512
+ };
513
+ }
514
+ async #submit(agentId, lease, raw) {
515
+ const receivedAt = this.#now();
516
+ const normalized = normalizeOperation(raw, agentId);
517
+ if (!normalized)
518
+ return this.#result(receivedAt, false, { reason: 'invalid_operation' });
519
+ if (this.#runs.resolve(lease.ref) !== lease || lease.signal.aborted) {
520
+ return this.#result(receivedAt, false, { reason: 'stale_lease' });
521
+ }
522
+ return this.#withWrite(async () => {
523
+ if (this.#runs.resolve(lease.ref) !== lease || lease.signal.aborted) {
524
+ return this.#result(receivedAt, false, { reason: 'stale_lease' });
525
+ }
526
+ const before = structuredClone(this.#state);
527
+ const run = this.#ensureRun(lease.ref);
528
+ const sourceDuplicate = this.#sourceReceipt(run, normalized.operation.source);
529
+ if (sourceDuplicate) {
530
+ if (sourceDuplicate.operationHash !== normalized.hash) {
531
+ this.#state = before;
532
+ this.#rebuildRunIndex();
533
+ return this.#result(receivedAt, false, { reason: 'invalid_operation' });
534
+ }
535
+ sourceDuplicate.lastReceivedAt = receivedAt;
536
+ if (run.latest && (run.latest.inboxSequence === sourceDuplicate.inboxSequence
537
+ || (sourceDuplicate.inboxSequence === undefined
538
+ && run.latest.source.sourceId === sourceDuplicate.sourceId
539
+ && run.latest.source.cursor === sourceDuplicate.cursor))) {
540
+ run.latest.receivedAt = receivedAt;
541
+ }
542
+ try {
543
+ this.#save();
544
+ }
545
+ catch {
546
+ this.#state = before;
547
+ this.#rebuildRunIndex();
548
+ return this.#result(receivedAt, false, { reason: 'persistence_failed' });
549
+ }
550
+ return this.#result(receivedAt, true, {
551
+ reason: 'duplicate_source',
552
+ ...(sourceDuplicate.inboxSequence === undefined ? {} : { inboxSequence: sourceDuplicate.inboxSequence }),
553
+ ...(sourceDuplicate.acceptedAt === undefined ? {} : { acceptedAt: sourceDuplicate.acceptedAt }),
554
+ });
555
+ }
556
+ const eventId = normalized.operation.eventId;
557
+ const eventDuplicate = eventId ? run.events.find((receipt) => receipt.eventId === eventId) : undefined;
558
+ if (eventDuplicate) {
559
+ if (eventDuplicate.operationHash !== normalized.hash) {
560
+ this.#state = before;
561
+ this.#rebuildRunIndex();
562
+ return this.#result(receivedAt, false, { reason: 'invalid_operation' });
563
+ }
564
+ eventDuplicate.lastReceivedAt = receivedAt;
565
+ if (run.latest?.inboxSequence === eventDuplicate.inboxSequence)
566
+ run.latest.receivedAt = receivedAt;
567
+ this.#addSourceReceipt(run, normalized.operation.source, normalized.hash, receivedAt, {
568
+ inboxSequence: eventDuplicate.inboxSequence,
569
+ acceptedAt: eventDuplicate.acceptedAt,
570
+ ...(eventId === undefined ? {} : { eventId }),
571
+ });
572
+ try {
573
+ this.#save();
574
+ }
575
+ catch {
576
+ this.#state = before;
577
+ this.#rebuildRunIndex();
578
+ return this.#result(receivedAt, false, { reason: 'persistence_failed' });
579
+ }
580
+ return this.#result(receivedAt, true, {
581
+ reason: 'duplicate_event',
582
+ inboxSequence: eventDuplicate.inboxSequence,
583
+ acceptedAt: eventDuplicate.acceptedAt,
584
+ });
585
+ }
586
+ // `runId` is process-local and is regenerated when Handmux restarts. A provider event remains the
587
+ // same durable fact inside its native session, so replaying it under the replacement run must inherit
588
+ // the original acceptance instead of creating a fresh unread notification at restart time.
589
+ const historicalEvents = eventId
590
+ ? this.#crossRunEventReceipts(lease.ref, eventId) : [];
591
+ if (eventId && historicalEvents.length > 0) {
592
+ if (historicalEvents.some((receipt) => receipt.operationHash !== normalized.hash)) {
593
+ this.#state = before;
594
+ this.#rebuildRunIndex();
595
+ return this.#result(receivedAt, false, { reason: 'invalid_operation' });
596
+ }
597
+ try {
598
+ const acceptedAt = Math.min(...historicalEvents.map((receipt) => receipt.acceptedAt));
599
+ const inboxSequence = this.#nextSequence(run);
600
+ if (normalized.operation.kind === 'clear') {
601
+ run.latest = undefined;
602
+ }
603
+ else {
604
+ const previous = run.latest;
605
+ const message = normalized.operation.message === undefined
606
+ ? previous?.message : normalized.operation.message ?? undefined;
607
+ const reason = normalized.operation.reason === undefined
608
+ ? previous?.reason : normalized.operation.reason ?? undefined;
609
+ const correlationId = normalized.operation.correlationId ?? previous?.correlationId;
610
+ run.latest = {
611
+ run: copyRunRef(lease.ref),
612
+ source: copySource(normalized.operation.source),
613
+ state: normalized.operation.state,
614
+ ...(message === undefined ? {} : { message }),
615
+ ...(reason === undefined ? {} : { reason }),
616
+ ...(correlationId === undefined ? {} : { correlationId }),
617
+ eventId,
618
+ inboxSequence,
619
+ ...(normalized.operation.sourceOccurredAt === undefined
620
+ ? {} : { sourceOccurredAt: normalized.operation.sourceOccurredAt }),
621
+ acceptedAt,
622
+ receivedAt,
623
+ };
624
+ }
625
+ run.events.push({
626
+ eventId, operationHash: normalized.hash, inboxSequence, acceptedAt, lastReceivedAt: receivedAt,
627
+ });
628
+ this.#addSourceReceipt(run, normalized.operation.source, normalized.hash, receivedAt, {
629
+ inboxSequence, acceptedAt, eventId,
630
+ });
631
+ this.#pruneExpired(receivedAt);
632
+ this.#save();
633
+ this.#revision += 1;
634
+ return this.#result(receivedAt, true, {
635
+ reason: 'duplicate_event', inboxSequence, acceptedAt,
636
+ });
637
+ }
638
+ catch (error) {
639
+ this.#state = before;
640
+ this.#rebuildRunIndex();
641
+ return this.#result(receivedAt, false, {
642
+ reason: error instanceof InboxContractError ? 'invalid_operation' : 'persistence_failed',
643
+ });
644
+ }
645
+ }
646
+ try {
647
+ const inboxSequence = this.#nextSequence(run);
648
+ const acceptedAt = eventId ? receivedAt : undefined;
649
+ if (normalized.operation.kind === 'clear') {
650
+ run.latest = undefined;
651
+ }
652
+ else {
653
+ const previous = run.latest;
654
+ const message = normalized.operation.message === undefined
655
+ ? previous?.message : normalized.operation.message ?? undefined;
656
+ const reason = normalized.operation.reason === undefined
657
+ ? previous?.reason : normalized.operation.reason ?? undefined;
658
+ const correlationId = normalized.operation.correlationId ?? previous?.correlationId;
659
+ run.latest = {
660
+ run: copyRunRef(lease.ref),
661
+ source: copySource(normalized.operation.source),
662
+ state: normalized.operation.state,
663
+ ...(message === undefined ? {} : { message }),
664
+ ...(reason === undefined ? {} : { reason }),
665
+ ...(correlationId === undefined ? {} : { correlationId }),
666
+ ...(eventId === undefined ? {} : { eventId }),
667
+ inboxSequence,
668
+ ...(normalized.operation.sourceOccurredAt === undefined
669
+ ? {} : { sourceOccurredAt: normalized.operation.sourceOccurredAt }),
670
+ ...(acceptedAt === undefined ? {} : { acceptedAt }),
671
+ receivedAt,
672
+ };
673
+ const terminal = acceptedAt === undefined
674
+ ? null : this.#terminalNotification(lease.ref, run.latest, acceptedAt);
675
+ if (terminal)
676
+ this.#state.terminalNotifications.push(terminal);
677
+ }
678
+ this.#addSourceReceipt(run, normalized.operation.source, normalized.hash, receivedAt, {
679
+ inboxSequence,
680
+ ...(acceptedAt === undefined ? {} : { acceptedAt }),
681
+ ...(eventId === undefined ? {} : { eventId }),
682
+ });
683
+ if (eventId && acceptedAt !== undefined) {
684
+ run.events.push({
685
+ eventId,
686
+ operationHash: normalized.hash,
687
+ inboxSequence,
688
+ acceptedAt,
689
+ lastReceivedAt: receivedAt,
690
+ });
691
+ }
692
+ this.#pruneExpired(receivedAt);
693
+ this.#save();
694
+ this.#revision += 1;
695
+ if (run.latest?.acceptedAt !== undefined && run.latest.eventId
696
+ && run.latest.state !== 'working') {
697
+ const terminal = this.#state.terminalNotifications.find((item) => (item.runId === lease.ref.runId && item.eventId === run.latest.eventId));
698
+ this.#queueNotification({
699
+ run: copyRunRef(lease.ref),
700
+ state: run.latest.state,
701
+ eventId: run.latest.eventId,
702
+ ...(run.latest.message === undefined ? {} : { message: run.latest.message }),
703
+ ...(run.latest.reason === undefined ? {} : { reason: run.latest.reason }),
704
+ ...(run.latest.correlationId === undefined ? {} : { correlationId: run.latest.correlationId }),
705
+ acceptedAt: run.latest.acceptedAt,
706
+ ...(terminal === undefined ? {} : { terminalNotificationId: terminal.id }),
707
+ });
708
+ }
709
+ return this.#result(receivedAt, true, {
710
+ inboxSequence,
711
+ ...(acceptedAt === undefined ? {} : { acceptedAt }),
712
+ });
713
+ }
714
+ catch (error) {
715
+ this.#state = before;
716
+ this.#rebuildRunIndex();
717
+ return this.#result(receivedAt, false, {
718
+ reason: error instanceof InboxContractError ? 'invalid_operation' : 'persistence_failed',
719
+ });
720
+ }
721
+ });
722
+ }
723
+ async #restore(agentId, result) {
724
+ if (!isRecord(result) || !AVAILABILITIES.has(result.availability)
725
+ || !optionalText(result.message, 1024)
726
+ || (result.snapshot !== undefined && !Array.isArray(result.snapshot))
727
+ || (result.availability === 'unavailable' && result.snapshot !== undefined)) {
728
+ throw new InboxContractError('Invalid Inbox restore result');
729
+ }
730
+ const baselines = (result.snapshot ?? []).map((baseline) => normalizeBaseline(baseline, agentId));
731
+ if (baselines.some((baseline) => baseline === null))
732
+ throw new InboxContractError('Invalid Inbox baseline');
733
+ const normalized = baselines;
734
+ if (new Set(normalized.map((item) => item.baseline.run.runId)).size !== normalized.length) {
735
+ throw new InboxContractError('Inbox restore contains duplicate run baselines');
736
+ }
737
+ for (const item of normalized) {
738
+ if (this.#runs.status(item.baseline.run) !== 'current') {
739
+ throw new InboxContractError('Inbox restore baseline requires a current verified run');
740
+ }
741
+ }
742
+ const receivedAt = this.#now();
743
+ return this.#withWrite(async () => {
744
+ const before = structuredClone(this.#state);
745
+ try {
746
+ const availability = {
747
+ agentId,
748
+ availability: result.availability,
749
+ ...(result.message === undefined ? {} : { message: result.message }),
750
+ };
751
+ const availabilityIndex = this.#state.availability.findIndex((entry) => entry.agentId === agentId);
752
+ if (availabilityIndex === -1)
753
+ this.#state.availability.push(availability);
754
+ else
755
+ this.#state.availability[availabilityIndex] = availability;
756
+ if (result.availability !== 'unavailable') {
757
+ const restoredRunIds = new Set();
758
+ for (const item of normalized) {
759
+ const baseline = item.baseline;
760
+ restoredRunIds.add(baseline.run.runId);
761
+ const run = this.#ensureRun(baseline.run);
762
+ let eventReceipt = baseline.eventId
763
+ ? run.events.find((receipt) => receipt.eventId === baseline.eventId)
764
+ : undefined;
765
+ if (!eventReceipt && baseline.eventId) {
766
+ const historical = this.#crossRunEventReceipts(baseline.run, baseline.eventId)
767
+ .sort((left, right) => left.acceptedAt - right.acceptedAt)[0];
768
+ if (historical) {
769
+ eventReceipt = {
770
+ eventId: baseline.eventId,
771
+ operationHash: item.hash,
772
+ inboxSequence: this.#nextSequence(run),
773
+ acceptedAt: historical.acceptedAt,
774
+ lastReceivedAt: receivedAt,
775
+ };
776
+ run.events.push(eventReceipt);
777
+ }
778
+ }
779
+ run.latest = {
780
+ run: copyRunRef(baseline.run),
781
+ source: copySource(baseline.source),
782
+ state: baseline.state,
783
+ ...(baseline.message === undefined ? {} : { message: baseline.message }),
784
+ ...(baseline.reason === undefined ? {} : { reason: baseline.reason }),
785
+ ...(baseline.correlationId === undefined ? {} : { correlationId: baseline.correlationId }),
786
+ ...(baseline.eventId === undefined ? {} : { eventId: baseline.eventId }),
787
+ ...(eventReceipt === undefined ? {} : {
788
+ inboxSequence: eventReceipt.inboxSequence,
789
+ acceptedAt: eventReceipt.acceptedAt,
790
+ }),
791
+ ...(baseline.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: baseline.sourceOccurredAt }),
792
+ receivedAt,
793
+ };
794
+ const existingSource = this.#sourceReceipt(run, baseline.source);
795
+ if (existingSource) {
796
+ existingSource.operationHash = item.hash;
797
+ existingSource.lastReceivedAt = receivedAt;
798
+ }
799
+ else {
800
+ this.#addSourceReceipt(run, baseline.source, item.hash, receivedAt, {
801
+ ...(eventReceipt === undefined ? {} : {
802
+ inboxSequence: eventReceipt.inboxSequence,
803
+ acceptedAt: eventReceipt.acceptedAt,
804
+ }),
805
+ ...(baseline.eventId === undefined ? {} : { eventId: baseline.eventId }),
806
+ });
807
+ }
808
+ }
809
+ if (result.availability === 'ready') {
810
+ for (const run of this.#state.runs) {
811
+ if (run.run.agentId === agentId && !restoredRunIds.has(run.run.runId))
812
+ run.latest = undefined;
813
+ }
814
+ }
815
+ }
816
+ this.#pruneExpired(receivedAt);
817
+ this.#save();
818
+ this.#revision += 1;
819
+ return {
820
+ availability: result.availability,
821
+ serviceEpoch: this.serviceEpoch,
822
+ revision: this.#revision,
823
+ };
824
+ }
825
+ catch (error) {
826
+ this.#state = before;
827
+ this.#rebuildRunIndex();
828
+ if (error instanceof InboxContractError)
829
+ throw error;
830
+ throw new InboxContractError('Inbox restore persistence failed');
831
+ }
832
+ });
833
+ }
834
+ async #submitTerminalReplay(agentId, replay) {
835
+ const receivedAt = this.#now();
836
+ if (!isRecord(replay) || !validRunRef(replay.run) || replay.run.agentId !== agentId
837
+ || this.#runs.status(replay.run) !== 'revoked'
838
+ || (replay.state !== 'done' && replay.state !== 'error')) {
839
+ return this.#result(receivedAt, false, { reason: 'invalid_operation' });
840
+ }
841
+ const normalized = normalizeOperation({
842
+ kind: 'set',
843
+ state: replay.state,
844
+ source: replay.source,
845
+ ...(replay.message === undefined ? {} : { message: replay.message }),
846
+ ...(replay.reason === undefined ? {} : { reason: replay.reason }),
847
+ ...(replay.correlationId === undefined ? {} : { correlationId: replay.correlationId }),
848
+ eventId: replay.eventId,
849
+ ...(replay.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: replay.sourceOccurredAt }),
850
+ }, agentId);
851
+ if (!normalized || normalized.operation.kind !== 'set') {
852
+ return this.#result(receivedAt, false, { reason: 'invalid_operation' });
853
+ }
854
+ const operation = normalized.operation;
855
+ return this.#withWrite(async () => {
856
+ const before = structuredClone(this.#state);
857
+ const run = this.#ensureRun(replay.run);
858
+ const sourceDuplicate = this.#sourceReceipt(run, operation.source);
859
+ if (sourceDuplicate) {
860
+ if (sourceDuplicate.operationHash !== normalized.hash) {
861
+ this.#state = before;
862
+ this.#rebuildRunIndex();
863
+ return this.#result(receivedAt, false, { reason: 'invalid_operation' });
864
+ }
865
+ sourceDuplicate.lastReceivedAt = receivedAt;
866
+ try {
867
+ this.#save();
868
+ }
869
+ catch {
870
+ this.#state = before;
871
+ this.#rebuildRunIndex();
872
+ return this.#result(receivedAt, false, { reason: 'persistence_failed' });
873
+ }
874
+ return this.#result(receivedAt, true, {
875
+ reason: 'duplicate_source',
876
+ ...(sourceDuplicate.inboxSequence === undefined ? {} : { inboxSequence: sourceDuplicate.inboxSequence }),
877
+ ...(sourceDuplicate.acceptedAt === undefined ? {} : { acceptedAt: sourceDuplicate.acceptedAt }),
878
+ });
879
+ }
880
+ const eventDuplicate = run.events.find((receipt) => receipt.eventId === operation.eventId);
881
+ if (eventDuplicate) {
882
+ if (eventDuplicate.operationHash !== normalized.hash) {
883
+ this.#state = before;
884
+ this.#rebuildRunIndex();
885
+ return this.#result(receivedAt, false, { reason: 'invalid_operation' });
886
+ }
887
+ eventDuplicate.lastReceivedAt = receivedAt;
888
+ this.#addSourceReceipt(run, operation.source, normalized.hash, receivedAt, {
889
+ inboxSequence: eventDuplicate.inboxSequence,
890
+ acceptedAt: eventDuplicate.acceptedAt,
891
+ ...(operation.eventId === undefined ? {} : { eventId: operation.eventId }),
892
+ });
893
+ try {
894
+ this.#save();
895
+ }
896
+ catch {
897
+ this.#state = before;
898
+ this.#rebuildRunIndex();
899
+ return this.#result(receivedAt, false, { reason: 'persistence_failed' });
900
+ }
901
+ return this.#result(receivedAt, true, {
902
+ reason: 'duplicate_event',
903
+ inboxSequence: eventDuplicate.inboxSequence,
904
+ acceptedAt: eventDuplicate.acceptedAt,
905
+ });
906
+ }
907
+ try {
908
+ const inboxSequence = this.#nextSequence(run);
909
+ const acceptedAt = receivedAt;
910
+ run.events.push({
911
+ eventId: operation.eventId,
912
+ operationHash: normalized.hash,
913
+ inboxSequence,
914
+ acceptedAt,
915
+ lastReceivedAt: receivedAt,
916
+ });
917
+ this.#addSourceReceipt(run, operation.source, normalized.hash, receivedAt, {
918
+ inboxSequence,
919
+ acceptedAt,
920
+ ...(operation.eventId === undefined ? {} : { eventId: operation.eventId }),
921
+ });
922
+ const terminal = this.#terminalNotification(replay.run, {
923
+ state: operation.state,
924
+ ...(operation.eventId === undefined ? {} : { eventId: operation.eventId }),
925
+ ...(typeof operation.message === 'string' ? { message: operation.message } : {}),
926
+ ...(typeof operation.reason === 'string' ? { reason: operation.reason } : {}),
927
+ ...(operation.correlationId === undefined ? {} : { correlationId: operation.correlationId }),
928
+ }, acceptedAt);
929
+ if (!terminal)
930
+ throw new InboxContractError('Terminal replay did not produce a notification');
931
+ this.#state.terminalNotifications.push(terminal);
932
+ this.#pruneExpired(receivedAt);
933
+ this.#save();
934
+ this.#revision += 1;
935
+ this.#queueNotification({
936
+ run: copyRunRef(replay.run),
937
+ state: replay.state,
938
+ eventId: operation.eventId,
939
+ ...(typeof operation.message === 'string' ? { message: operation.message } : {}),
940
+ ...(typeof operation.reason === 'string' ? { reason: operation.reason } : {}),
941
+ ...(operation.correlationId === undefined ? {} : { correlationId: operation.correlationId }),
942
+ acceptedAt,
943
+ terminalNotificationId: terminal.id,
944
+ });
945
+ return this.#result(receivedAt, true, { inboxSequence, acceptedAt });
946
+ }
947
+ catch (error) {
948
+ this.#state = before;
949
+ this.#rebuildRunIndex();
950
+ return this.#result(receivedAt, false, {
951
+ reason: error instanceof InboxContractError ? 'invalid_operation' : 'persistence_failed',
952
+ });
953
+ }
954
+ });
955
+ }
956
+ async #invalidateRevokedRun(ref) {
957
+ await this.#withWrite(async () => {
958
+ const run = this.#runsById.get(ref.runId);
959
+ if (!run?.latest || (run.latest.state !== 'working' && run.latest.state !== 'waiting'))
960
+ return;
961
+ const before = structuredClone(this.#state);
962
+ try {
963
+ run.latest = undefined;
964
+ this.#save();
965
+ this.#revision += 1;
966
+ }
967
+ catch {
968
+ this.#state = before;
969
+ this.#rebuildRunIndex();
970
+ }
971
+ });
972
+ }
973
+ }