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.
- package/README.md +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const DEFAULT_MAX_BUFFERED_EVENTS = 256;
|
|
2
|
+
const DEFAULT_IDLE_GRACE_MS = 5_000;
|
|
3
|
+
export class InteractionLiveHubError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = 'InteractionLiveHubError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
class EventQueue {
|
|
12
|
+
checkpoint;
|
|
13
|
+
#maxBufferedEvents;
|
|
14
|
+
#detach;
|
|
15
|
+
#values = [];
|
|
16
|
+
#waiting;
|
|
17
|
+
#closed = false;
|
|
18
|
+
#detached = false;
|
|
19
|
+
constructor(checkpoint, maxBufferedEvents, detach) {
|
|
20
|
+
this.checkpoint = Object.freeze(structuredClone(checkpoint));
|
|
21
|
+
this.#maxBufferedEvents = maxBufferedEvents;
|
|
22
|
+
this.#detach = detach;
|
|
23
|
+
}
|
|
24
|
+
[Symbol.asyncIterator]() { return this; }
|
|
25
|
+
next() {
|
|
26
|
+
const value = this.#values.shift();
|
|
27
|
+
if (value)
|
|
28
|
+
return Promise.resolve({ value: structuredClone(value), done: false });
|
|
29
|
+
if (this.#closed)
|
|
30
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
31
|
+
return new Promise((resolve) => { this.#waiting = resolve; });
|
|
32
|
+
}
|
|
33
|
+
push(event) {
|
|
34
|
+
if (this.#closed)
|
|
35
|
+
return;
|
|
36
|
+
const cloned = structuredClone(event);
|
|
37
|
+
if (this.#waiting) {
|
|
38
|
+
const waiting = this.#waiting;
|
|
39
|
+
this.#waiting = undefined;
|
|
40
|
+
waiting({ value: cloned, done: false });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (this.#values.length >= this.#maxBufferedEvents) {
|
|
44
|
+
this.close();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.#values.push(cloned);
|
|
48
|
+
}
|
|
49
|
+
finishAfterDrain() {
|
|
50
|
+
if (this.#closed)
|
|
51
|
+
return;
|
|
52
|
+
this.#closed = true;
|
|
53
|
+
this.#detachOnce();
|
|
54
|
+
if (!this.#values.length && this.#waiting) {
|
|
55
|
+
const waiting = this.#waiting;
|
|
56
|
+
this.#waiting = undefined;
|
|
57
|
+
waiting({ value: undefined, done: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
close() {
|
|
61
|
+
if (this.#closed)
|
|
62
|
+
return;
|
|
63
|
+
this.#closed = true;
|
|
64
|
+
this.#values.splice(0);
|
|
65
|
+
this.#detachOnce();
|
|
66
|
+
if (this.#waiting) {
|
|
67
|
+
const waiting = this.#waiting;
|
|
68
|
+
this.#waiting = undefined;
|
|
69
|
+
waiting({ value: undefined, done: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
#detachOnce() {
|
|
73
|
+
if (this.#detached)
|
|
74
|
+
return;
|
|
75
|
+
this.#detached = true;
|
|
76
|
+
this.#detach();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// App-level fan-out only. InteractionService remains the owner of native observation, persistence,
|
|
80
|
+
// revision ordering, resolution tokens, and first-response-wins semantics.
|
|
81
|
+
export class InteractionLiveHub {
|
|
82
|
+
#interaction;
|
|
83
|
+
#maxBufferedEvents;
|
|
84
|
+
#idleGraceMs;
|
|
85
|
+
#observations = new Map();
|
|
86
|
+
#closed = false;
|
|
87
|
+
constructor({ interaction, maxBufferedEvents = DEFAULT_MAX_BUFFERED_EVENTS, idleGraceMs = DEFAULT_IDLE_GRACE_MS, }) {
|
|
88
|
+
if (!interaction || typeof interaction.open !== 'function'
|
|
89
|
+
|| !Number.isSafeInteger(maxBufferedEvents) || maxBufferedEvents <= 0
|
|
90
|
+
|| !Number.isSafeInteger(idleGraceMs) || idleGraceMs < 0) {
|
|
91
|
+
throw new TypeError('Interaction live hub requires a service and bounded queues');
|
|
92
|
+
}
|
|
93
|
+
this.#interaction = interaction;
|
|
94
|
+
this.#maxBufferedEvents = maxBufferedEvents;
|
|
95
|
+
this.#idleGraceMs = idleGraceMs;
|
|
96
|
+
}
|
|
97
|
+
async subscribe(run) {
|
|
98
|
+
if (this.#closed)
|
|
99
|
+
throw new InteractionLiveHubError('closed', 'Interaction live hub is closed');
|
|
100
|
+
let shared = this.#observations.get(run.ref.runId);
|
|
101
|
+
if (!shared)
|
|
102
|
+
shared = this.#create(run);
|
|
103
|
+
if (shared.run !== run || shared.terminated) {
|
|
104
|
+
throw new InteractionLiveHubError('stale_run', 'Interaction live run is no longer current');
|
|
105
|
+
}
|
|
106
|
+
if (shared.idleTimer) {
|
|
107
|
+
clearTimeout(shared.idleTimer);
|
|
108
|
+
shared.idleTimer = undefined;
|
|
109
|
+
}
|
|
110
|
+
await shared.ready;
|
|
111
|
+
if (!shared.checkpoint || shared.terminated) {
|
|
112
|
+
throw new InteractionLiveHubError('unavailable', 'Interaction live observation is unavailable');
|
|
113
|
+
}
|
|
114
|
+
let subscription;
|
|
115
|
+
subscription = new EventQueue(shared.checkpoint, this.#maxBufferedEvents, () => {
|
|
116
|
+
this.#detach(shared, subscription);
|
|
117
|
+
});
|
|
118
|
+
shared.subscribers.add(subscription);
|
|
119
|
+
return subscription;
|
|
120
|
+
}
|
|
121
|
+
close() {
|
|
122
|
+
if (this.#closed)
|
|
123
|
+
return;
|
|
124
|
+
this.#closed = true;
|
|
125
|
+
for (const shared of [...this.#observations.values()])
|
|
126
|
+
this.#terminate(shared);
|
|
127
|
+
}
|
|
128
|
+
#create(run) {
|
|
129
|
+
const shared = {
|
|
130
|
+
run,
|
|
131
|
+
ready: Promise.resolve(),
|
|
132
|
+
subscribers: new Set(),
|
|
133
|
+
idleTimer: undefined,
|
|
134
|
+
terminated: false,
|
|
135
|
+
};
|
|
136
|
+
this.#observations.set(run.ref.runId, shared);
|
|
137
|
+
shared.ready = this.#interaction.open(run, (event) => this.#publish(shared, event)).then((handle) => {
|
|
138
|
+
if (shared.terminated) {
|
|
139
|
+
try {
|
|
140
|
+
void Promise.resolve(handle.close()).catch(() => { });
|
|
141
|
+
}
|
|
142
|
+
catch { /* best effort */ }
|
|
143
|
+
throw new Error('Interaction live run was revoked during open');
|
|
144
|
+
}
|
|
145
|
+
shared.handle = handle;
|
|
146
|
+
shared.checkpoint = {
|
|
147
|
+
revision: handle.revision,
|
|
148
|
+
pending: structuredClone(handle.pending),
|
|
149
|
+
};
|
|
150
|
+
void handle.closed.then(() => this.#terminate(shared, true));
|
|
151
|
+
}).catch((error) => {
|
|
152
|
+
this.#terminate(shared);
|
|
153
|
+
throw error;
|
|
154
|
+
});
|
|
155
|
+
return shared;
|
|
156
|
+
}
|
|
157
|
+
#publish(shared, event) {
|
|
158
|
+
if (shared.terminated)
|
|
159
|
+
return;
|
|
160
|
+
const checkpoint = shared.checkpoint;
|
|
161
|
+
if (!checkpoint || !Number.isSafeInteger(event.revision)
|
|
162
|
+
|| event.revision !== checkpoint.revision + 1) {
|
|
163
|
+
this.#terminate(shared);
|
|
164
|
+
throw new InteractionLiveHubError('unavailable', 'Interaction revision is invalid');
|
|
165
|
+
}
|
|
166
|
+
const pending = new Map(checkpoint.pending.map((item) => [item.id, item]));
|
|
167
|
+
if (event.type === 'opened') {
|
|
168
|
+
if (event.interaction.runId !== shared.run.ref.runId || pending.has(event.interaction.id)) {
|
|
169
|
+
this.#terminate(shared);
|
|
170
|
+
throw new InteractionLiveHubError('unavailable', 'Interaction opened event is invalid');
|
|
171
|
+
}
|
|
172
|
+
pending.set(event.interaction.id, structuredClone(event.interaction));
|
|
173
|
+
}
|
|
174
|
+
else if (!pending.delete(event.interactionId)) {
|
|
175
|
+
this.#terminate(shared);
|
|
176
|
+
throw new InteractionLiveHubError('unavailable', 'Interaction terminal event is invalid');
|
|
177
|
+
}
|
|
178
|
+
shared.checkpoint = {
|
|
179
|
+
revision: event.revision,
|
|
180
|
+
pending: [...pending.values()],
|
|
181
|
+
};
|
|
182
|
+
for (const subscriber of [...shared.subscribers])
|
|
183
|
+
subscriber.push(event);
|
|
184
|
+
}
|
|
185
|
+
#detach(shared, subscription) {
|
|
186
|
+
shared.subscribers.delete(subscription);
|
|
187
|
+
this.#idle(shared);
|
|
188
|
+
}
|
|
189
|
+
#idle(shared) {
|
|
190
|
+
if (shared.terminated || shared.subscribers.size || shared.idleTimer)
|
|
191
|
+
return;
|
|
192
|
+
if (this.#idleGraceMs === 0) {
|
|
193
|
+
this.#terminate(shared);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
shared.idleTimer = setTimeout(() => {
|
|
197
|
+
shared.idleTimer = undefined;
|
|
198
|
+
if (!shared.subscribers.size)
|
|
199
|
+
this.#terminate(shared);
|
|
200
|
+
}, this.#idleGraceMs);
|
|
201
|
+
shared.idleTimer.unref?.();
|
|
202
|
+
}
|
|
203
|
+
#terminate(shared, drain = false) {
|
|
204
|
+
if (shared.terminated)
|
|
205
|
+
return;
|
|
206
|
+
shared.terminated = true;
|
|
207
|
+
if (this.#observations.get(shared.run.ref.runId) === shared) {
|
|
208
|
+
this.#observations.delete(shared.run.ref.runId);
|
|
209
|
+
}
|
|
210
|
+
if (shared.idleTimer)
|
|
211
|
+
clearTimeout(shared.idleTimer);
|
|
212
|
+
shared.idleTimer = undefined;
|
|
213
|
+
for (const subscriber of [...shared.subscribers]) {
|
|
214
|
+
if (drain)
|
|
215
|
+
subscriber.finishAfterDrain();
|
|
216
|
+
else
|
|
217
|
+
subscriber.close();
|
|
218
|
+
}
|
|
219
|
+
shared.subscribers.clear();
|
|
220
|
+
try {
|
|
221
|
+
void Promise.resolve(shared.handle?.close()).catch(() => { });
|
|
222
|
+
}
|
|
223
|
+
catch { /* close is best effort */ }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PrivateStateStore } from '../privateStateStore.js';
|
|
2
|
+
export class MemoryInteractionStateStore {
|
|
3
|
+
#state = null;
|
|
4
|
+
load() {
|
|
5
|
+
return this.#state === null ? null : structuredClone(this.#state);
|
|
6
|
+
}
|
|
7
|
+
save(state) {
|
|
8
|
+
this.#state = structuredClone(state);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class FileInteractionStateStore {
|
|
12
|
+
#store;
|
|
13
|
+
constructor(file) {
|
|
14
|
+
this.#store = new PrivateStateStore(file);
|
|
15
|
+
}
|
|
16
|
+
load() { return this.#store.readStrict(); }
|
|
17
|
+
save(state) { this.#store.write(state); }
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const LEGACY_KIND = {
|
|
2
|
+
working: 'working',
|
|
3
|
+
waiting: 'permission',
|
|
4
|
+
done: 'done',
|
|
5
|
+
error: 'error',
|
|
6
|
+
};
|
|
7
|
+
const LEGACY_MESSAGE = {
|
|
8
|
+
working: '进行中',
|
|
9
|
+
waiting: '需要你',
|
|
10
|
+
done: '已完成',
|
|
11
|
+
error: '出错',
|
|
12
|
+
};
|
|
13
|
+
function sameRun(record, run) {
|
|
14
|
+
return record.run.agentId === run.agentId
|
|
15
|
+
&& record.run.paneId === run.paneId
|
|
16
|
+
&& record.run.runId === run.runId
|
|
17
|
+
&& record.run.sessionId === run.sessionId;
|
|
18
|
+
}
|
|
19
|
+
function sameTerminalEvent(candidate, run, eventId) {
|
|
20
|
+
if (candidate.agentId !== run.agentId || candidate.eventId !== eventId)
|
|
21
|
+
return false;
|
|
22
|
+
if (candidate.sessionId !== undefined && run.sessionId !== undefined) {
|
|
23
|
+
return candidate.sessionId === run.sessionId;
|
|
24
|
+
}
|
|
25
|
+
return candidate.runId === run.runId && candidate.paneId === run.paneId;
|
|
26
|
+
}
|
|
27
|
+
// Temporary compatibility projection for the existing phone roster. Runtime/Inbox remain the only
|
|
28
|
+
// identity and state authorities; tmux contributes presentation location only.
|
|
29
|
+
export function projectLegacyInboxStates({ snapshot, runs, panes, allowedSessions, }) {
|
|
30
|
+
const allowed = allowedSessions === null ? null : new Set(allowedSessions);
|
|
31
|
+
const paneById = new Map(panes.map((pane) => [pane.paneId, pane]));
|
|
32
|
+
const recordByRun = new Map(snapshot.records.map((record) => [record.run.runId, record]));
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const run of runs) {
|
|
35
|
+
const pane = paneById.get(run.paneId);
|
|
36
|
+
if (allowed && (!pane || !allowed.has(pane.sessionName)))
|
|
37
|
+
continue;
|
|
38
|
+
const record = recordByRun.get(run.runId);
|
|
39
|
+
const current = record && sameRun(record, run) ? record : undefined;
|
|
40
|
+
const terminal = current && (current.state === 'done' || current.state === 'error');
|
|
41
|
+
const notifications = terminal && current.eventId
|
|
42
|
+
? snapshot.terminalNotifications.filter((candidate) => (sameTerminalEvent(candidate, run, current.eventId))) : [];
|
|
43
|
+
// One event may have legacy duplicates from Server restarts. Any read copy makes the durable event
|
|
44
|
+
// read; otherwise keep one canonical notification id so a click marks every duplicate together.
|
|
45
|
+
const notification = notifications.find((candidate) => candidate.readAt !== undefined)
|
|
46
|
+
?? notifications[0];
|
|
47
|
+
const displayTime = terminal
|
|
48
|
+
? current?.sourceOccurredAt ?? current?.acceptedAt
|
|
49
|
+
: current?.acceptedAt;
|
|
50
|
+
out[run.paneId] = {
|
|
51
|
+
...(pane === undefined ? {} : {
|
|
52
|
+
session: pane.sessionName,
|
|
53
|
+
window: pane.windowId,
|
|
54
|
+
windowName: pane.windowName,
|
|
55
|
+
}),
|
|
56
|
+
kind: current ? LEGACY_KIND[current.state] : null,
|
|
57
|
+
// `reason` is a machine-readable state code. Adapters may use it for recovery/diagnostics,
|
|
58
|
+
// but the user-facing roster must never render values such as `agent_end_idle` verbatim.
|
|
59
|
+
msg: current?.message ?? (current ? LEGACY_MESSAGE[current.state] : ''),
|
|
60
|
+
...(displayTime === undefined ? {} : { ts: displayTime }),
|
|
61
|
+
agent: run.agentId,
|
|
62
|
+
...(terminal ? {
|
|
63
|
+
terminalUnread: notifications.length > 0
|
|
64
|
+
&& notifications.every((candidate) => candidate.readAt === undefined),
|
|
65
|
+
} : {}),
|
|
66
|
+
...(notification === undefined ? {} : { terminalNotificationId: notification.id }),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseCodexOutboxSnapshot } from '../codexQueueProtocol.js';
|
|
5
|
+
import { PrivateStateStore } from '../privateStateStore.js';
|
|
6
|
+
import { parseConversationState } from './conversationStore.js';
|
|
7
|
+
const DELIVERY_RECEIPT_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
8
|
+
function payloadHash(text) {
|
|
9
|
+
return crypto.createHash('sha256').update(text).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
function migrationId(threadId, itemId, text) {
|
|
12
|
+
return `codex-legacy:${crypto.createHash('sha256').update(`${threadId}\0${itemId}\0${text}`)
|
|
13
|
+
.digest('hex').slice(0, 40)}`;
|
|
14
|
+
}
|
|
15
|
+
function fsyncDirectory(directory) {
|
|
16
|
+
let descriptor;
|
|
17
|
+
try {
|
|
18
|
+
descriptor = fs.openSync(directory, 'r');
|
|
19
|
+
fs.fsyncSync(descriptor);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
if (descriptor !== undefined)
|
|
23
|
+
fs.closeSync(descriptor);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function verifyBackup(file, fingerprint) {
|
|
27
|
+
if (!fs.existsSync(file)) {
|
|
28
|
+
throw new Error('Conversation migration marker exists but the Codex outbox backup is missing');
|
|
29
|
+
}
|
|
30
|
+
const actual = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
31
|
+
if (actual !== fingerprint) {
|
|
32
|
+
throw new Error('Codex outbox migration backup does not match its marker');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function verifyMarkerSubmissions(state, marker) {
|
|
36
|
+
if (!marker.submissionKeys) {
|
|
37
|
+
throw new Error('Conversation migration marker has no scoped submission keys');
|
|
38
|
+
}
|
|
39
|
+
for (const key of marker.submissionKeys) {
|
|
40
|
+
const [agentId, sessionId, clientRequestId] = key.split('\0');
|
|
41
|
+
if (!agentId || !sessionId || !clientRequestId || !(state.submissions.some((submission) => (submission.agentId === agentId && submission.sessionId === sessionId
|
|
42
|
+
&& submission.clientRequestId === clientRequestId)) || state.deliveryReceipts?.some((receipt) => receipt.agentId === agentId
|
|
43
|
+
&& receipt.sessionId === sessionId && receipt.clientRequestId === clientRequestId))) {
|
|
44
|
+
throw new Error('Conversation migration marker is missing a referenced delivery');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function requiredKeysFromBackup(file) {
|
|
49
|
+
if (!fs.existsSync(file))
|
|
50
|
+
return null;
|
|
51
|
+
const legacy = parseCodexOutboxSnapshot(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
52
|
+
if (!legacy)
|
|
53
|
+
throw new Error('Legacy Codex outbox backup is corrupt');
|
|
54
|
+
const keys = legacy.queues.flatMap((queue) => queue.items.map((item) => (`codex\0${queue.threadId}\0${item.requestId ?? migrationId(queue.threadId, item.id, item.text)}`)));
|
|
55
|
+
for (const receipt of legacy.receipts) {
|
|
56
|
+
if (receipt.status === 'pending' || receipt.status === 'accepted') {
|
|
57
|
+
keys.push(`codex\0${receipt.threadId}\0${receipt.requestId}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...new Set(keys)];
|
|
61
|
+
}
|
|
62
|
+
function verifyRequiredDeliveries(state, keys) {
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
const [agentId, sessionId, clientRequestId] = key.split('\0');
|
|
65
|
+
if (!(state.submissions.some((submission) => submission.agentId === agentId
|
|
66
|
+
&& submission.sessionId === sessionId && submission.clientRequestId === clientRequestId)
|
|
67
|
+
|| state.deliveryReceipts?.some((receipt) => receipt.agentId === agentId
|
|
68
|
+
&& receipt.sessionId === sessionId && receipt.clientRequestId === clientRequestId))) {
|
|
69
|
+
throw new Error('Conversation migration is missing a queued or uncertain delivery');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function finalizeMigration(legacyFile, conversationFile, state, fingerprint) {
|
|
74
|
+
const marker = state.migrations?.legacyCodexOutboxImported;
|
|
75
|
+
if (marker)
|
|
76
|
+
verifyMarkerSubmissions(state, marker);
|
|
77
|
+
const backup = `${legacyFile}.imported.${fingerprint}.json`;
|
|
78
|
+
const required = requiredKeysFromBackup(backup);
|
|
79
|
+
if (required)
|
|
80
|
+
verifyRequiredDeliveries(state, required);
|
|
81
|
+
for (const file of [legacyFile, backup]) {
|
|
82
|
+
try {
|
|
83
|
+
fs.unlinkSync(file);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT')
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
fsyncDirectory(path.dirname(legacyFile));
|
|
91
|
+
if (state.migrations?.legacyCodexOutboxImported) {
|
|
92
|
+
delete state.migrations.legacyCodexOutboxImported;
|
|
93
|
+
if (Object.keys(state.migrations).length === 0)
|
|
94
|
+
delete state.migrations;
|
|
95
|
+
new PrivateStateStore(conversationFile).write(state);
|
|
96
|
+
fsyncDirectory(path.dirname(conversationFile));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function exactMarkerSet(actual, expected) {
|
|
100
|
+
if (actual.length !== expected.length)
|
|
101
|
+
return false;
|
|
102
|
+
const values = new Set(actual);
|
|
103
|
+
return values.size === actual.length && expected.every((value) => values.has(value));
|
|
104
|
+
}
|
|
105
|
+
function acquireLock(file) {
|
|
106
|
+
const lock = `${file}.migration.lock`;
|
|
107
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
108
|
+
try {
|
|
109
|
+
const descriptor = fs.openSync(lock, 'wx', 0o600);
|
|
110
|
+
fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, createdAt: Date.now() }));
|
|
111
|
+
fs.fsyncSync(descriptor);
|
|
112
|
+
fs.closeSync(descriptor);
|
|
113
|
+
return () => { try {
|
|
114
|
+
fs.unlinkSync(lock);
|
|
115
|
+
}
|
|
116
|
+
catch { /* already released */ } };
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'EEXIST')
|
|
120
|
+
throw error;
|
|
121
|
+
let owner = 0;
|
|
122
|
+
try {
|
|
123
|
+
owner = Number(JSON.parse(fs.readFileSync(lock, 'utf8')).pid);
|
|
124
|
+
}
|
|
125
|
+
catch { /* stale */ }
|
|
126
|
+
try {
|
|
127
|
+
if (owner > 0)
|
|
128
|
+
process.kill(owner, 0);
|
|
129
|
+
throw new Error('Codex outbox migration is already running');
|
|
130
|
+
}
|
|
131
|
+
catch (probe) {
|
|
132
|
+
if (probe instanceof Error && !('code' in probe && probe.code === 'ESRCH'))
|
|
133
|
+
throw probe;
|
|
134
|
+
fs.unlinkSync(lock);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw new Error('Could not acquire Codex outbox migration lock');
|
|
139
|
+
}
|
|
140
|
+
export function migrateLegacyCodexOutbox(legacyFile, conversationFile, now = Date.now()) {
|
|
141
|
+
if (!fs.existsSync(legacyFile)) {
|
|
142
|
+
if (!fs.existsSync(conversationFile))
|
|
143
|
+
return { imported: 0 };
|
|
144
|
+
fsyncDirectory(path.dirname(conversationFile));
|
|
145
|
+
const stateStore = new PrivateStateStore(conversationFile);
|
|
146
|
+
const state = parseConversationState(stateStore.readStrict());
|
|
147
|
+
const marker = state.migrations?.legacyCodexOutboxImported;
|
|
148
|
+
if (!marker)
|
|
149
|
+
return { imported: 0 };
|
|
150
|
+
const backup = `${legacyFile}.imported.${marker.fingerprint}.json`;
|
|
151
|
+
if (fs.existsSync(backup))
|
|
152
|
+
verifyBackup(backup, marker.fingerprint);
|
|
153
|
+
finalizeMigration(legacyFile, conversationFile, state, marker.fingerprint);
|
|
154
|
+
return { imported: 0, fingerprint: marker.fingerprint };
|
|
155
|
+
}
|
|
156
|
+
const release = acquireLock(legacyFile);
|
|
157
|
+
try {
|
|
158
|
+
const bytes = fs.readFileSync(legacyFile);
|
|
159
|
+
const fingerprint = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
160
|
+
let raw;
|
|
161
|
+
try {
|
|
162
|
+
raw = JSON.parse(bytes.toString('utf8'));
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new Error('Legacy Codex outbox is truncated or invalid JSON');
|
|
166
|
+
}
|
|
167
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
168
|
+
&& raw.migratedToConversationCore === true) {
|
|
169
|
+
const state = parseConversationState(new PrivateStateStore(conversationFile).readStrict());
|
|
170
|
+
const marker = state.migrations?.legacyCodexOutboxImported;
|
|
171
|
+
if (!marker || marker.fingerprint !== raw.fingerprint) {
|
|
172
|
+
throw new Error('Codex outbox tombstone does not match Conversation migration marker');
|
|
173
|
+
}
|
|
174
|
+
const backup = `${legacyFile}.imported.${marker.fingerprint}.json`;
|
|
175
|
+
if (fs.existsSync(backup))
|
|
176
|
+
verifyBackup(backup, marker.fingerprint);
|
|
177
|
+
finalizeMigration(legacyFile, conversationFile, state, marker.fingerprint);
|
|
178
|
+
return { imported: 0, fingerprint: marker.fingerprint };
|
|
179
|
+
}
|
|
180
|
+
const legacy = parseCodexOutboxSnapshot(raw);
|
|
181
|
+
if (!legacy)
|
|
182
|
+
throw new Error('Unsupported or corrupt legacy Codex outbox schema');
|
|
183
|
+
const store = new PrivateStateStore(conversationFile);
|
|
184
|
+
const state = parseConversationState(store.readStrict());
|
|
185
|
+
const existingMarker = state.migrations?.legacyCodexOutboxImported;
|
|
186
|
+
const expectedIds = [];
|
|
187
|
+
const expectedKeys = [];
|
|
188
|
+
const byKey = new Map(state.submissions.map((item) => [`${item.agentId}\0${item.sessionId}\0${item.clientRequestId}`, item]));
|
|
189
|
+
const receiptsByKey = new Map((state.deliveryReceipts ?? []).map((item) => [`${item.agentId}\0${item.sessionId}\0${item.clientRequestId}`, item]));
|
|
190
|
+
const queuedByNativeId = new Map();
|
|
191
|
+
let imported = 0;
|
|
192
|
+
for (const queue of legacy.queues) {
|
|
193
|
+
for (const item of queue.items) {
|
|
194
|
+
const id = item.requestId ?? migrationId(queue.threadId, item.id, item.text);
|
|
195
|
+
const key = `codex\0${queue.threadId}\0${id}`;
|
|
196
|
+
expectedIds.push(id);
|
|
197
|
+
expectedKeys.push(key);
|
|
198
|
+
const existing = byKey.get(key);
|
|
199
|
+
if (existing) {
|
|
200
|
+
if (existing.text !== item.text)
|
|
201
|
+
throw new Error('Legacy Codex queue conflicts with Conversation ledger');
|
|
202
|
+
queuedByNativeId.set(`${queue.pane}\0${queue.threadId}\0${item.id}`, existing);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const revision = ++state.ledgerRevision;
|
|
206
|
+
const submission = {
|
|
207
|
+
agentId: 'codex', sessionId: queue.threadId, clientRequestId: id,
|
|
208
|
+
text: item.text, payloadHash: payloadHash(item.text), state: 'queued', revision,
|
|
209
|
+
queueOrderKey: `${String(Math.floor(item.createdAt)).padStart(16, '0')}:${String(revision).padStart(16, '0')}`,
|
|
210
|
+
lastRunId: queue.pane, createdAt: item.createdAt, updatedAt: item.createdAt,
|
|
211
|
+
};
|
|
212
|
+
state.submissions.push(submission);
|
|
213
|
+
byKey.set(key, submission);
|
|
214
|
+
queuedByNativeId.set(`${queue.pane}\0${queue.threadId}\0${item.id}`, submission);
|
|
215
|
+
imported += 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const receipt of legacy.receipts) {
|
|
219
|
+
const key = `codex\0${receipt.threadId}\0${receipt.requestId}`;
|
|
220
|
+
const existing = byKey.get(key);
|
|
221
|
+
const existingReceipt = receiptsByKey.get(key);
|
|
222
|
+
if (receipt.status === 'queued') {
|
|
223
|
+
const queued = queuedByNativeId.get(`${receipt.pane}\0${receipt.threadId}\0${receipt.queueItemId}`);
|
|
224
|
+
if (!queued || queued.clientRequestId !== receipt.requestId || queued.text !== receipt.text) {
|
|
225
|
+
throw new Error('Legacy Codex queued receipt has no matching queue item');
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (receipt.status === 'pending' || receipt.status === 'accepted') {
|
|
230
|
+
expectedIds.push(receipt.requestId);
|
|
231
|
+
expectedKeys.push(key);
|
|
232
|
+
}
|
|
233
|
+
if (existingReceipt) {
|
|
234
|
+
if (existingReceipt.payloadHash !== payloadHash(receipt.text)) {
|
|
235
|
+
throw new Error('Legacy Codex receipt conflicts with Conversation delivery receipt');
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (existing) {
|
|
240
|
+
if (existing.text !== receipt.text)
|
|
241
|
+
throw new Error('Legacy Codex receipt conflicts with Conversation ledger');
|
|
242
|
+
if (receipt.status !== 'accepted')
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (receipt.status === 'accepted') {
|
|
246
|
+
const settled = {
|
|
247
|
+
agentId: 'codex', sessionId: receipt.threadId,
|
|
248
|
+
clientRequestId: receipt.requestId,
|
|
249
|
+
payloadHash: existing?.payloadHash ?? payloadHash(receipt.text),
|
|
250
|
+
...(existing?.baseline === undefined ? {} : { baseline: structuredClone(existing.baseline) }),
|
|
251
|
+
...(receipt.turnId ?? existing?.nativeId
|
|
252
|
+
? { nativeId: (receipt.turnId ?? existing?.nativeId) } : {}),
|
|
253
|
+
...(existing?.steerActionId === undefined ? {} : { steerActionId: existing.steerActionId }),
|
|
254
|
+
...(existing?.steerBaseRevision === undefined
|
|
255
|
+
? {} : { steerBaseRevision: existing.steerBaseRevision }),
|
|
256
|
+
...(existing?.steerAnchor === undefined
|
|
257
|
+
? {} : { steerAnchor: structuredClone(existing.steerAnchor) }),
|
|
258
|
+
...(existing?.steerAttempts === undefined
|
|
259
|
+
? {} : { steerAttempts: structuredClone(existing.steerAttempts) }),
|
|
260
|
+
acceptedAt: receipt.updatedAt,
|
|
261
|
+
expiresAt: receipt.updatedAt + DELIVERY_RECEIPT_TTL_MS,
|
|
262
|
+
};
|
|
263
|
+
if (existing) {
|
|
264
|
+
state.submissions = state.submissions.filter((candidate) => candidate !== existing);
|
|
265
|
+
byKey.delete(key);
|
|
266
|
+
}
|
|
267
|
+
state.deliveryReceipts ??= [];
|
|
268
|
+
state.deliveryReceipts.push(settled);
|
|
269
|
+
receiptsByKey.set(key, settled);
|
|
270
|
+
imported += 1;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const revision = ++state.ledgerRevision;
|
|
274
|
+
const submission = {
|
|
275
|
+
agentId: 'codex', sessionId: receipt.threadId, clientRequestId: receipt.requestId,
|
|
276
|
+
text: receipt.text, payloadHash: payloadHash(receipt.text),
|
|
277
|
+
state: 'unknown', revision,
|
|
278
|
+
dispatchOrigin: 'direct',
|
|
279
|
+
...(receipt.turnId ? { nativeId: receipt.turnId } : {}),
|
|
280
|
+
lastRunId: receipt.pane, createdAt: receipt.createdAt, updatedAt: receipt.updatedAt,
|
|
281
|
+
};
|
|
282
|
+
state.submissions.push(submission);
|
|
283
|
+
byKey.set(key, submission);
|
|
284
|
+
imported += 1;
|
|
285
|
+
}
|
|
286
|
+
const uniqueIds = [...new Set(expectedIds)].sort();
|
|
287
|
+
const uniqueKeys = [...new Set(expectedKeys)].sort();
|
|
288
|
+
if (existingMarker) {
|
|
289
|
+
if (existingMarker.fingerprint !== fingerprint
|
|
290
|
+
|| (existingMarker.submissionKeys
|
|
291
|
+
? !exactMarkerSet(existingMarker.submissionKeys, uniqueKeys)
|
|
292
|
+
: !exactMarkerSet(existingMarker.submissionIds, uniqueIds))) {
|
|
293
|
+
throw new Error('Legacy Codex outbox migration marker conflicts with its source');
|
|
294
|
+
}
|
|
295
|
+
const needsMarkerUpgrade = !existingMarker.submissionKeys;
|
|
296
|
+
if (needsMarkerUpgrade)
|
|
297
|
+
existingMarker.submissionKeys = uniqueKeys;
|
|
298
|
+
if (imported > 0 || needsMarkerUpgrade)
|
|
299
|
+
store.write(state);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
state.migrations ??= {};
|
|
303
|
+
state.migrations.legacyCodexOutboxImported = {
|
|
304
|
+
fingerprint, submissionIds: uniqueIds, submissionKeys: uniqueKeys, importedAt: now,
|
|
305
|
+
};
|
|
306
|
+
store.write(state);
|
|
307
|
+
}
|
|
308
|
+
// PrivateStateStore makes the file contents durable before rename. Persist the directory entry too,
|
|
309
|
+
// then re-read the committed ledger before the only recoverable legacy source is moved aside.
|
|
310
|
+
fsyncDirectory(path.dirname(conversationFile));
|
|
311
|
+
const verified = parseConversationState(store.readStrict());
|
|
312
|
+
for (const key of uniqueKeys) {
|
|
313
|
+
const [agentId, sessionId, clientRequestId] = key.split('\0');
|
|
314
|
+
if (!(verified.submissions.some((submission) => submission.agentId === agentId
|
|
315
|
+
&& submission.sessionId === sessionId && submission.clientRequestId === clientRequestId)
|
|
316
|
+
|| verified.deliveryReceipts?.some((receipt) => receipt.agentId === agentId
|
|
317
|
+
&& receipt.sessionId === sessionId && receipt.clientRequestId === clientRequestId))) {
|
|
318
|
+
throw new Error('Conversation migration marker is missing a referenced delivery');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const backup = `${legacyFile}.imported.${fingerprint}.json`;
|
|
322
|
+
if (!fs.existsSync(backup))
|
|
323
|
+
fs.renameSync(legacyFile, backup);
|
|
324
|
+
else {
|
|
325
|
+
verifyBackup(backup, fingerprint);
|
|
326
|
+
if (fs.existsSync(legacyFile))
|
|
327
|
+
fs.unlinkSync(legacyFile);
|
|
328
|
+
}
|
|
329
|
+
fsyncDirectory(path.dirname(legacyFile));
|
|
330
|
+
finalizeMigration(legacyFile, conversationFile, verified, fingerprint);
|
|
331
|
+
return { imported, fingerprint };
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
release();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|