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,1103 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { transcriptReader } from '../transcriptReader.js';
|
|
4
|
+
import { createCodexTranscriptParser, } from '../codexTranscriptParse.js';
|
|
5
|
+
import { projectCodexConversationMutation } from '../codexConversationProjection.js';
|
|
6
|
+
import { parseCodexStreamEvent } from '../codexStreamProtocol.js';
|
|
7
|
+
import { resolveCodexRollout, sessionsDir } from './codex.js';
|
|
8
|
+
import { clipConversationText, safeProviderPathLabel, sanitizeToolInputWithMetadata, sanitizeToolResultText, } from './conversationProjectionSafety.js';
|
|
9
|
+
const DURABLE_QUIET_MS = 100;
|
|
10
|
+
const DEFAULT_DURABLE_SETTLE_TIMEOUT_MS = 15_000;
|
|
11
|
+
const MAX_OPENING_NATIVE_EVENTS = 4_096;
|
|
12
|
+
const MAX_SEEN_GOAL_LIFECYCLES = 2_048;
|
|
13
|
+
const MAX_DURABLE_CHECKPOINTS = 64;
|
|
14
|
+
const HISTORY_HASH_SEED = createHash('sha256').update('codex-conversation-v1').digest('hex');
|
|
15
|
+
function goalIdentity(item) {
|
|
16
|
+
if (item.kind !== 'notice' || item.code !== 'goal_updated')
|
|
17
|
+
return null;
|
|
18
|
+
const goal = item.extensions?.['conversation.goal'];
|
|
19
|
+
if (!goal || typeof goal !== 'object' || Array.isArray(goal))
|
|
20
|
+
return null;
|
|
21
|
+
const event = item.extensions?.['conversation.goalEvent'];
|
|
22
|
+
const objective = typeof goal.objective === 'string' ? goal.objective : null;
|
|
23
|
+
const status = typeof goal.status === 'string' ? goal.status : null;
|
|
24
|
+
const createdAt = typeof goal.createdAt === 'number' && Number.isFinite(goal.createdAt)
|
|
25
|
+
? goal.createdAt : null;
|
|
26
|
+
if (!objective || !status || typeof event !== 'string')
|
|
27
|
+
return null;
|
|
28
|
+
return { objective, status, event, createdAt };
|
|
29
|
+
}
|
|
30
|
+
function sameGoal(left, right) {
|
|
31
|
+
const lifecycle = (goal) => (goal.status === 'active' && ['set', 'restarted', 'active'].includes(goal.event)
|
|
32
|
+
? 'active' : goal.event);
|
|
33
|
+
return left.objective === right.objective && left.status === right.status
|
|
34
|
+
&& lifecycle(left) === lifecycle(right)
|
|
35
|
+
// The injected durable Goal context does not carry App Server's createdAt. If both sides have it,
|
|
36
|
+
// use it to distinguish a genuinely repeated Goal with the same objective and lifecycle.
|
|
37
|
+
&& (left.createdAt === null || right.createdAt === null || left.createdAt === right.createdAt);
|
|
38
|
+
}
|
|
39
|
+
function durableOwnsSettledItem(pending, durable) {
|
|
40
|
+
if (durable.id !== pending.id || durable.kind !== pending.kind)
|
|
41
|
+
return false;
|
|
42
|
+
if (pending.kind === 'message' && durable.kind === 'message') {
|
|
43
|
+
return pending.role === durable.role
|
|
44
|
+
&& JSON.stringify(pending.content) === JSON.stringify(durable.content);
|
|
45
|
+
}
|
|
46
|
+
const pendingGoal = goalIdentity(pending);
|
|
47
|
+
const durableGoal = goalIdentity(durable);
|
|
48
|
+
if (pendingGoal || durableGoal) {
|
|
49
|
+
return pendingGoal !== null && durableGoal !== null && sameGoal(pendingGoal, durableGoal);
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function durableMatchCount(pending, durableItems) {
|
|
54
|
+
// A Goal's immediate App Server card uses its lifecycle timestamp, while the injected rollout
|
|
55
|
+
// context uses the source item identity. Match that one documented identity handoff semantically.
|
|
56
|
+
const pendingGoal = goalIdentity(pending);
|
|
57
|
+
if (pendingGoal) {
|
|
58
|
+
return durableItems.filter((item) => {
|
|
59
|
+
const durableGoal = goalIdentity(item);
|
|
60
|
+
return durableGoal !== null && sameGoal(pendingGoal, durableGoal);
|
|
61
|
+
}).length;
|
|
62
|
+
}
|
|
63
|
+
return durableItems.filter((item) => item.id === pending.id).length;
|
|
64
|
+
}
|
|
65
|
+
function waitsForNextTurn(item) {
|
|
66
|
+
return goalIdentity(item)?.status === 'active';
|
|
67
|
+
}
|
|
68
|
+
function sameDurableItem(left, right) {
|
|
69
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
70
|
+
}
|
|
71
|
+
function trustedCheckpointLength(checkpoint, cutoff, baseline) {
|
|
72
|
+
if (!checkpoint || checkpoint.file !== cutoff.file
|
|
73
|
+
|| checkpoint.snapshot.items.length > baseline.items.length)
|
|
74
|
+
return null;
|
|
75
|
+
return checkpoint.snapshot.items.every((item, index) => (sameDurableItem(item, baseline.items[index]))) ? checkpoint.snapshot.items.length : null;
|
|
76
|
+
}
|
|
77
|
+
function goalLifecycleKey(sessionId, messageId) {
|
|
78
|
+
return `${sessionId}\0${messageId}`;
|
|
79
|
+
}
|
|
80
|
+
function rememberGoalLifecycle(seen, sessionId, messageId) {
|
|
81
|
+
const key = goalLifecycleKey(sessionId, messageId);
|
|
82
|
+
// Refresh insertion order so this bounded Set behaves like a small LRU. Reconnect repair only needs
|
|
83
|
+
// recent lifecycle knowledge; evicting an old ID safely falls back to showing a possibly duplicate card.
|
|
84
|
+
seen.delete(key);
|
|
85
|
+
seen.add(key);
|
|
86
|
+
if (seen.size <= MAX_SEEN_GOAL_LIFECYCLES)
|
|
87
|
+
return;
|
|
88
|
+
const oldest = seen.values().next().value;
|
|
89
|
+
if (typeof oldest === 'string')
|
|
90
|
+
seen.delete(oldest);
|
|
91
|
+
}
|
|
92
|
+
function suppressedOpeningEventIndexes(values, sessionId, baseline, knownLength, durablyOwnedGoalLifecycleIds) {
|
|
93
|
+
const groups = new Map();
|
|
94
|
+
values.forEach((value, index) => {
|
|
95
|
+
const event = parseCodexStreamEvent(value);
|
|
96
|
+
if (!event || event.type === 'ready' || event.type === 'cursorReset'
|
|
97
|
+
|| event.type === 'conversationSnapshot' || event.type === 'disconnected'
|
|
98
|
+
|| event.type === 'error')
|
|
99
|
+
return;
|
|
100
|
+
const mutation = projectCodexConversationMutation(event);
|
|
101
|
+
if (!mutation || mutation.operation !== 'upsert')
|
|
102
|
+
return;
|
|
103
|
+
const draft = liveDraft(mutation.message);
|
|
104
|
+
if (!draft)
|
|
105
|
+
return;
|
|
106
|
+
let group = groups.get(mutation.message.id);
|
|
107
|
+
if (!group) {
|
|
108
|
+
group = {
|
|
109
|
+
messageId: mutation.message.id,
|
|
110
|
+
messageType: mutation.message.type,
|
|
111
|
+
indexes: [],
|
|
112
|
+
draft: null,
|
|
113
|
+
text: null,
|
|
114
|
+
settled: null,
|
|
115
|
+
repairOnly: event.observationSnapshot === true,
|
|
116
|
+
};
|
|
117
|
+
groups.set(mutation.message.id, group);
|
|
118
|
+
}
|
|
119
|
+
group.indexes.push(index);
|
|
120
|
+
// A stable ID can appear as both a setup snapshot and a real journal lifecycle. Such a mixed group
|
|
121
|
+
// is real lifecycle evidence and must never inherit the snapshot's weaker semantic suppression rule.
|
|
122
|
+
group.repairOnly &&= event.observationSnapshot === true;
|
|
123
|
+
if (mutation.message.type === 'text') {
|
|
124
|
+
group.text = mutation.mode === 'append'
|
|
125
|
+
? `${group.text ?? ''}${mutation.message.text}` : mutation.message.text;
|
|
126
|
+
group.draft = liveDraft({ ...mutation.message, text: group.text });
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
group.draft = draft;
|
|
130
|
+
}
|
|
131
|
+
if (mutation.message.completed) {
|
|
132
|
+
group.settled = finalItem(mutation.message.id, sessionId, group.draft ?? draft);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const suppressed = new Set();
|
|
136
|
+
const durablyOwnedGoalIds = new Set();
|
|
137
|
+
const claimedDurableIndexes = new Set();
|
|
138
|
+
const suppress = (group) => {
|
|
139
|
+
for (const index of group.indexes)
|
|
140
|
+
suppressed.add(index);
|
|
141
|
+
};
|
|
142
|
+
// Exact native identity is authoritative. Reserve those durable occurrences globally and drop every
|
|
143
|
+
// lifecycle event for that entity, not merely the terminal event after an opened card already flashed.
|
|
144
|
+
for (const group of groups.values()) {
|
|
145
|
+
const projected = group.settled ?? (group.messageType === 'text' && group.draft
|
|
146
|
+
? finalItem(group.messageId, sessionId, group.draft) : null);
|
|
147
|
+
if (!projected)
|
|
148
|
+
continue;
|
|
149
|
+
const durableIndex = baseline.items.findIndex((item, index) => (!claimedDurableIndexes.has(index)
|
|
150
|
+
&& durableOwnsSettledItem(projected, item)));
|
|
151
|
+
if (durableIndex < 0)
|
|
152
|
+
continue;
|
|
153
|
+
claimedDurableIndexes.add(durableIndex);
|
|
154
|
+
if (goalIdentity(projected))
|
|
155
|
+
durablyOwnedGoalIds.add(group.messageId);
|
|
156
|
+
suppress(group);
|
|
157
|
+
}
|
|
158
|
+
// A tagged Goal is a repair of current App Server state, not evidence of a newly created historical
|
|
159
|
+
// lifecycle. Semantic suppression is allowed only after a prior durable reconciliation proved that this
|
|
160
|
+
// stable lifecycle ID owns an occurrence; otherwise an old same-objective Goal could swallow a new one.
|
|
161
|
+
for (const group of groups.values()) {
|
|
162
|
+
if (!group.repairOnly || !group.settled
|
|
163
|
+
|| !durablyOwnedGoalLifecycleIds.has(goalLifecycleKey(sessionId, group.messageId))
|
|
164
|
+
|| group.indexes.every((index) => suppressed.has(index)))
|
|
165
|
+
continue;
|
|
166
|
+
const pendingGoal = goalIdentity(group.settled);
|
|
167
|
+
if (!pendingGoal)
|
|
168
|
+
continue;
|
|
169
|
+
const candidates = baseline.items.flatMap((item, index) => {
|
|
170
|
+
if (claimedDurableIndexes.has(index))
|
|
171
|
+
return [];
|
|
172
|
+
const durableGoal = goalIdentity(item);
|
|
173
|
+
return durableGoal !== null && sameGoal(pendingGoal, durableGoal) ? [index] : [];
|
|
174
|
+
});
|
|
175
|
+
const durableIndex = candidates.find((index) => (group.settled.groupingId
|
|
176
|
+
&& baseline.items[index]?.groupingId === group.settled.groupingId)) ?? candidates[0] ?? -1;
|
|
177
|
+
if (durableIndex < 0)
|
|
178
|
+
continue;
|
|
179
|
+
claimedDurableIndexes.add(durableIndex);
|
|
180
|
+
suppress(group);
|
|
181
|
+
}
|
|
182
|
+
// Active Goal context uses a rollout item identity rather than App Server's timestamp identity. Only a
|
|
183
|
+
// proven addition after a previously read durable prefix may bridge that identity, one occurrence each.
|
|
184
|
+
// With no trustworthy checkpoint, retaining the native card is safer than swallowing a repeated Goal.
|
|
185
|
+
if (knownLength === null)
|
|
186
|
+
return { suppressed, durablyOwnedGoalIds };
|
|
187
|
+
const semanticGroups = [...groups.values()].filter((group) => (!group.repairOnly && group.settled
|
|
188
|
+
&& !group.indexes.every((index) => suppressed.has(index))
|
|
189
|
+
&& goalIdentity(group.settled) !== null));
|
|
190
|
+
// Reserve correlation matches before FIFO fallback, so an earlier unanchored Goal cannot steal the
|
|
191
|
+
// durable occurrence explicitly tied to a later opening Goal.
|
|
192
|
+
for (const group of semanticGroups) {
|
|
193
|
+
const pendingGoal = goalIdentity(group.settled);
|
|
194
|
+
const groupingId = group.settled.groupingId;
|
|
195
|
+
if (!pendingGoal || !groupingId)
|
|
196
|
+
continue;
|
|
197
|
+
const durableIndex = baseline.items.findIndex((item, index) => {
|
|
198
|
+
if (index < knownLength || claimedDurableIndexes.has(index)
|
|
199
|
+
|| item.groupingId !== groupingId)
|
|
200
|
+
return false;
|
|
201
|
+
const durableGoal = goalIdentity(item);
|
|
202
|
+
return durableGoal !== null && sameGoal(pendingGoal, durableGoal);
|
|
203
|
+
});
|
|
204
|
+
if (durableIndex < 0)
|
|
205
|
+
continue;
|
|
206
|
+
claimedDurableIndexes.add(durableIndex);
|
|
207
|
+
durablyOwnedGoalIds.add(group.messageId);
|
|
208
|
+
suppress(group);
|
|
209
|
+
}
|
|
210
|
+
for (const group of semanticGroups) {
|
|
211
|
+
if (group.indexes.every((index) => suppressed.has(index)))
|
|
212
|
+
continue;
|
|
213
|
+
const pendingGoal = goalIdentity(group.settled);
|
|
214
|
+
if (!pendingGoal)
|
|
215
|
+
continue;
|
|
216
|
+
const durableIndex = baseline.items.findIndex((item, index) => {
|
|
217
|
+
if (index < knownLength || claimedDurableIndexes.has(index))
|
|
218
|
+
return false;
|
|
219
|
+
const durableGoal = goalIdentity(item);
|
|
220
|
+
return durableGoal !== null && sameGoal(pendingGoal, durableGoal);
|
|
221
|
+
});
|
|
222
|
+
if (durableIndex < 0)
|
|
223
|
+
continue;
|
|
224
|
+
claimedDurableIndexes.add(durableIndex);
|
|
225
|
+
durablyOwnedGoalIds.add(group.messageId);
|
|
226
|
+
suppress(group);
|
|
227
|
+
}
|
|
228
|
+
return { suppressed, durablyOwnedGoalIds };
|
|
229
|
+
}
|
|
230
|
+
function coveredPendingItemIds(pendingItems, durableItems) {
|
|
231
|
+
const covered = new Set();
|
|
232
|
+
const claimedDurableIndexes = new Set();
|
|
233
|
+
// Reserve stable-ID ownership globally before any Goal is allowed to claim a semantic occurrence.
|
|
234
|
+
// Otherwise an earlier Goal can steal the exact durable item that belongs to a later pending Goal.
|
|
235
|
+
for (const [pendingId, pending] of pendingItems) {
|
|
236
|
+
const exactIndex = durableItems.findIndex((item, index) => (!claimedDurableIndexes.has(index) && durableOwnsSettledItem(pending.item, item)));
|
|
237
|
+
if (exactIndex >= 0) {
|
|
238
|
+
claimedDurableIndexes.add(exactIndex);
|
|
239
|
+
covered.add(pendingId);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const [pendingId, pending] of pendingItems) {
|
|
243
|
+
if (covered.has(pendingId))
|
|
244
|
+
continue;
|
|
245
|
+
const pendingGoal = goalIdentity(pending.item);
|
|
246
|
+
if (!pendingGoal)
|
|
247
|
+
continue;
|
|
248
|
+
const candidates = durableItems.flatMap((item, index) => {
|
|
249
|
+
const durableGoal = goalIdentity(item);
|
|
250
|
+
return durableGoal && sameGoal(pendingGoal, durableGoal) ? [index] : [];
|
|
251
|
+
});
|
|
252
|
+
// The first N equivalent occurrences were already in the committed baseline when this live card
|
|
253
|
+
// settled. Allocate only a later occurrence, and allocate every durable occurrence at most once.
|
|
254
|
+
const candidate = candidates.slice(pending.baselineMatches)
|
|
255
|
+
.find((index) => !claimedDurableIndexes.has(index));
|
|
256
|
+
if (candidate === undefined)
|
|
257
|
+
continue;
|
|
258
|
+
claimedDurableIndexes.add(candidate);
|
|
259
|
+
covered.add(pendingId);
|
|
260
|
+
}
|
|
261
|
+
return [...covered];
|
|
262
|
+
}
|
|
263
|
+
function isRun(target) {
|
|
264
|
+
return 'runId' in target;
|
|
265
|
+
}
|
|
266
|
+
function viewId(sessionId) {
|
|
267
|
+
return `codex-thread:${sessionId}`;
|
|
268
|
+
}
|
|
269
|
+
function stableId(message, suffix = '') {
|
|
270
|
+
const base = typeof message.id === 'string' && message.id
|
|
271
|
+
? message.id : `codex:line-${message.i}:${message.type}`;
|
|
272
|
+
return suffix ? `${base}:${suffix}` : base;
|
|
273
|
+
}
|
|
274
|
+
function sourceTime(value) {
|
|
275
|
+
if (typeof value !== 'string')
|
|
276
|
+
return undefined;
|
|
277
|
+
const parsed = Date.parse(value);
|
|
278
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
279
|
+
}
|
|
280
|
+
function json(value) {
|
|
281
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
282
|
+
return value;
|
|
283
|
+
if (typeof value === 'number')
|
|
284
|
+
return Number.isFinite(value) ? value : String(value);
|
|
285
|
+
if (Array.isArray(value))
|
|
286
|
+
return value.map(json);
|
|
287
|
+
if (value && typeof value === 'object') {
|
|
288
|
+
const out = {};
|
|
289
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
290
|
+
if (entry !== undefined && typeof entry !== 'function' && typeof entry !== 'symbol')
|
|
291
|
+
out[key] = json(entry);
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
return String(value ?? '');
|
|
296
|
+
}
|
|
297
|
+
function itemBase(message, sessionId, id = stableId(message)) {
|
|
298
|
+
const createdAt = sourceTime(message.ts);
|
|
299
|
+
return {
|
|
300
|
+
id,
|
|
301
|
+
sessionId,
|
|
302
|
+
status: 'complete',
|
|
303
|
+
...(createdAt === undefined ? {} : { sourceCreatedAt: createdAt }),
|
|
304
|
+
...(typeof message.turnId === 'string' && message.turnId
|
|
305
|
+
? { groupingId: message.turnId } : {}),
|
|
306
|
+
...(typeof message.correlationId === 'string' && message.correlationId
|
|
307
|
+
? { correlationId: message.correlationId } : {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function projectDurableMessage(message, sessionId) {
|
|
311
|
+
if (message.type === 'text' && message.role && typeof message.text === 'string') {
|
|
312
|
+
const output = clipConversationText(message.text);
|
|
313
|
+
return [{
|
|
314
|
+
...itemBase(message, sessionId), kind: 'message', role: message.role,
|
|
315
|
+
content: [{ type: 'text', text: output.text }],
|
|
316
|
+
...(output.truncated ? {
|
|
317
|
+
status: 'truncated',
|
|
318
|
+
truncation: { reason: 'size_limit', originalBytes: output.originalBytes },
|
|
319
|
+
} : {}),
|
|
320
|
+
}];
|
|
321
|
+
}
|
|
322
|
+
if (message.type === 'thinking' && typeof message.text === 'string') {
|
|
323
|
+
// Codex rollout stores the provider's public reasoning summary, not hidden reasoning content.
|
|
324
|
+
const output = clipConversationText(message.text);
|
|
325
|
+
return [{
|
|
326
|
+
...itemBase(message, sessionId), kind: 'reasoning_summary', text: output.text,
|
|
327
|
+
...(output.truncated ? {
|
|
328
|
+
status: 'truncated',
|
|
329
|
+
truncation: { reason: 'size_limit', originalBytes: output.originalBytes },
|
|
330
|
+
} : {}),
|
|
331
|
+
}];
|
|
332
|
+
}
|
|
333
|
+
if (message.type === 'tool' && message.tool) {
|
|
334
|
+
const callId = stableId(message, 'call');
|
|
335
|
+
const inputProjection = sanitizeToolInputWithMetadata(message.tool.input);
|
|
336
|
+
const input = inputProjection.value;
|
|
337
|
+
const name = clipConversationText(message.tool.name || 'tool', 512).text || 'tool';
|
|
338
|
+
const rawResult = message.tool.result === null || message.tool.result === undefined
|
|
339
|
+
? null : String(message.tool.result);
|
|
340
|
+
const result = rawResult === null ? null : sanitizeToolResultText(rawResult);
|
|
341
|
+
const extensionInput = input !== null && typeof input === 'object' ? input : {};
|
|
342
|
+
const diffProjection = message.tool.diff === undefined
|
|
343
|
+
? undefined : sanitizeToolInputWithMetadata(message.tool.diff);
|
|
344
|
+
const safeDiff = diffProjection?.value;
|
|
345
|
+
const toolProjectionBytes = inputProjection.originalBytes + (diffProjection?.originalBytes ?? 0);
|
|
346
|
+
const toolProjectionTruncated = inputProjection.truncated || diffProjection?.truncated === true;
|
|
347
|
+
const outcomes = ['running', 'success', 'failed', 'declined', 'completed'];
|
|
348
|
+
const outcome = typeof message.tool.outcome === 'string' && outcomes.includes(message.tool.outcome)
|
|
349
|
+
? message.tool.outcome : undefined;
|
|
350
|
+
const items = [{
|
|
351
|
+
...itemBase(message, sessionId, callId), kind: 'tool_call', callId,
|
|
352
|
+
name, ...(input === undefined ? {} : { input }),
|
|
353
|
+
...(toolProjectionTruncated ? {
|
|
354
|
+
status: 'truncated',
|
|
355
|
+
truncation: { reason: 'size_limit', originalBytes: toolProjectionBytes },
|
|
356
|
+
} : {}),
|
|
357
|
+
extensions: {
|
|
358
|
+
'conversation.tool': {
|
|
359
|
+
name, input: extensionInput, result: result?.text ?? null,
|
|
360
|
+
isError: message.tool.isError === true,
|
|
361
|
+
...(outcome === undefined ? {} : { outcome }),
|
|
362
|
+
...(safeDiff === undefined ? {} : { diff: safeDiff }),
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
}];
|
|
366
|
+
// A successful tool may legitimately produce no stdout. Do not manufacture an empty durable text
|
|
367
|
+
// block: Core intentionally rejects those because they have no user-visible content.
|
|
368
|
+
if (rawResult !== null && rawResult.length > 0 && result) {
|
|
369
|
+
items.push({
|
|
370
|
+
...itemBase(message, sessionId, stableId(message, 'result')),
|
|
371
|
+
kind: 'tool_result', callId,
|
|
372
|
+
content: [{ type: 'text', text: result.text }],
|
|
373
|
+
...(message.tool.isError ? { isError: true } : {}),
|
|
374
|
+
...(result.truncated ? {
|
|
375
|
+
status: 'truncated',
|
|
376
|
+
truncation: { reason: 'size_limit', originalBytes: result.originalBytes },
|
|
377
|
+
} : {}),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
const rawInput = message.tool.input && typeof message.tool.input === 'object'
|
|
381
|
+
&& !Array.isArray(message.tool.input) ? message.tool.input : {};
|
|
382
|
+
if (message.tool.diff) {
|
|
383
|
+
const rawPath = rawInput.file_path ?? rawInput.path;
|
|
384
|
+
const path = safeProviderPathLabel(rawPath);
|
|
385
|
+
const patch = typeof rawInput.patch === 'string'
|
|
386
|
+
? sanitizeToolResultText(rawInput.patch) : undefined;
|
|
387
|
+
items.push({
|
|
388
|
+
...itemBase(message, sessionId, stableId(message, 'diff')),
|
|
389
|
+
kind: 'diff',
|
|
390
|
+
...(path === undefined ? {} : { path }),
|
|
391
|
+
...(patch === undefined ? {} : { patch: patch.text }),
|
|
392
|
+
summary: `+${message.tool.diff.added} -${message.tool.diff.removed}`,
|
|
393
|
+
...(patch?.truncated || diffProjection?.truncated ? {
|
|
394
|
+
status: 'truncated',
|
|
395
|
+
truncation: {
|
|
396
|
+
reason: 'size_limit',
|
|
397
|
+
originalBytes: (patch?.originalBytes ?? 0) + (diffProjection?.originalBytes ?? 0),
|
|
398
|
+
},
|
|
399
|
+
} : {}),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return items;
|
|
403
|
+
}
|
|
404
|
+
if (message.type === 'compact') {
|
|
405
|
+
const summary = typeof message.summary === 'string' && message.summary
|
|
406
|
+
? clipConversationText(message.summary) : null;
|
|
407
|
+
const truncated = message.summaryTruncated === true || summary?.truncated === true;
|
|
408
|
+
const originalBytes = typeof message.summaryOriginalBytes === 'number'
|
|
409
|
+
? message.summaryOriginalBytes : summary?.originalBytes;
|
|
410
|
+
return [{
|
|
411
|
+
...itemBase(message, sessionId), kind: 'compaction',
|
|
412
|
+
...(summary ? { summary: summary.text } : {}),
|
|
413
|
+
...(truncated ? {
|
|
414
|
+
status: 'truncated',
|
|
415
|
+
truncation: { reason: 'size_limit', originalBytes: originalBytes ?? 0 },
|
|
416
|
+
} : {}),
|
|
417
|
+
}];
|
|
418
|
+
}
|
|
419
|
+
if (message.type === 'interrupt') {
|
|
420
|
+
return [{ ...itemBase(message, sessionId), kind: 'interrupt', actor: 'user' }];
|
|
421
|
+
}
|
|
422
|
+
if (message.type === 'slash') {
|
|
423
|
+
return [{
|
|
424
|
+
...itemBase(message, sessionId), kind: 'notice', level: 'info', code: 'slash_command',
|
|
425
|
+
message: `${message.name ?? '/command'}${message.args ? ` ${message.args}` : ''}`,
|
|
426
|
+
extensions: {
|
|
427
|
+
'conversation.slash': json({
|
|
428
|
+
name: message.name ?? '/command',
|
|
429
|
+
...(typeof message.args === 'string' ? { args: message.args } : {}),
|
|
430
|
+
...(typeof message.result === 'string' ? { result: message.result } : {}),
|
|
431
|
+
}),
|
|
432
|
+
},
|
|
433
|
+
}];
|
|
434
|
+
}
|
|
435
|
+
if (message.type === 'plan') {
|
|
436
|
+
return [{
|
|
437
|
+
...itemBase(message, sessionId), kind: 'notice', level: 'info', code: 'plan_updated',
|
|
438
|
+
message: typeof message.explanation === 'string' && message.explanation
|
|
439
|
+
? message.explanation : 'Plan updated',
|
|
440
|
+
extensions: { 'conversation.plan': json(message.plan ?? []) },
|
|
441
|
+
}];
|
|
442
|
+
}
|
|
443
|
+
if (message.type === 'goal') {
|
|
444
|
+
const goal = message.goal && typeof message.goal === 'object' ? message.goal : {};
|
|
445
|
+
const objective = typeof goal.objective === 'string' ? goal.objective : 'Goal updated';
|
|
446
|
+
const status = typeof goal.status === 'string' ? goal.status : message.event ?? 'updated';
|
|
447
|
+
return [{
|
|
448
|
+
...itemBase(message, sessionId), kind: 'notice', level: 'info', code: 'goal_updated',
|
|
449
|
+
message: `${objective} · ${status}`,
|
|
450
|
+
extensions: {
|
|
451
|
+
'conversation.goal': json(goal),
|
|
452
|
+
...(typeof message.event === 'string' ? { 'conversation.goalEvent': message.event } : {}),
|
|
453
|
+
},
|
|
454
|
+
}];
|
|
455
|
+
}
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
458
|
+
function liveDraft(message) {
|
|
459
|
+
const grouping = typeof message.turnId === 'string' && message.turnId
|
|
460
|
+
? { groupingId: message.turnId } : {};
|
|
461
|
+
if (message.type === 'text') {
|
|
462
|
+
return {
|
|
463
|
+
kind: 'message', role: 'assistant',
|
|
464
|
+
content: [{ type: 'text', text: message.text ?? '' }],
|
|
465
|
+
...grouping,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (message.type === 'goal') {
|
|
469
|
+
const goal = message.goal && typeof message.goal === 'object'
|
|
470
|
+
? message.goal : {};
|
|
471
|
+
const objective = typeof goal.objective === 'string' ? goal.objective : 'Goal updated';
|
|
472
|
+
const status = typeof goal.status === 'string' ? goal.status : 'updated';
|
|
473
|
+
return {
|
|
474
|
+
kind: 'notice', level: 'info', code: 'goal_updated', message: `${objective} · ${status}`,
|
|
475
|
+
...grouping,
|
|
476
|
+
extensions: {
|
|
477
|
+
'conversation.goal': json(goal),
|
|
478
|
+
...(typeof message.event === 'string' ? { 'conversation.goalEvent': message.event } : {}),
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
function finalItem(provisionalId, sessionId, draft) {
|
|
485
|
+
return { id: provisionalId, sessionId, status: 'complete', ...draft };
|
|
486
|
+
}
|
|
487
|
+
export function createCodexConversationAdapter({ app, sessionsRoot = sessionsDir(), reader = transcriptReader, findRollout = resolveCodexRollout, rolloutSize = async (file) => {
|
|
488
|
+
try {
|
|
489
|
+
const snapshot = await stat(file);
|
|
490
|
+
return snapshot.isFile() ? snapshot.size : null;
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}, durablePollMs = 500, durableSettleTimeoutMs = DEFAULT_DURABLE_SETTLE_TIMEOUT_MS, }) {
|
|
496
|
+
if (!app || typeof app.discover !== 'function' || typeof app.observeConversation !== 'function') {
|
|
497
|
+
throw new TypeError('Codex Conversation adapter requires App Server');
|
|
498
|
+
}
|
|
499
|
+
if (!Number.isFinite(durablePollMs) || durablePollMs <= 0) {
|
|
500
|
+
throw new TypeError('Codex Conversation adapter requires a positive durable poll interval');
|
|
501
|
+
}
|
|
502
|
+
if (!Number.isFinite(durableSettleTimeoutMs) || durableSettleTimeoutMs <= 0) {
|
|
503
|
+
throw new TypeError('Codex Conversation adapter requires a positive durable settle timeout');
|
|
504
|
+
}
|
|
505
|
+
if (!reader.readPrefix) {
|
|
506
|
+
throw new TypeError('Codex Conversation adapter requires immutable transcript prefix reads');
|
|
507
|
+
}
|
|
508
|
+
const readPrefix = reader.readPrefix.bind(reader);
|
|
509
|
+
const durableCheckpoints = new Map();
|
|
510
|
+
const durablyOwnedGoalLifecycleIds = new Set();
|
|
511
|
+
// Core reads its authoritative opening page immediately after observeNative returns. Freeze that one
|
|
512
|
+
// read at the same byte cutoff as the adapter baseline so later rollout suffixes cannot appear in both
|
|
513
|
+
// the page and the already-buffered native stream.
|
|
514
|
+
const openingSnapshots = new Map();
|
|
515
|
+
function cloneCheckpoint(checkpoint) {
|
|
516
|
+
return {
|
|
517
|
+
...checkpoint,
|
|
518
|
+
itemOffsets: [...checkpoint.itemOffsets],
|
|
519
|
+
historyHashes: [...checkpoint.historyHashes],
|
|
520
|
+
snapshot: { ...checkpoint.snapshot, items: [...checkpoint.snapshot.items] },
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function rememberCheckpoint(sessionId, checkpoint) {
|
|
524
|
+
durableCheckpoints.delete(sessionId);
|
|
525
|
+
durableCheckpoints.set(sessionId, checkpoint);
|
|
526
|
+
while (durableCheckpoints.size > MAX_DURABLE_CHECKPOINTS) {
|
|
527
|
+
const oldest = durableCheckpoints.keys().next().value;
|
|
528
|
+
if (oldest === undefined)
|
|
529
|
+
break;
|
|
530
|
+
durableCheckpoints.delete(oldest);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function snapshotFromMessages(session, parsed, file, version, readerGeneration, changedFrom, checkpoint) {
|
|
534
|
+
const sameReader = readerGeneration === undefined || checkpoint?.readerGeneration === undefined
|
|
535
|
+
|| checkpoint.readerGeneration === readerGeneration;
|
|
536
|
+
const reusable = checkpoint?.file === file && sameReader && changedFrom !== null
|
|
537
|
+
&& changedFrom >= 0 && changedFrom <= checkpoint.messageCount
|
|
538
|
+
&& changedFrom <= parsed.length;
|
|
539
|
+
// Build transactionally. A backwards/truncated read must not corrupt the last readable checkpoint
|
|
540
|
+
// before the floor check rejects it.
|
|
541
|
+
const next = reusable && checkpoint ? cloneCheckpoint(checkpoint) : undefined;
|
|
542
|
+
const from = next ? changedFrom : 0;
|
|
543
|
+
const durableFloor = checkpoint?.snapshot.items.length ?? 0;
|
|
544
|
+
const items = next?.snapshot.items ?? [];
|
|
545
|
+
const itemOffsets = next?.itemOffsets ?? [0];
|
|
546
|
+
const historyHashes = next?.historyHashes ?? [];
|
|
547
|
+
const prefixItems = itemOffsets[from] ?? 0;
|
|
548
|
+
items.length = prefixItems;
|
|
549
|
+
itemOffsets.length = from + 1;
|
|
550
|
+
historyHashes.length = from;
|
|
551
|
+
let historyHash = historyHashes.at(-1) ?? HISTORY_HASH_SEED;
|
|
552
|
+
for (let index = from; index < parsed.length; index++) {
|
|
553
|
+
const projected = projectDurableMessage(parsed[index], session.sessionId);
|
|
554
|
+
items.push(...projected);
|
|
555
|
+
itemOffsets.push(items.length);
|
|
556
|
+
historyHash = createHash('sha256')
|
|
557
|
+
.update(historyHash)
|
|
558
|
+
.update('\0')
|
|
559
|
+
.update(JSON.stringify(projected))
|
|
560
|
+
.digest('hex');
|
|
561
|
+
historyHashes.push(historyHash);
|
|
562
|
+
}
|
|
563
|
+
if (items.length < durableFloor) {
|
|
564
|
+
throw new Error('Codex durable rollout projection moved backwards');
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
file,
|
|
568
|
+
...(version === undefined ? {} : { version }),
|
|
569
|
+
...(readerGeneration === undefined ? {} : { readerGeneration }),
|
|
570
|
+
messageCount: parsed.length,
|
|
571
|
+
itemOffsets,
|
|
572
|
+
historyHashes,
|
|
573
|
+
snapshot: {
|
|
574
|
+
sessionId: session.sessionId,
|
|
575
|
+
sourceViewId: viewId(session.sessionId),
|
|
576
|
+
sourceHistoryToken: historyHash,
|
|
577
|
+
items,
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function readDurableSnapshot(session) {
|
|
582
|
+
const file = await findRollout(sessionsRoot, session.sessionId);
|
|
583
|
+
const read = file && reader.readSnapshot
|
|
584
|
+
? await reader.readSnapshot(file, createCodexTranscriptParser)
|
|
585
|
+
: {
|
|
586
|
+
messages: file ? await reader.read(file, createCodexTranscriptParser) : [],
|
|
587
|
+
version: undefined,
|
|
588
|
+
changedFrom: 0,
|
|
589
|
+
generation: undefined,
|
|
590
|
+
};
|
|
591
|
+
const known = durableCheckpoints.get(session.sessionId);
|
|
592
|
+
if (known && known.file === file && read.version !== undefined && known.version === read.version) {
|
|
593
|
+
rememberCheckpoint(session.sessionId, known);
|
|
594
|
+
return known.snapshot;
|
|
595
|
+
}
|
|
596
|
+
const sameReader = read.generation === undefined || known?.readerGeneration === undefined
|
|
597
|
+
|| known.readerGeneration === read.generation;
|
|
598
|
+
if (known && known.file === file && sameReader && read.changedFrom === null) {
|
|
599
|
+
if (read.version === undefined)
|
|
600
|
+
delete known.version;
|
|
601
|
+
else
|
|
602
|
+
known.version = read.version;
|
|
603
|
+
rememberCheckpoint(session.sessionId, known);
|
|
604
|
+
return known.snapshot;
|
|
605
|
+
}
|
|
606
|
+
const checkpoint = snapshotFromMessages(session, read.messages, file, read.version, read.generation, read.changedFrom, known);
|
|
607
|
+
rememberCheckpoint(session.sessionId, checkpoint);
|
|
608
|
+
return checkpoint.snapshot;
|
|
609
|
+
}
|
|
610
|
+
async function captureDurableCutoff(sessionId) {
|
|
611
|
+
const file = await findRollout(sessionsRoot, sessionId);
|
|
612
|
+
if (!file)
|
|
613
|
+
return { file: null, endExclusive: 0 };
|
|
614
|
+
const endExclusive = await rolloutSize(file);
|
|
615
|
+
return typeof endExclusive === 'number' && Number.isSafeInteger(endExclusive) && endExclusive >= 0
|
|
616
|
+
? { file, endExclusive }
|
|
617
|
+
: { file: null, endExclusive: 0 };
|
|
618
|
+
}
|
|
619
|
+
async function readDurablePrefix(session, cutoff) {
|
|
620
|
+
const known = durableCheckpoints.get(session.sessionId);
|
|
621
|
+
const working = known ? cloneCheckpoint(known) : undefined;
|
|
622
|
+
const read = cutoff.file && reader.readPrefixSnapshot
|
|
623
|
+
? await reader.readPrefixSnapshot(cutoff.file, cutoff.endExclusive, createCodexTranscriptParser)
|
|
624
|
+
: {
|
|
625
|
+
messages: cutoff.file
|
|
626
|
+
? await readPrefix(cutoff.file, cutoff.endExclusive, createCodexTranscriptParser) : [],
|
|
627
|
+
version: undefined,
|
|
628
|
+
changedFrom: 0,
|
|
629
|
+
generation: undefined,
|
|
630
|
+
};
|
|
631
|
+
const checkpoint = working && working.file === cutoff.file
|
|
632
|
+
&& read.version !== undefined && working.version === read.version
|
|
633
|
+
? working
|
|
634
|
+
: snapshotFromMessages(session, read.messages, cutoff.file, read.version, read.generation, read.changedFrom, working);
|
|
635
|
+
return {
|
|
636
|
+
checkpoint,
|
|
637
|
+
snapshot: { ...checkpoint.snapshot, items: [...checkpoint.snapshot.items] },
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
async function readPage(session, request) {
|
|
641
|
+
const opening = request.beforeSourceCursor === undefined
|
|
642
|
+
? openingSnapshots.get(session.sessionId) : undefined;
|
|
643
|
+
const snapshot = opening?.snapshot ?? await readDurableSnapshot(session);
|
|
644
|
+
let end = snapshot.items.length;
|
|
645
|
+
if (request.beforeSourceCursor !== undefined) {
|
|
646
|
+
if (!/^\d+$/.test(request.beforeSourceCursor))
|
|
647
|
+
throw new Error('Invalid Codex source cursor');
|
|
648
|
+
const cursor = Number(request.beforeSourceCursor);
|
|
649
|
+
if (!Number.isSafeInteger(cursor))
|
|
650
|
+
throw new Error('Invalid Codex source cursor');
|
|
651
|
+
end = Math.min(cursor, snapshot.items.length);
|
|
652
|
+
}
|
|
653
|
+
const start = Math.max(0, end - request.limit);
|
|
654
|
+
const page = {
|
|
655
|
+
sessionId: snapshot.sessionId,
|
|
656
|
+
sourceViewId: snapshot.sourceViewId,
|
|
657
|
+
sourceHistoryToken: snapshot.sourceHistoryToken,
|
|
658
|
+
items: snapshot.items.slice(start, end),
|
|
659
|
+
...(start > 0 ? { previousSourceCursor: String(start) } : {}),
|
|
660
|
+
hasMore: start > 0,
|
|
661
|
+
};
|
|
662
|
+
if (opening && openingSnapshots.get(session.sessionId) === opening) {
|
|
663
|
+
openingSnapshots.delete(session.sessionId);
|
|
664
|
+
opening.release();
|
|
665
|
+
}
|
|
666
|
+
return page;
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
apiVersion: 1,
|
|
670
|
+
async discoverNative(target) {
|
|
671
|
+
if (target.agentId !== 'codex' || !target.sessionId)
|
|
672
|
+
return null;
|
|
673
|
+
if (isRun(target)) {
|
|
674
|
+
const discovered = await app.discover(target.paneId);
|
|
675
|
+
if (!discovered?.managed || discovered.threadId !== target.sessionId)
|
|
676
|
+
return null;
|
|
677
|
+
return {
|
|
678
|
+
session: { agentId: 'codex', sessionId: target.sessionId },
|
|
679
|
+
run: target,
|
|
680
|
+
sourceViewId: viewId(target.sessionId),
|
|
681
|
+
capabilities: {
|
|
682
|
+
history: true, live: 'delta', sendable: true, steer: true,
|
|
683
|
+
send: ['prompt'], interrupt: true,
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
const file = await findRollout(sessionsRoot, target.sessionId);
|
|
688
|
+
if (!file)
|
|
689
|
+
return null;
|
|
690
|
+
return {
|
|
691
|
+
session: target,
|
|
692
|
+
sourceViewId: viewId(target.sessionId),
|
|
693
|
+
capabilities: { history: true, live: 'poll' },
|
|
694
|
+
};
|
|
695
|
+
},
|
|
696
|
+
readNativePage: readPage,
|
|
697
|
+
async observeNative(run, sink) {
|
|
698
|
+
if (!run.ref.sessionId)
|
|
699
|
+
throw new Error('Codex run has no thread');
|
|
700
|
+
const sessionId = run.ref.sessionId;
|
|
701
|
+
const knownCheckpoint = durableCheckpoints.get(sessionId);
|
|
702
|
+
const live = {
|
|
703
|
+
sink, sourceSequence: 0, closed: false, provisional: new Map(),
|
|
704
|
+
pendingDurableItems: new Map(),
|
|
705
|
+
observedTurnIds: new Set(),
|
|
706
|
+
tail: Promise.resolve(), closeNative: undefined, durableToken: undefined,
|
|
707
|
+
durableItems: [],
|
|
708
|
+
durableCandidateToken: undefined, durableCandidateAt: undefined,
|
|
709
|
+
pollTimer: undefined, pollDueAt: undefined,
|
|
710
|
+
openingPagePending: true,
|
|
711
|
+
};
|
|
712
|
+
const emit = async (event) => {
|
|
713
|
+
if (live.closed)
|
|
714
|
+
return;
|
|
715
|
+
await sink({ ...event, sourceSequence: ++live.sourceSequence });
|
|
716
|
+
};
|
|
717
|
+
const closeLive = async () => {
|
|
718
|
+
if (live.closed)
|
|
719
|
+
return;
|
|
720
|
+
live.closed = true;
|
|
721
|
+
if (live.pollTimer)
|
|
722
|
+
clearTimeout(live.pollTimer);
|
|
723
|
+
live.pollTimer = undefined;
|
|
724
|
+
live.pollDueAt = undefined;
|
|
725
|
+
live.durableCandidateToken = undefined;
|
|
726
|
+
live.durableCandidateAt = undefined;
|
|
727
|
+
live.provisional.clear();
|
|
728
|
+
live.pendingDurableItems.clear();
|
|
729
|
+
live.observedTurnIds.clear();
|
|
730
|
+
live.durableItems = [];
|
|
731
|
+
const opening = openingSnapshots.get(sessionId);
|
|
732
|
+
if (opening?.owner === live)
|
|
733
|
+
openingSnapshots.delete(sessionId);
|
|
734
|
+
await live.closeNative?.();
|
|
735
|
+
};
|
|
736
|
+
const failClosed = async () => {
|
|
737
|
+
if (live.closed)
|
|
738
|
+
return;
|
|
739
|
+
try {
|
|
740
|
+
await emit({ type: 'stream.gap', afterSourceSequence: live.sourceSequence });
|
|
741
|
+
}
|
|
742
|
+
catch { /* the sink itself is already unavailable */ }
|
|
743
|
+
await closeLive();
|
|
744
|
+
};
|
|
745
|
+
const reconcileDurable = async (retryReadFailure) => {
|
|
746
|
+
if (live.closed || live.openingPagePending)
|
|
747
|
+
return;
|
|
748
|
+
const deadlineReached = (pending, now) => (waitsForNextTurn(pending.item)
|
|
749
|
+
? pending.uncoveredAdvanceAt !== undefined
|
|
750
|
+
&& now - pending.uncoveredAdvanceAt >= durableSettleTimeoutMs
|
|
751
|
+
: now - pending.settledAt >= durableSettleTimeoutMs);
|
|
752
|
+
let page;
|
|
753
|
+
try {
|
|
754
|
+
page = await readDurableSnapshot({ agentId: 'codex', sessionId });
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
// A writer can transiently rotate or replace the rollout. Periodic reconciliation retries
|
|
758
|
+
// read failures; the initial baseline still fails the open because Core has no safe checkpoint.
|
|
759
|
+
if (retryReadFailure) {
|
|
760
|
+
if ([...live.pendingDurableItems.values()].some((pending) => (deadlineReached(pending, Date.now()))))
|
|
761
|
+
throw new Error('Codex settled item did not become durable');
|
|
762
|
+
live.durableCandidateToken = undefined;
|
|
763
|
+
live.durableCandidateAt = undefined;
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
768
|
+
if (live.closed)
|
|
769
|
+
return;
|
|
770
|
+
const now = Date.now();
|
|
771
|
+
if (live.durableToken === undefined) {
|
|
772
|
+
live.durableToken = page.sourceHistoryToken;
|
|
773
|
+
live.durableItems = [...page.items];
|
|
774
|
+
live.durableCandidateToken = undefined;
|
|
775
|
+
live.durableCandidateAt = undefined;
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
let coveredPendingIds = [];
|
|
779
|
+
const rememberCoveredGoals = (ids) => {
|
|
780
|
+
for (const id of ids) {
|
|
781
|
+
const pending = live.pendingDurableItems.get(id);
|
|
782
|
+
if (pending && goalIdentity(pending.item)) {
|
|
783
|
+
rememberGoalLifecycle(durablyOwnedGoalLifecycleIds, sessionId, id);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
if (live.pendingDurableItems.size > 0) {
|
|
788
|
+
coveredPendingIds = coveredPendingItemIds(live.pendingDurableItems, page.items);
|
|
789
|
+
// A settled live item remains part of the visible frontier until an equivalent durable item
|
|
790
|
+
// is readable. Publishing an earlier user/tool-only token would erase that reply on page seed.
|
|
791
|
+
if (coveredPendingIds.length !== live.pendingDurableItems.size) {
|
|
792
|
+
const covered = new Set(coveredPendingIds);
|
|
793
|
+
let expired = false;
|
|
794
|
+
for (const [id, pending] of live.pendingDurableItems) {
|
|
795
|
+
if (covered.has(id))
|
|
796
|
+
continue;
|
|
797
|
+
if (waitsForNextTurn(pending.item)) {
|
|
798
|
+
const nextTurnVisible = [...live.observedTurnIds].some((turnId) => (!pending.baselineTurnIds.has(turnId))) || page.items.some((item) => (typeof item.groupingId === 'string' && item.groupingId
|
|
799
|
+
&& !pending.baselineTurnIds.has(item.groupingId)));
|
|
800
|
+
if (!nextTurnVisible) {
|
|
801
|
+
delete pending.uncoveredAdvanceAt;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
pending.uncoveredAdvanceAt ??= now;
|
|
805
|
+
}
|
|
806
|
+
if (deadlineReached(pending, now))
|
|
807
|
+
expired = true;
|
|
808
|
+
}
|
|
809
|
+
// An item that never becomes durable must not poison every later history token forever.
|
|
810
|
+
// An active Goal starts this deadline only after a new native turn or durable correlation;
|
|
811
|
+
// the current turn may keep appending long after the Goal was set. Fail closed via Core's gap.
|
|
812
|
+
if (expired)
|
|
813
|
+
throw new Error('Codex settled item did not become durable');
|
|
814
|
+
live.durableCandidateToken = undefined;
|
|
815
|
+
live.durableCandidateAt = undefined;
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (page.sourceHistoryToken === live.durableToken) {
|
|
820
|
+
rememberCoveredGoals(coveredPendingIds);
|
|
821
|
+
for (const id of coveredPendingIds)
|
|
822
|
+
live.pendingDurableItems.delete(id);
|
|
823
|
+
live.durableCandidateToken = undefined;
|
|
824
|
+
live.durableCandidateAt = undefined;
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
// Do not advance the durable barrier while a provisional reply is still streaming: the page seed
|
|
828
|
+
// would otherwise replace a live frontier whose remaining native deltas are not durable yet.
|
|
829
|
+
if (live.provisional.size > 0) {
|
|
830
|
+
live.durableCandidateToken = undefined;
|
|
831
|
+
live.durableCandidateAt = undefined;
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
// Require the same readable token twice. Codex appends several adjacent rollout records per
|
|
835
|
+
// action; this quiet-read barrier avoids publishing a token that changes before Core verifies it.
|
|
836
|
+
if (live.durableCandidateToken !== page.sourceHistoryToken) {
|
|
837
|
+
live.durableCandidateToken = page.sourceHistoryToken;
|
|
838
|
+
live.durableCandidateAt = Date.now();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (live.durableCandidateAt === undefined
|
|
842
|
+
|| Date.now() - live.durableCandidateAt < DURABLE_QUIET_MS)
|
|
843
|
+
return;
|
|
844
|
+
live.durableToken = page.sourceHistoryToken;
|
|
845
|
+
live.durableItems = [...page.items];
|
|
846
|
+
rememberCoveredGoals(coveredPendingIds);
|
|
847
|
+
for (const id of coveredPendingIds)
|
|
848
|
+
live.pendingDurableItems.delete(id);
|
|
849
|
+
live.durableCandidateToken = undefined;
|
|
850
|
+
live.durableCandidateAt = undefined;
|
|
851
|
+
await emit({
|
|
852
|
+
type: 'history.committed', sourceViewId: page.sourceViewId,
|
|
853
|
+
sourceHistoryToken: page.sourceHistoryToken,
|
|
854
|
+
});
|
|
855
|
+
};
|
|
856
|
+
const armDurablePoll = (delay = durablePollMs) => {
|
|
857
|
+
if (live.closed || live.openingPagePending)
|
|
858
|
+
return;
|
|
859
|
+
const dueAt = Date.now() + delay;
|
|
860
|
+
if (live.pollTimer && live.pollDueAt !== undefined && live.pollDueAt <= dueAt)
|
|
861
|
+
return;
|
|
862
|
+
if (live.pollTimer)
|
|
863
|
+
clearTimeout(live.pollTimer);
|
|
864
|
+
live.pollDueAt = dueAt;
|
|
865
|
+
live.pollTimer = setTimeout(() => {
|
|
866
|
+
live.pollTimer = undefined;
|
|
867
|
+
live.pollDueAt = undefined;
|
|
868
|
+
const pending = live.tail.then(() => reconcileDurable(true));
|
|
869
|
+
live.tail = pending.catch(failClosed);
|
|
870
|
+
void live.tail.finally(() => {
|
|
871
|
+
armDurablePoll(live.durableCandidateToken ? DURABLE_QUIET_MS : durablePollMs);
|
|
872
|
+
}).catch(() => { });
|
|
873
|
+
}, delay);
|
|
874
|
+
live.pollTimer.unref?.();
|
|
875
|
+
};
|
|
876
|
+
const process = async (value, suppressMutation = false) => {
|
|
877
|
+
const event = parseCodexStreamEvent(value);
|
|
878
|
+
if (!event || live.closed)
|
|
879
|
+
return;
|
|
880
|
+
if ('turnId' in event && typeof event.turnId === 'string' && event.turnId) {
|
|
881
|
+
live.observedTurnIds.add(event.turnId);
|
|
882
|
+
const observedAt = Date.now();
|
|
883
|
+
for (const pending of live.pendingDurableItems.values()) {
|
|
884
|
+
if (waitsForNextTurn(pending.item) && !pending.baselineTurnIds.has(event.turnId)) {
|
|
885
|
+
pending.uncoveredAdvanceAt ??= observedAt;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (event.type === 'cursorReset' || event.type === 'disconnected' || event.type === 'error') {
|
|
890
|
+
await emit({ type: 'stream.gap', afterSourceSequence: live.sourceSequence });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (event.type === 'conversationSnapshot') {
|
|
894
|
+
await reconcileDurable(true);
|
|
895
|
+
armDurablePoll(DURABLE_QUIET_MS);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (event.type === 'turnCompleted') {
|
|
899
|
+
for (const [provisionalId, current] of live.provisional) {
|
|
900
|
+
if (current.turnId !== event.turnId)
|
|
901
|
+
continue;
|
|
902
|
+
live.provisional.delete(provisionalId);
|
|
903
|
+
await emit({
|
|
904
|
+
type: 'item.cancelled', provisionalId,
|
|
905
|
+
reason: event.status === 'interrupted' ? 'interrupted' : 'stream_reset',
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
await reconcileDurable(true);
|
|
909
|
+
armDurablePoll(DURABLE_QUIET_MS);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (event.type === 'goalCleared') {
|
|
913
|
+
for (const [id, pending] of live.pendingDurableItems) {
|
|
914
|
+
if (waitsForNextTurn(pending.item) && pending.uncoveredAdvanceAt === undefined) {
|
|
915
|
+
live.pendingDurableItems.delete(id);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
await reconcileDurable(true);
|
|
919
|
+
armDurablePoll(DURABLE_QUIET_MS);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (event.type === 'ready')
|
|
923
|
+
return;
|
|
924
|
+
const mutation = projectCodexConversationMutation(event);
|
|
925
|
+
if (!mutation || mutation.operation !== 'upsert')
|
|
926
|
+
return;
|
|
927
|
+
const message = mutation.message;
|
|
928
|
+
const draft = liveDraft(message);
|
|
929
|
+
if (!draft)
|
|
930
|
+
return;
|
|
931
|
+
const provisionalId = message.id;
|
|
932
|
+
const current = live.provisional.get(provisionalId);
|
|
933
|
+
const completedItem = message.completed ? finalItem(provisionalId, sessionId, draft) : null;
|
|
934
|
+
if (completedItem && goalIdentity(completedItem)) {
|
|
935
|
+
// App Server has only one current Goal. A later Goal lifecycle supersedes any older active card
|
|
936
|
+
// that never reached rollout, otherwise the impossible old occurrence blocks every future token.
|
|
937
|
+
for (const [id, pending] of live.pendingDurableItems) {
|
|
938
|
+
if (id !== provisionalId && waitsForNextTurn(pending.item)
|
|
939
|
+
&& pending.uncoveredAdvanceAt === undefined) {
|
|
940
|
+
live.pendingDurableItems.delete(id);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (suppressMutation)
|
|
945
|
+
return;
|
|
946
|
+
if (completedItem
|
|
947
|
+
&& live.durableItems.some((item) => durableOwnsSettledItem(completedItem, item))) {
|
|
948
|
+
if (goalIdentity(completedItem)) {
|
|
949
|
+
rememberGoalLifecycle(durablyOwnedGoalLifecycleIds, sessionId, provisionalId);
|
|
950
|
+
}
|
|
951
|
+
// The durable baseline can race ahead of a native event with the same stable item identity.
|
|
952
|
+
// The page already owns that position, so do not append a redundant live card.
|
|
953
|
+
if (current) {
|
|
954
|
+
live.provisional.delete(provisionalId);
|
|
955
|
+
await emit({ type: 'item.cancelled', provisionalId, reason: 'superseded' });
|
|
956
|
+
}
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (!current) {
|
|
960
|
+
live.provisional.set(provisionalId, { draft, turnId: message.turnId });
|
|
961
|
+
await emit({ type: 'item.opened', provisionalId, draft });
|
|
962
|
+
}
|
|
963
|
+
else if (mutation.mode === 'append' && message.type === 'text') {
|
|
964
|
+
await emit({
|
|
965
|
+
type: 'item.delta', provisionalId,
|
|
966
|
+
delta: { op: 'text.append', target: 'message.content', blockIndex: 0, text: message.text },
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
live.provisional.set(provisionalId, { draft, turnId: message.turnId });
|
|
971
|
+
await emit({ type: 'item.delta', provisionalId, delta: { op: 'item.replace', draft } });
|
|
972
|
+
}
|
|
973
|
+
if (message.completed) {
|
|
974
|
+
const settled = live.provisional.get(provisionalId)?.draft ?? draft;
|
|
975
|
+
live.provisional.delete(provisionalId);
|
|
976
|
+
const item = finalItem(provisionalId, sessionId, settled);
|
|
977
|
+
live.pendingDurableItems.set(provisionalId, {
|
|
978
|
+
item,
|
|
979
|
+
baselineMatches: durableMatchCount(item, live.durableItems),
|
|
980
|
+
settledAt: Date.now(),
|
|
981
|
+
baselineTurnIds: new Set([
|
|
982
|
+
...live.observedTurnIds,
|
|
983
|
+
...live.durableItems.flatMap((durable) => (durable.groupingId ? [durable.groupingId] : [])),
|
|
984
|
+
]),
|
|
985
|
+
});
|
|
986
|
+
await emit({
|
|
987
|
+
type: 'item.settled', provisionalId, durableItemId: provisionalId,
|
|
988
|
+
item,
|
|
989
|
+
});
|
|
990
|
+
await reconcileDurable(true);
|
|
991
|
+
armDurablePoll(DURABLE_QUIET_MS);
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
const enqueueNative = (event, suppressMutation = false) => {
|
|
995
|
+
live.tail = live.tail.then(() => process(event, suppressMutation)).catch(failClosed);
|
|
996
|
+
};
|
|
997
|
+
// The native observer must exist before the durable cutoff/read, but no event may calculate its
|
|
998
|
+
// Goal occurrence baseline until that prefix finishes. Buffer this short opening window explicitly.
|
|
999
|
+
let opening = true;
|
|
1000
|
+
let openingOverflow = false;
|
|
1001
|
+
const openingEvents = [];
|
|
1002
|
+
try {
|
|
1003
|
+
const native = await app.observeConversation(run.ref.paneId, sessionId, (event) => {
|
|
1004
|
+
if (live.closed)
|
|
1005
|
+
return;
|
|
1006
|
+
if (opening) {
|
|
1007
|
+
if (openingEvents.length >= MAX_OPENING_NATIVE_EVENTS)
|
|
1008
|
+
openingOverflow = true;
|
|
1009
|
+
else
|
|
1010
|
+
openingEvents.push(event);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
enqueueNative(event);
|
|
1014
|
+
});
|
|
1015
|
+
live.closeNative = native.close;
|
|
1016
|
+
if (live.closed) {
|
|
1017
|
+
await native.close();
|
|
1018
|
+
throw new Error('Codex Conversation observation failed while opening');
|
|
1019
|
+
}
|
|
1020
|
+
if (openingOverflow)
|
|
1021
|
+
throw new Error('Codex Conversation opening event buffer overflowed');
|
|
1022
|
+
// Capture the append-only byte boundary only after the observer owns the native suffix. Anything
|
|
1023
|
+
// appended later belongs to that buffered suffix or the first post-open durable reconciliation.
|
|
1024
|
+
const cutoff = await captureDurableCutoff(sessionId);
|
|
1025
|
+
const openingRead = await readDurablePrefix({ agentId: 'codex', sessionId }, cutoff);
|
|
1026
|
+
const baseline = openingRead.snapshot;
|
|
1027
|
+
live.durableToken = baseline.sourceHistoryToken;
|
|
1028
|
+
live.durableItems = [...baseline.items];
|
|
1029
|
+
if (openingOverflow)
|
|
1030
|
+
throw new Error('Codex Conversation opening event buffer overflowed');
|
|
1031
|
+
const knownLength = trustedCheckpointLength(knownCheckpoint, cutoff, baseline);
|
|
1032
|
+
const openingSuppression = suppressedOpeningEventIndexes(openingEvents, sessionId, baseline, knownLength, durablyOwnedGoalLifecycleIds);
|
|
1033
|
+
for (const id of openingSuppression.durablyOwnedGoalIds) {
|
|
1034
|
+
rememberGoalLifecycle(durablyOwnedGoalLifecycleIds, sessionId, id);
|
|
1035
|
+
}
|
|
1036
|
+
rememberCheckpoint(sessionId, openingRead.checkpoint);
|
|
1037
|
+
opening = false;
|
|
1038
|
+
openingEvents.forEach((event, index) => (enqueueNative(event, openingSuppression.suppressed.has(index))));
|
|
1039
|
+
openingEvents.length = 0;
|
|
1040
|
+
await live.tail;
|
|
1041
|
+
if (live.closed)
|
|
1042
|
+
throw new Error('Codex Conversation observation failed while opening');
|
|
1043
|
+
const release = () => {
|
|
1044
|
+
if (live.closed || !live.openingPagePending)
|
|
1045
|
+
return;
|
|
1046
|
+
live.openingPagePending = false;
|
|
1047
|
+
armDurablePoll(0);
|
|
1048
|
+
};
|
|
1049
|
+
openingSnapshots.set(sessionId, { owner: live, snapshot: baseline, release });
|
|
1050
|
+
}
|
|
1051
|
+
catch (error) {
|
|
1052
|
+
opening = false;
|
|
1053
|
+
openingEvents.length = 0;
|
|
1054
|
+
await closeLive();
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
checkpoint: { sourceViewId: viewId(sessionId), sourceSequence: 0 },
|
|
1059
|
+
close: closeLive,
|
|
1060
|
+
};
|
|
1061
|
+
},
|
|
1062
|
+
async dispatchPrompt(run, request) {
|
|
1063
|
+
if (!run.ref.sessionId) {
|
|
1064
|
+
return { outcome: 'rejected', nativeMutation: false, reason: 'invalid_request' };
|
|
1065
|
+
}
|
|
1066
|
+
const result = await app.dispatchPrompt(run.ref.paneId, run.ref.sessionId, request.text, request.clientRequestId);
|
|
1067
|
+
const record = result && typeof result === 'object' ? result : {};
|
|
1068
|
+
if (record.busy === true)
|
|
1069
|
+
return { outcome: 'busy', nativeMutation: false };
|
|
1070
|
+
const body = record.result && typeof record.result === 'object'
|
|
1071
|
+
? record.result : {};
|
|
1072
|
+
const turn = body.turn && typeof body.turn === 'object'
|
|
1073
|
+
? body.turn : body;
|
|
1074
|
+
return {
|
|
1075
|
+
outcome: 'accepted',
|
|
1076
|
+
...(typeof turn.id === 'string' ? { nativeId: turn.id } : {}),
|
|
1077
|
+
};
|
|
1078
|
+
},
|
|
1079
|
+
async dispatchSteer(run, request) {
|
|
1080
|
+
if (!run.ref.sessionId) {
|
|
1081
|
+
return { outcome: 'rejected', nativeMutation: false, reason: 'invalid_request' };
|
|
1082
|
+
}
|
|
1083
|
+
const result = await app.dispatchSteer(run.ref.paneId, run.ref.sessionId, request.text, request.clientRequestId, request.plan);
|
|
1084
|
+
const record = result && typeof result === 'object' ? result : {};
|
|
1085
|
+
if (record.busy === true)
|
|
1086
|
+
return { outcome: 'busy', nativeMutation: false };
|
|
1087
|
+
const body = record.result && typeof record.result === 'object'
|
|
1088
|
+
? record.result : {};
|
|
1089
|
+
const turn = body.turn && typeof body.turn === 'object'
|
|
1090
|
+
? body.turn : body;
|
|
1091
|
+
return { outcome: 'accepted', ...(typeof turn.id === 'string' ? { nativeId: turn.id } : {}) };
|
|
1092
|
+
},
|
|
1093
|
+
async dispatchInterrupt(run) {
|
|
1094
|
+
if (!run.ref.sessionId)
|
|
1095
|
+
return { status: 'rejected', reason: 'invalid_request' };
|
|
1096
|
+
const result = await app.interrupt(run.ref.paneId, run.ref.sessionId);
|
|
1097
|
+
const record = result && typeof result === 'object' ? result : {};
|
|
1098
|
+
return record.interrupted === true
|
|
1099
|
+
? { status: 'accepted' }
|
|
1100
|
+
: { status: 'rejected', reason: 'provider_rejected' };
|
|
1101
|
+
},
|
|
1102
|
+
};
|
|
1103
|
+
}
|