@linzumi/cli 0.0.5-beta → 0.0.6-beta
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 +197 -85
- package/package.json +17 -11
- package/src/authResolution.ts +2 -0
- package/src/boundedCache.ts +57 -0
- package/src/channelSession.ts +907 -453
- package/src/codexRuntimeOptions.ts +80 -0
- package/src/dependencyStatus.ts +198 -0
- package/src/forwardTunnel.ts +834 -0
- package/src/forwardTunnelProtocol.ts +324 -0
- package/src/index.ts +414 -30
- package/src/kandanTls.ts +86 -0
- package/src/localCapabilities.ts +130 -0
- package/src/localCodexMessageState.ts +135 -0
- package/src/localCodexTurnState.ts +108 -0
- package/src/localEditor.ts +963 -0
- package/src/localEditorRuntime.ts +603 -0
- package/src/localForwarding.ts +500 -0
- package/src/oauth.ts +135 -4
- package/src/pendingKandanMessageQueue.ts +109 -0
- package/src/phoenix.ts +8 -0
- package/src/portForwardApproval.ts +181 -0
- package/src/portForwardWatcher.ts +404 -0
- package/src/protocol.ts +97 -3
- package/src/runner.ts +391 -30
- package/src/streamDeltaCoalescing.ts +129 -0
- package/src/streamDeltaQueue.ts +102 -0
package/src/channelSession.ts
CHANGED
|
@@ -33,6 +33,63 @@
|
|
|
33
33
|
Relationship: Surfaces non-auto-accepted Codex app-server approval requests
|
|
34
34
|
as Kandan message state, then resolves the blocked app-server request only
|
|
35
35
|
after a scoped Kandan approval control arrives.
|
|
36
|
+
|
|
37
|
+
- Date: 2026-04-26
|
|
38
|
+
Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
39
|
+
Relationship: Batches live Codex streaming deltas at the runner boundary so
|
|
40
|
+
one logical Codex item stays one Kandan row without per-token persistence
|
|
41
|
+
churn, while turn completion still flushes all pending live output before
|
|
42
|
+
final reconciliation.
|
|
43
|
+
|
|
44
|
+
- Date: 2026-04-26
|
|
45
|
+
Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
46
|
+
Relationship: Stores stream and turn registries in bounded keyed caches so
|
|
47
|
+
long local Codex turns avoid repeated array replacement while preserving the
|
|
48
|
+
deterministic reconciliation order required by the transcript state machine.
|
|
49
|
+
|
|
50
|
+
- Date: 2026-04-26
|
|
51
|
+
Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
52
|
+
Relationship: Keeps approval requests, TUI input mirrors, and turn membership
|
|
53
|
+
registries keyed by their stable logical ids, so approval resolution,
|
|
54
|
+
duplicate-output guards, and local-TUI mirroring remain constant-time under
|
|
55
|
+
long-running shared Codex sessions.
|
|
56
|
+
|
|
57
|
+
- Date: 2026-04-26
|
|
58
|
+
Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
59
|
+
Relationship: Delegates shared Codex stream timer/flush/forward-chain
|
|
60
|
+
behavior to `streamDeltaQueue.ts`, leaving this module to map each concrete
|
|
61
|
+
Codex event kind into Kandan transcript semantics.
|
|
62
|
+
|
|
63
|
+
- Date: 2026-04-26
|
|
64
|
+
Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
65
|
+
Relationship: Uses `pendingKandanMessageQueue.ts` for accepted human replies
|
|
66
|
+
so high-volume shared Codex threads drain pending work without repeated
|
|
67
|
+
`Array.shift()` reindexing, while preserving interrupt fusion and recovery
|
|
68
|
+
requeue semantics at this orchestration seam.
|
|
69
|
+
|
|
70
|
+
- Date: 2026-04-26
|
|
71
|
+
Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
72
|
+
Relationship: Delegates app-server runtime option construction to
|
|
73
|
+
`codexRuntimeOptions.ts` so this orchestrator applies a single tested mapping
|
|
74
|
+
for model, effort, fast tier, approval policy, and sandbox settings.
|
|
75
|
+
|
|
76
|
+
- Date: 2026-04-26
|
|
77
|
+
Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
78
|
+
Relationship: Delegates user-visible local Codex message state labels,
|
|
79
|
+
approval metadata extraction, and Codex notification-to-processing-reason
|
|
80
|
+
mapping to `localCodexMessageState.ts`.
|
|
81
|
+
|
|
82
|
+
- Date: 2026-04-26
|
|
83
|
+
Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
84
|
+
Relationship: Delegates pure Codex turn lifecycle queries to
|
|
85
|
+
`localCodexTurnState.ts` so interruption, completion, and failure paths share
|
|
86
|
+
one tested interpretation of idle/starting/active/completing states.
|
|
87
|
+
|
|
88
|
+
- Date: 2026-04-26
|
|
89
|
+
Spec: plans/2026-04-26-local-runner-port-forward-approval.md
|
|
90
|
+
Relationship: Watches descendant listener ports, prompts the authorized
|
|
91
|
+
Kandan listener user for approval, and resolves the approval into a dynamic
|
|
92
|
+
runner forwarding capability before publishing the same-origin preview URL.
|
|
36
93
|
*/
|
|
37
94
|
import {
|
|
38
95
|
availabilityMessage,
|
|
@@ -48,6 +105,10 @@ import {
|
|
|
48
105
|
} from "./channelSessionSupport";
|
|
49
106
|
import { createHash, randomUUID } from "node:crypto";
|
|
50
107
|
import { type CodexAppServerClient } from "./codexAppServer";
|
|
108
|
+
import {
|
|
109
|
+
codexThreadRuntimeOverrides,
|
|
110
|
+
codexTurnRuntimeOverrides,
|
|
111
|
+
} from "./codexRuntimeOptions";
|
|
51
112
|
import {
|
|
52
113
|
codexAssistantDeltaFromNotification,
|
|
53
114
|
codexAssistantStructuredMessage,
|
|
@@ -64,14 +125,46 @@ import {
|
|
|
64
125
|
webSearchProgressBody,
|
|
65
126
|
codexUserInputMessageFromNotification,
|
|
66
127
|
codexUserInputMessagesForTurn,
|
|
128
|
+
type CodexAssistantDelta,
|
|
129
|
+
type CodexCommandOutputDelta,
|
|
130
|
+
type CodexFileChangeDelta,
|
|
131
|
+
type CodexReasoningDelta,
|
|
67
132
|
type CodexUserInputMessage,
|
|
68
133
|
} from "./codexOutput";
|
|
69
134
|
import { arrayValue, integerValue, objectValue, stringValue } from "./json";
|
|
70
135
|
import {
|
|
71
136
|
codexInputForQueuedKandanMessage,
|
|
72
|
-
interruptQueuedMessages,
|
|
73
137
|
type QueuedKandanMessage,
|
|
74
138
|
} from "./kandanQueue";
|
|
139
|
+
import {
|
|
140
|
+
approvalRequestKey,
|
|
141
|
+
codexApprovalMessageState,
|
|
142
|
+
processingMessageStateFromActive,
|
|
143
|
+
processingReasonForCodexNotification,
|
|
144
|
+
type ActiveProcessingState,
|
|
145
|
+
type CodexApprovalMessageState,
|
|
146
|
+
type LocalCodexMessageState,
|
|
147
|
+
type LocalCodexProcessingReason,
|
|
148
|
+
} from "./localCodexMessageState";
|
|
149
|
+
import {
|
|
150
|
+
activeQueuedSeqForTurn,
|
|
151
|
+
activeTurnId,
|
|
152
|
+
completingQueuedSeqForTurn,
|
|
153
|
+
interruptibleQueuedSeq,
|
|
154
|
+
markInterruptAfterStart,
|
|
155
|
+
shouldClearTurnAfterFailure,
|
|
156
|
+
turnCanForwardByState,
|
|
157
|
+
type TurnState,
|
|
158
|
+
} from "./localCodexTurnState";
|
|
159
|
+
import {
|
|
160
|
+
createPendingKandanMessageQueue,
|
|
161
|
+
dequeuePendingKandanMessage,
|
|
162
|
+
enqueuePendingKandanMessage,
|
|
163
|
+
interruptPendingKandanMessages,
|
|
164
|
+
pendingKandanMessageQueueLength,
|
|
165
|
+
requeuePendingKandanMessageFront,
|
|
166
|
+
type PendingKandanMessageQueue,
|
|
167
|
+
} from "./pendingKandanMessageQueue";
|
|
75
168
|
import { type PhoenixClient } from "./phoenix";
|
|
76
169
|
import {
|
|
77
170
|
type JsonObject,
|
|
@@ -82,6 +175,46 @@ import {
|
|
|
82
175
|
isJsonObject,
|
|
83
176
|
} from "./protocol";
|
|
84
177
|
import { type RunnerLogger } from "./runnerLogger";
|
|
178
|
+
import {
|
|
179
|
+
boundedCacheValues,
|
|
180
|
+
createBoundedCache,
|
|
181
|
+
forgetBoundedCacheValue,
|
|
182
|
+
getBoundedCacheValue,
|
|
183
|
+
rememberBoundedCacheValue,
|
|
184
|
+
type BoundedCache,
|
|
185
|
+
} from "./boundedCache";
|
|
186
|
+
import {
|
|
187
|
+
coalesceAssistantDeltas,
|
|
188
|
+
coalesceCommandOutputDeltas,
|
|
189
|
+
coalesceFileChangeDeltas,
|
|
190
|
+
coalesceReasoningDeltas,
|
|
191
|
+
firstDeltaTurnId,
|
|
192
|
+
} from "./streamDeltaCoalescing";
|
|
193
|
+
import {
|
|
194
|
+
clearStreamDeltaFlushTimer,
|
|
195
|
+
createStreamDeltaQueue,
|
|
196
|
+
enqueueStreamDelta,
|
|
197
|
+
flushStreamDeltaQueue,
|
|
198
|
+
type StreamDeltaQueue,
|
|
199
|
+
type StreamDeltaQueueRuntime,
|
|
200
|
+
} from "./streamDeltaQueue";
|
|
201
|
+
import {
|
|
202
|
+
startPortForwardWatcher,
|
|
203
|
+
type PortForwardCandidate,
|
|
204
|
+
type PortForwardWatcher,
|
|
205
|
+
type PortForwardWatcherOptions,
|
|
206
|
+
} from "./portForwardWatcher";
|
|
207
|
+
import {
|
|
208
|
+
approvedTargetFromRequest,
|
|
209
|
+
forwardPreviewPath,
|
|
210
|
+
pendingRequestFromCandidate,
|
|
211
|
+
portForwardPromptBody,
|
|
212
|
+
portForwardPromptLabel,
|
|
213
|
+
portForwardPromptReason,
|
|
214
|
+
reviewPortForwardCandidate,
|
|
215
|
+
revocationCapabilities,
|
|
216
|
+
type PendingPortForwardRequest,
|
|
217
|
+
} from "./portForwardApproval";
|
|
85
218
|
|
|
86
219
|
export type ChannelSessionRuntime = {
|
|
87
220
|
readonly handleCodexNotification: (
|
|
@@ -102,9 +235,20 @@ export type ChannelSessionRunnerOptions = {
|
|
|
102
235
|
readonly codexBin: string;
|
|
103
236
|
readonly fast?: boolean | undefined;
|
|
104
237
|
readonly launchTui?: boolean | undefined;
|
|
238
|
+
readonly enablePortForwardWatch?: boolean | undefined;
|
|
239
|
+
readonly initialForwardPorts?: readonly number[] | undefined;
|
|
240
|
+
readonly suppressedForwardPorts?: (() => readonly number[]) | undefined;
|
|
241
|
+
readonly portForwardWatcher?: PortForwardWatchRuntimeOptions | undefined;
|
|
242
|
+
readonly onForwardPortApproved?: ((port: number) => JsonObject | undefined) | undefined;
|
|
243
|
+
readonly onForwardPortRevoked?: ((port: number) => JsonObject | undefined) | undefined;
|
|
105
244
|
readonly channelSession: KandanChannelSessionOptions;
|
|
106
245
|
};
|
|
107
246
|
|
|
247
|
+
type PortForwardWatchRuntimeOptions =
|
|
248
|
+
Partial<Omit<PortForwardWatcherOptions, "onCandidate">> & {
|
|
249
|
+
readonly start?: ((options: PortForwardWatcherOptions) => PortForwardWatcher) | undefined;
|
|
250
|
+
};
|
|
251
|
+
|
|
108
252
|
type ChannelSessionContext = {
|
|
109
253
|
readonly kandan: PhoenixClient;
|
|
110
254
|
readonly codex: CodexAppServerClient;
|
|
@@ -121,39 +265,39 @@ type ChannelSessionState = {
|
|
|
121
265
|
turn: TurnState;
|
|
122
266
|
closed: boolean;
|
|
123
267
|
minSeq: number;
|
|
124
|
-
queue:
|
|
125
|
-
forwardedTurnIds: string
|
|
126
|
-
forwardingTurnIds: string
|
|
127
|
-
retryableTurnIds: string
|
|
128
|
-
localTuiTurnIds: string
|
|
129
|
-
mirroredTuiInputProjections: MirroredTuiInputProjection
|
|
130
|
-
pendingTuiInputMirrors:
|
|
131
|
-
turnReplyTargets: TurnReplyTarget
|
|
132
|
-
streamingAssistantOutputs: StreamingAssistantOutput
|
|
133
|
-
streamingReasoningOutputs: StreamingReasoningOutput
|
|
134
|
-
streamingCommandOutputs: StreamingCommandOutput
|
|
135
|
-
streamingFileChangeOutputs: StreamingFileChangeOutput
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
268
|
+
queue: PendingKandanMessageQueue;
|
|
269
|
+
forwardedTurnIds: Set<string>;
|
|
270
|
+
forwardingTurnIds: Set<string>;
|
|
271
|
+
retryableTurnIds: Set<string>;
|
|
272
|
+
localTuiTurnIds: Set<string>;
|
|
273
|
+
mirroredTuiInputProjections: BoundedCache<MirroredTuiInputProjection>;
|
|
274
|
+
pendingTuiInputMirrors: Map<string, Promise<void>>;
|
|
275
|
+
turnReplyTargets: BoundedCache<TurnReplyTarget>;
|
|
276
|
+
streamingAssistantOutputs: BoundedCache<StreamingAssistantOutput>;
|
|
277
|
+
streamingReasoningOutputs: BoundedCache<StreamingReasoningOutput>;
|
|
278
|
+
streamingCommandOutputs: BoundedCache<StreamingCommandOutput>;
|
|
279
|
+
streamingFileChangeOutputs: BoundedCache<StreamingFileChangeOutput>;
|
|
280
|
+
assistantDeltaQueue: StreamDeltaQueue<CodexAssistantDelta>;
|
|
281
|
+
reasoningDeltaQueue: StreamDeltaQueue<CodexReasoningDelta>;
|
|
282
|
+
commandOutputQueue: StreamDeltaQueue<CodexCommandOutputDelta>;
|
|
283
|
+
fileChangeQueue: StreamDeltaQueue<CodexFileChangeDelta>;
|
|
284
|
+
forwardedTerminalInputKeys: Set<string>;
|
|
285
|
+
webSearchProgressOutputs: BoundedCache<WebSearchProgressOutput>;
|
|
286
|
+
pendingApprovalRequests: Map<string, PendingCodexApprovalRequest>;
|
|
287
|
+
pendingPortForwardRequests: Map<string, PendingPortForwardRequest>;
|
|
288
|
+
approvedForwardPorts: Set<number>;
|
|
289
|
+
approvedForwardTargets: Map<number, PortForwardCandidate>;
|
|
290
|
+
dismissedForwardTargets: Map<number, PortForwardCandidate>;
|
|
291
|
+
portForwardWatcher: PortForwardWatcher | undefined;
|
|
139
292
|
activeProcessingState: ActiveProcessingState | undefined;
|
|
140
|
-
assistantDeltaForwardChain: Promise<void>;
|
|
141
|
-
reasoningDeltaForwardChain: Promise<void>;
|
|
142
|
-
commandOutputForwardChain: Promise<void>;
|
|
143
|
-
fileChangeForwardChain: Promise<void>;
|
|
144
293
|
terminalInputForwardChain: Promise<void>;
|
|
145
294
|
webSearchProgressForwardChain: Promise<void>;
|
|
146
295
|
typingHeartbeat: ReturnType<typeof setInterval> | undefined;
|
|
147
296
|
typingHeartbeatInFlight: boolean;
|
|
148
297
|
};
|
|
149
298
|
|
|
150
|
-
type TurnState =
|
|
151
|
-
| { readonly status: "idle" }
|
|
152
|
-
| { readonly status: "starting"; readonly queuedSeq: number; readonly interruptAfterStart: boolean }
|
|
153
|
-
| { readonly status: "active"; readonly turnId: string; readonly queuedSeq: number }
|
|
154
|
-
| { readonly status: "completing"; readonly turnId: string; readonly queuedSeq: number };
|
|
155
|
-
|
|
156
299
|
const codexTypingHeartbeatMs = 5_000;
|
|
300
|
+
const defaultStreamFlushIntervalMs = 150;
|
|
157
301
|
const maxForwardedTurnIds = 64;
|
|
158
302
|
|
|
159
303
|
type StreamingAssistantOutput = {
|
|
@@ -200,27 +344,11 @@ type MirroredTuiInputProjection = {
|
|
|
200
344
|
readonly itemKeys: string[];
|
|
201
345
|
};
|
|
202
346
|
|
|
203
|
-
type PendingTuiInputMirror = {
|
|
204
|
-
readonly turnId: string;
|
|
205
|
-
readonly promise: Promise<void>;
|
|
206
|
-
};
|
|
207
|
-
|
|
208
347
|
type TurnReplyTarget = {
|
|
209
348
|
readonly turnId: string;
|
|
210
349
|
readonly replyToSeq: number;
|
|
211
350
|
};
|
|
212
351
|
|
|
213
|
-
type ActiveProcessingState =
|
|
214
|
-
| {
|
|
215
|
-
readonly seq: number;
|
|
216
|
-
readonly reason: Exclude<LocalCodexProcessingReason, "awaiting approval">;
|
|
217
|
-
}
|
|
218
|
-
| {
|
|
219
|
-
readonly seq: number;
|
|
220
|
-
readonly reason: "awaiting approval";
|
|
221
|
-
readonly approval: CodexApprovalMessageState;
|
|
222
|
-
};
|
|
223
|
-
|
|
224
352
|
type PendingCodexApprovalRequest = {
|
|
225
353
|
readonly requestId: string;
|
|
226
354
|
readonly sourceSeq: number;
|
|
@@ -229,12 +357,6 @@ type PendingCodexApprovalRequest = {
|
|
|
229
357
|
readonly reject: (error: Error) => void;
|
|
230
358
|
};
|
|
231
359
|
|
|
232
|
-
type CodexApprovalMessageState = {
|
|
233
|
-
readonly requestId: string;
|
|
234
|
-
readonly kind: string;
|
|
235
|
-
readonly summary: string;
|
|
236
|
-
};
|
|
237
|
-
|
|
238
360
|
export async function attachChannelSession(
|
|
239
361
|
args: ChannelSessionContext,
|
|
240
362
|
): Promise<ChannelSessionRuntime> {
|
|
@@ -283,6 +405,7 @@ export async function attachChannelSession(
|
|
|
283
405
|
await bindChannelSession(args, state, payloadContext);
|
|
284
406
|
await processBufferedKandanEvents(args, state, runnerIdentity, payloadContext, eventBuffer.pending);
|
|
285
407
|
eventBuffer.ready = true;
|
|
408
|
+
startPortForwardWatchIfEnabled(args, state, payloadContext);
|
|
286
409
|
await drainKandanMessageQueue(args, state, payloadContext);
|
|
287
410
|
|
|
288
411
|
return {
|
|
@@ -401,6 +524,9 @@ export async function attachChannelSession(
|
|
|
401
524
|
},
|
|
402
525
|
close: async () => {
|
|
403
526
|
state.closed = true;
|
|
527
|
+
state.portForwardWatcher?.close();
|
|
528
|
+
state.portForwardWatcher = undefined;
|
|
529
|
+
clearPendingStreamFlushTimers(state);
|
|
404
530
|
rejectPendingApprovalRequests(state, new Error("runner closed"));
|
|
405
531
|
await stopCodexTyping(args, state);
|
|
406
532
|
},
|
|
@@ -436,26 +562,31 @@ function initialChannelSessionState(
|
|
|
436
562
|
turn: { status: "idle" },
|
|
437
563
|
closed: false,
|
|
438
564
|
minSeq: cursor,
|
|
439
|
-
queue:
|
|
440
|
-
forwardedTurnIds:
|
|
441
|
-
forwardingTurnIds:
|
|
442
|
-
retryableTurnIds:
|
|
443
|
-
localTuiTurnIds:
|
|
444
|
-
mirroredTuiInputProjections:
|
|
445
|
-
pendingTuiInputMirrors:
|
|
446
|
-
turnReplyTargets:
|
|
447
|
-
streamingAssistantOutputs:
|
|
448
|
-
streamingReasoningOutputs:
|
|
449
|
-
streamingCommandOutputs:
|
|
450
|
-
streamingFileChangeOutputs:
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
565
|
+
queue: createPendingKandanMessageQueue(),
|
|
566
|
+
forwardedTurnIds: new Set<string>(),
|
|
567
|
+
forwardingTurnIds: new Set<string>(),
|
|
568
|
+
retryableTurnIds: new Set<string>(),
|
|
569
|
+
localTuiTurnIds: new Set<string>(),
|
|
570
|
+
mirroredTuiInputProjections: createBoundedCache(maxForwardedTurnIds),
|
|
571
|
+
pendingTuiInputMirrors: new Map<string, Promise<void>>(),
|
|
572
|
+
turnReplyTargets: createBoundedCache(maxForwardedTurnIds),
|
|
573
|
+
streamingAssistantOutputs: createBoundedCache(maxForwardedTurnIds),
|
|
574
|
+
streamingReasoningOutputs: createBoundedCache(maxForwardedTurnIds),
|
|
575
|
+
streamingCommandOutputs: createBoundedCache(maxForwardedTurnIds),
|
|
576
|
+
streamingFileChangeOutputs: createBoundedCache(maxForwardedTurnIds),
|
|
577
|
+
assistantDeltaQueue: createStreamDeltaQueue(),
|
|
578
|
+
reasoningDeltaQueue: createStreamDeltaQueue(),
|
|
579
|
+
commandOutputQueue: createStreamDeltaQueue(),
|
|
580
|
+
fileChangeQueue: createStreamDeltaQueue(),
|
|
581
|
+
forwardedTerminalInputKeys: new Set<string>(),
|
|
582
|
+
webSearchProgressOutputs: createBoundedCache(maxForwardedTurnIds),
|
|
583
|
+
pendingApprovalRequests: new Map<string, PendingCodexApprovalRequest>(),
|
|
584
|
+
pendingPortForwardRequests: new Map<string, PendingPortForwardRequest>(),
|
|
585
|
+
approvedForwardPorts: new Set<number>(),
|
|
586
|
+
approvedForwardTargets: new Map<number, PortForwardCandidate>(),
|
|
587
|
+
dismissedForwardTargets: new Map<number, PortForwardCandidate>(),
|
|
588
|
+
portForwardWatcher: undefined,
|
|
454
589
|
activeProcessingState: undefined,
|
|
455
|
-
assistantDeltaForwardChain: Promise.resolve(),
|
|
456
|
-
reasoningDeltaForwardChain: Promise.resolve(),
|
|
457
|
-
commandOutputForwardChain: Promise.resolve(),
|
|
458
|
-
fileChangeForwardChain: Promise.resolve(),
|
|
459
590
|
terminalInputForwardChain: Promise.resolve(),
|
|
460
591
|
webSearchProgressForwardChain: Promise.resolve(),
|
|
461
592
|
typingHeartbeat: undefined,
|
|
@@ -463,6 +594,27 @@ function initialChannelSessionState(
|
|
|
463
594
|
};
|
|
464
595
|
}
|
|
465
596
|
|
|
597
|
+
function startPortForwardWatchIfEnabled(
|
|
598
|
+
args: ChannelSessionContext,
|
|
599
|
+
state: ChannelSessionState,
|
|
600
|
+
payloadContext: RunnerPayloadContext,
|
|
601
|
+
): void {
|
|
602
|
+
if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const { start: configuredStart, ...watchOptions } = args.options.portForwardWatcher ?? {};
|
|
607
|
+
const start = configuredStart ?? startPortForwardWatcher;
|
|
608
|
+
for (const port of args.options.initialForwardPorts ?? []) {
|
|
609
|
+
state.approvedForwardPorts.add(port);
|
|
610
|
+
}
|
|
611
|
+
state.portForwardWatcher = start({
|
|
612
|
+
...watchOptions,
|
|
613
|
+
onCandidate: candidate => publishPortForwardPrompt(args, state, payloadContext, candidate),
|
|
614
|
+
onError: error => args.log("port_forward.watch_failed", { message: error.message }),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
466
618
|
async function bindChannelSession(
|
|
467
619
|
args: ChannelSessionContext,
|
|
468
620
|
state: ChannelSessionState,
|
|
@@ -531,6 +683,10 @@ async function handleChannelSessionControl(
|
|
|
531
683
|
return resolvePendingCodexApprovalRequest(args, state, control);
|
|
532
684
|
}
|
|
533
685
|
|
|
686
|
+
if (control.type === "resolve_port_forward_request") {
|
|
687
|
+
return resolvePendingPortForwardRequest(args, state, payloadContext, control);
|
|
688
|
+
}
|
|
689
|
+
|
|
534
690
|
if (control.type !== "interrupt_queued_messages") {
|
|
535
691
|
return undefined;
|
|
536
692
|
}
|
|
@@ -543,11 +699,8 @@ async function handleChannelSessionControl(
|
|
|
543
699
|
return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
|
|
544
700
|
}
|
|
545
701
|
|
|
546
|
-
const interrupted =
|
|
547
|
-
const activeQueuedSeq =
|
|
548
|
-
state.turn.status === "active" || state.turn.status === "starting"
|
|
549
|
-
? state.turn.queuedSeq
|
|
550
|
-
: undefined;
|
|
702
|
+
const interrupted = interruptPendingKandanMessages(state.queue, control.throughSeq);
|
|
703
|
+
const activeQueuedSeq = interruptibleQueuedSeq(state.turn);
|
|
551
704
|
const targetsActiveTurn =
|
|
552
705
|
control.throughSeq !== undefined && activeQueuedSeq === control.throughSeq;
|
|
553
706
|
|
|
@@ -563,7 +716,6 @@ async function handleChannelSessionControl(
|
|
|
563
716
|
turnId: state.turn.turnId,
|
|
564
717
|
});
|
|
565
718
|
|
|
566
|
-
state.queue = interrupted.ok ? interrupted.queue : state.queue;
|
|
567
719
|
state.turn = { status: "idle" };
|
|
568
720
|
clearActiveProcessingState(state, interruptedActiveSeq);
|
|
569
721
|
await publishMessageState(args, state.kandanThreadId, interruptedActiveSeq, {
|
|
@@ -571,23 +723,19 @@ async function handleChannelSessionControl(
|
|
|
571
723
|
});
|
|
572
724
|
break;
|
|
573
725
|
case "starting":
|
|
574
|
-
state.
|
|
575
|
-
state.turn = {
|
|
576
|
-
status: "starting",
|
|
577
|
-
queuedSeq: state.turn.queuedSeq,
|
|
578
|
-
interruptAfterStart: true,
|
|
579
|
-
};
|
|
726
|
+
state.turn = markInterruptAfterStart(state.turn);
|
|
580
727
|
break;
|
|
581
728
|
case "idle":
|
|
582
729
|
case "completing":
|
|
583
|
-
state.queue = interrupted.ok ? interrupted.queue : state.queue;
|
|
584
730
|
break;
|
|
585
731
|
}
|
|
586
732
|
|
|
587
733
|
args.log("codex.queued_messages_interrupted", {
|
|
588
734
|
through_seq: control.throughSeq ?? null,
|
|
589
735
|
selected_count: interrupted.ok ? interrupted.selectedCount : 0,
|
|
590
|
-
remaining_count: interrupted.ok
|
|
736
|
+
remaining_count: interrupted.ok
|
|
737
|
+
? interrupted.remainingCount
|
|
738
|
+
: pendingKandanMessageQueueLength(state.queue),
|
|
591
739
|
});
|
|
592
740
|
if (interrupted.ok) {
|
|
593
741
|
for (const seq of interrupted.selectedSeqs) {
|
|
@@ -618,19 +766,15 @@ async function resolvePendingCodexApprovalRequest(
|
|
|
618
766
|
return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
|
|
619
767
|
}
|
|
620
768
|
|
|
621
|
-
const approval = state.pendingApprovalRequests.
|
|
622
|
-
|
|
623
|
-
pending.requestId === control.requestId &&
|
|
624
|
-
pending.sourceSeq === control.sourceSeq,
|
|
769
|
+
const approval = state.pendingApprovalRequests.get(
|
|
770
|
+
approvalRequestKey(control.requestId, control.sourceSeq),
|
|
625
771
|
);
|
|
626
772
|
|
|
627
773
|
if (approval === undefined) {
|
|
628
774
|
return { instanceId: args.instanceId, ok: false, error: "approval_request_not_found" };
|
|
629
775
|
}
|
|
630
776
|
|
|
631
|
-
state.pendingApprovalRequests
|
|
632
|
-
pending => pending !== approval,
|
|
633
|
-
);
|
|
777
|
+
state.pendingApprovalRequests.delete(approvalRequestKey(control.requestId, control.sourceSeq));
|
|
634
778
|
|
|
635
779
|
const codexDecision = control.decision === "approve" ? "accept" : "decline";
|
|
636
780
|
approval.resolve({ decision: codexDecision });
|
|
@@ -651,6 +795,335 @@ async function resolvePendingCodexApprovalRequest(
|
|
|
651
795
|
return { instanceId: args.instanceId, ok: true };
|
|
652
796
|
}
|
|
653
797
|
|
|
798
|
+
async function resolvePendingPortForwardRequest(
|
|
799
|
+
args: ChannelSessionContext,
|
|
800
|
+
state: ChannelSessionState,
|
|
801
|
+
payloadContext: RunnerPayloadContext,
|
|
802
|
+
control: Extract<KandanControl, { readonly type: "resolve_port_forward_request" }>,
|
|
803
|
+
): Promise<JsonObject> {
|
|
804
|
+
if (!portForwardControlSenderAllowed(args, payloadContext, control)) {
|
|
805
|
+
args.log("port_forward.request_resolution_ignored", {
|
|
806
|
+
request_id: control.requestId,
|
|
807
|
+
actor_slug: control.actorSlug ?? null,
|
|
808
|
+
actor_user_id: control.actorUserId ?? null,
|
|
809
|
+
reason: "sender_not_allowed",
|
|
810
|
+
});
|
|
811
|
+
return { instanceId: args.instanceId, ok: false, error: "sender_not_allowed" };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const request = state.pendingPortForwardRequests.get(control.requestId);
|
|
815
|
+
|
|
816
|
+
if (request === undefined) {
|
|
817
|
+
return { instanceId: args.instanceId, ok: false, error: "port_forward_request_not_found" };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
state.pendingPortForwardRequests.delete(control.requestId);
|
|
821
|
+
|
|
822
|
+
if (control.decision === "deny") {
|
|
823
|
+
state.dismissedForwardTargets.set(request.port, approvedTargetFromRequest(request));
|
|
824
|
+
await publishMessageStateForPortForwardResult(args, state, request, "failed");
|
|
825
|
+
args.log("port_forward.request_denied", {
|
|
826
|
+
request_id: control.requestId,
|
|
827
|
+
port: request.port,
|
|
828
|
+
pid: request.pid,
|
|
829
|
+
});
|
|
830
|
+
return { instanceId: args.instanceId, ok: true };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
state.approvedForwardPorts.add(request.port);
|
|
834
|
+
state.approvedForwardTargets.set(request.port, approvedTargetFromRequest(request));
|
|
835
|
+
const capabilities = args.options.onForwardPortApproved?.(request.port);
|
|
836
|
+
await publishForwardPortResolvedEvent(args, request, capabilities);
|
|
837
|
+
await publishMessageStateForPortForwardResult(args, state, request, "processed");
|
|
838
|
+
await publishPortForwardReadyMessage(args, state, payloadContext, request);
|
|
839
|
+
|
|
840
|
+
args.log("port_forward.request_approved", {
|
|
841
|
+
request_id: control.requestId,
|
|
842
|
+
port: request.port,
|
|
843
|
+
pid: request.pid,
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
return { instanceId: args.instanceId, ok: true, port: request.port };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function portForwardControlSenderAllowed(
|
|
850
|
+
args: ChannelSessionContext,
|
|
851
|
+
payloadContext: RunnerPayloadContext,
|
|
852
|
+
control: Extract<KandanControl, { readonly type: "resolve_port_forward_request" }>,
|
|
853
|
+
): boolean {
|
|
854
|
+
if (control.actorSlug === undefined && control.actorUserId === undefined) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return senderAllowed(
|
|
859
|
+
args.options.channelSession.listenUser,
|
|
860
|
+
{
|
|
861
|
+
actorSlug: control.actorSlug,
|
|
862
|
+
actorUserId: control.actorUserId,
|
|
863
|
+
},
|
|
864
|
+
payloadContext.runnerIdentity,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function publishPortForwardPrompt(
|
|
869
|
+
args: ChannelSessionContext,
|
|
870
|
+
state: ChannelSessionState,
|
|
871
|
+
payloadContext: RunnerPayloadContext,
|
|
872
|
+
candidate: PortForwardCandidate,
|
|
873
|
+
): Promise<void> {
|
|
874
|
+
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
875
|
+
args.log("port_forward.prompt_skipped", {
|
|
876
|
+
port: candidate.port,
|
|
877
|
+
pid: candidate.pid,
|
|
878
|
+
reason: "thread_not_bound",
|
|
879
|
+
});
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const review = reviewPortForwardCandidate({
|
|
884
|
+
candidate,
|
|
885
|
+
threadBound: true,
|
|
886
|
+
suppressedPorts: new Set(args.options.suppressedForwardPorts?.() ?? []),
|
|
887
|
+
approvedPorts: state.approvedForwardPorts,
|
|
888
|
+
approvedTargets: state.approvedForwardTargets,
|
|
889
|
+
dismissedTargets: state.dismissedForwardTargets,
|
|
890
|
+
pendingRequests: Array.from(state.pendingPortForwardRequests.values()),
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
switch (review.type) {
|
|
894
|
+
case "skip":
|
|
895
|
+
return;
|
|
896
|
+
case "remember_approved_target":
|
|
897
|
+
state.approvedForwardTargets.set(review.target.port, review.target);
|
|
898
|
+
return;
|
|
899
|
+
case "revoke_and_prompt":
|
|
900
|
+
await revokeApprovedForwardPort(args, state, review.revoked, review.reason);
|
|
901
|
+
break;
|
|
902
|
+
case "prompt":
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const requestId = `port-forward-${randomUUID()}`;
|
|
907
|
+
const label = portForwardPromptLabel(candidate);
|
|
908
|
+
const body = portForwardPromptBody(candidate, requestId);
|
|
909
|
+
const payload = {
|
|
910
|
+
...localRunnerPayload(
|
|
911
|
+
args.options,
|
|
912
|
+
args.instanceId,
|
|
913
|
+
"port_forward_request",
|
|
914
|
+
state.codexThreadId,
|
|
915
|
+
payloadContext,
|
|
916
|
+
),
|
|
917
|
+
reply_to_seq: state.rootSeq ?? null,
|
|
918
|
+
structured: {
|
|
919
|
+
kind: "local_runner_port_forward_request",
|
|
920
|
+
request_id: requestId,
|
|
921
|
+
port: candidate.port,
|
|
922
|
+
pid: candidate.pid,
|
|
923
|
+
command: candidate.command,
|
|
924
|
+
...(candidate.cwd === undefined ? {} : { cwd: candidate.cwd }),
|
|
925
|
+
command_label: label,
|
|
926
|
+
},
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const reply = await pushOk(args.kandan, args.topic, "session:post_thread_message", {
|
|
930
|
+
workspace: args.options.channelSession.workspaceSlug,
|
|
931
|
+
channel: args.options.channelSession.channelSlug,
|
|
932
|
+
thread_id: state.kandanThreadId,
|
|
933
|
+
body,
|
|
934
|
+
payload,
|
|
935
|
+
});
|
|
936
|
+
const sourceSeq = integerValue(reply.seq);
|
|
937
|
+
|
|
938
|
+
if (sourceSeq === undefined) {
|
|
939
|
+
throw new Error("port forward prompt did not return a Kandan message seq");
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const request = pendingRequestFromCandidate({
|
|
943
|
+
requestId,
|
|
944
|
+
sourceSeq,
|
|
945
|
+
candidate,
|
|
946
|
+
});
|
|
947
|
+
state.pendingPortForwardRequests.set(requestId, request);
|
|
948
|
+
|
|
949
|
+
await publishForwardPortRequestedEvent(args, request);
|
|
950
|
+
await publishMessageState(args, state.kandanThreadId, sourceSeq, {
|
|
951
|
+
status: "processing",
|
|
952
|
+
reason: "awaiting approval",
|
|
953
|
+
approval: {
|
|
954
|
+
requestId,
|
|
955
|
+
kind: "local_runner_port_forward",
|
|
956
|
+
summary: `Open runner port ${candidate.port} from ${label}`,
|
|
957
|
+
reason: portForwardPromptReason(candidate),
|
|
958
|
+
choices: [
|
|
959
|
+
{
|
|
960
|
+
decision: "approve",
|
|
961
|
+
label: "Open preview",
|
|
962
|
+
description: `Allow Kandan to proxy HTTP, HTTPS, and WebSocket traffic for runner port ${candidate.port}.`,
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
decision: "deny",
|
|
966
|
+
label: "Deny",
|
|
967
|
+
description: `Keep runner port ${candidate.port} private for this runner session.`,
|
|
968
|
+
},
|
|
969
|
+
],
|
|
970
|
+
allowedActorSlug: args.options.channelSession.listenUser,
|
|
971
|
+
allowedActorUserId:
|
|
972
|
+
args.options.channelSession.listenUser.toLowerCase() ===
|
|
973
|
+
(payloadContext.runnerIdentity.actorUsername ?? "").toLowerCase()
|
|
974
|
+
? payloadContext.runnerIdentity.actorUserId
|
|
975
|
+
: undefined,
|
|
976
|
+
},
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
args.log("port_forward.request_pending", {
|
|
980
|
+
request_id: requestId,
|
|
981
|
+
port: candidate.port,
|
|
982
|
+
pid: candidate.pid,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function revokeApprovedForwardPort(
|
|
987
|
+
args: ChannelSessionContext,
|
|
988
|
+
state: ChannelSessionState,
|
|
989
|
+
target: PortForwardCandidate,
|
|
990
|
+
reason: "listener_changed",
|
|
991
|
+
): Promise<void> {
|
|
992
|
+
state.approvedForwardPorts.delete(target.port);
|
|
993
|
+
state.approvedForwardTargets.delete(target.port);
|
|
994
|
+
const capabilities = args.options.onForwardPortRevoked?.(target.port);
|
|
995
|
+
|
|
996
|
+
await pushOptional(args.kandan, args.topic, "forward_port_revoked", {
|
|
997
|
+
instanceId: args.instanceId,
|
|
998
|
+
port: target.port,
|
|
999
|
+
pid: target.pid,
|
|
1000
|
+
command: target.command,
|
|
1001
|
+
...(target.cwd === undefined ? {} : { cwd: target.cwd }),
|
|
1002
|
+
reason,
|
|
1003
|
+
capabilities: revocationCapabilities(capabilities, target.port),
|
|
1004
|
+
}, args.log);
|
|
1005
|
+
|
|
1006
|
+
args.log("port_forward.approved_port_revoked", {
|
|
1007
|
+
port: target.port,
|
|
1008
|
+
pid: target.pid,
|
|
1009
|
+
reason,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async function publishPortForwardReadyMessage(
|
|
1014
|
+
args: ChannelSessionContext,
|
|
1015
|
+
state: ChannelSessionState,
|
|
1016
|
+
payloadContext: RunnerPayloadContext,
|
|
1017
|
+
request: PendingPortForwardRequest,
|
|
1018
|
+
): Promise<void> {
|
|
1019
|
+
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const path = forwardPreviewPath(args.options.runnerId, request.port);
|
|
1024
|
+
await pushOptional(args.kandan, args.topic, "session:post_thread_message", {
|
|
1025
|
+
workspace: args.options.channelSession.workspaceSlug,
|
|
1026
|
+
channel: args.options.channelSession.channelSlug,
|
|
1027
|
+
thread_id: state.kandanThreadId,
|
|
1028
|
+
body: `Runner port ${request.port} is open in Kandan: [Open preview](${path})`,
|
|
1029
|
+
payload: {
|
|
1030
|
+
...localRunnerPayload(
|
|
1031
|
+
args.options,
|
|
1032
|
+
args.instanceId,
|
|
1033
|
+
"port_forward_ready",
|
|
1034
|
+
state.codexThreadId,
|
|
1035
|
+
payloadContext,
|
|
1036
|
+
),
|
|
1037
|
+
...(state.rootSeq === undefined ? {} : { reply_to_seq: state.rootSeq }),
|
|
1038
|
+
structured: {
|
|
1039
|
+
kind: "local_runner_port_forward_ready",
|
|
1040
|
+
status: "ready",
|
|
1041
|
+
summary: `Runner port ${request.port} is open in Kandan.`,
|
|
1042
|
+
next_action: "Open HTTP/HTTPS/WebSocket preview",
|
|
1043
|
+
source_path: path,
|
|
1044
|
+
link_label: "Open preview",
|
|
1045
|
+
request_id: request.requestId,
|
|
1046
|
+
port: request.port,
|
|
1047
|
+
pid: request.pid,
|
|
1048
|
+
command: request.command,
|
|
1049
|
+
...(request.cwd === undefined ? {} : { cwd: request.cwd }),
|
|
1050
|
+
url: path,
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
}, args.log);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async function publishForwardPortRequestedEvent(
|
|
1057
|
+
args: ChannelSessionContext,
|
|
1058
|
+
request: PendingPortForwardRequest,
|
|
1059
|
+
): Promise<void> {
|
|
1060
|
+
await pushOptional(args.kandan, args.topic, "forward_port_requested", {
|
|
1061
|
+
instanceId: args.instanceId,
|
|
1062
|
+
requestId: request.requestId,
|
|
1063
|
+
port: request.port,
|
|
1064
|
+
pid: request.pid,
|
|
1065
|
+
command: request.command,
|
|
1066
|
+
...(request.cwd === undefined ? {} : { cwd: request.cwd }),
|
|
1067
|
+
}, args.log);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function publishForwardPortResolvedEvent(
|
|
1071
|
+
args: ChannelSessionContext,
|
|
1072
|
+
request: PendingPortForwardRequest,
|
|
1073
|
+
capabilities: JsonObject | undefined,
|
|
1074
|
+
): Promise<void> {
|
|
1075
|
+
await pushOptional(args.kandan, args.topic, "forward_port_resolved", {
|
|
1076
|
+
instanceId: args.instanceId,
|
|
1077
|
+
requestId: request.requestId,
|
|
1078
|
+
port: request.port,
|
|
1079
|
+
pid: request.pid,
|
|
1080
|
+
command: request.command,
|
|
1081
|
+
...(request.cwd === undefined ? {} : { cwd: request.cwd }),
|
|
1082
|
+
decision: "approve",
|
|
1083
|
+
...(capabilities === undefined ? {} : { capabilities }),
|
|
1084
|
+
}, args.log);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function publishMessageStateForPortForwardResult(
|
|
1088
|
+
args: ChannelSessionContext,
|
|
1089
|
+
state: ChannelSessionState,
|
|
1090
|
+
request: PendingPortForwardRequest,
|
|
1091
|
+
status: "processed" | "failed",
|
|
1092
|
+
): Promise<void> {
|
|
1093
|
+
if (state.kandanThreadId === undefined) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
switch (status) {
|
|
1098
|
+
case "processed":
|
|
1099
|
+
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
|
|
1100
|
+
status: "processed",
|
|
1101
|
+
});
|
|
1102
|
+
break;
|
|
1103
|
+
case "failed":
|
|
1104
|
+
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
|
|
1105
|
+
status: "failed",
|
|
1106
|
+
reason: "port_forward_denied",
|
|
1107
|
+
});
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function parsePortForwardDecision(
|
|
1113
|
+
body: string,
|
|
1114
|
+
): { readonly decision: "approve" | "deny"; readonly requestId: string } | undefined {
|
|
1115
|
+
const match = body.trim().match(/^\/kandan\s+(approve|deny)-port-forward\s+(\S+)$/i);
|
|
1116
|
+
|
|
1117
|
+
if (match === null) {
|
|
1118
|
+
return undefined;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return {
|
|
1122
|
+
decision: match[1]?.toLocaleLowerCase() === "approve" ? "approve" : "deny",
|
|
1123
|
+
requestId: match[2] ?? "",
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
654
1127
|
async function handleKandanChatEvent(
|
|
655
1128
|
args: ChannelSessionContext,
|
|
656
1129
|
state: ChannelSessionState,
|
|
@@ -698,6 +1171,28 @@ async function handleKandanChatEvent(
|
|
|
698
1171
|
return;
|
|
699
1172
|
}
|
|
700
1173
|
|
|
1174
|
+
const portForwardDecision = parsePortForwardDecision(event.body);
|
|
1175
|
+
if (portForwardDecision !== undefined) {
|
|
1176
|
+
const result = await resolvePendingPortForwardRequest(args, state, payloadContext, {
|
|
1177
|
+
type: "resolve_port_forward_request",
|
|
1178
|
+
instanceId: args.instanceId,
|
|
1179
|
+
requestId: portForwardDecision.requestId,
|
|
1180
|
+
decision: portForwardDecision.decision,
|
|
1181
|
+
actorUserId: event.actorUserId,
|
|
1182
|
+
actorSlug: event.actorSlug,
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
if (result.ok === true) {
|
|
1186
|
+
await publishKandanMessageState(args, event, { status: "processed" });
|
|
1187
|
+
} else {
|
|
1188
|
+
await publishKandanMessageState(args, event, {
|
|
1189
|
+
status: "failed",
|
|
1190
|
+
reason: stringValue(result.error) ?? "port_forward_decision_failed",
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
701
1196
|
if (state.kandanThreadId === undefined) {
|
|
702
1197
|
if (state.rootSeq === undefined || event.replyToSeq !== state.rootSeq) {
|
|
703
1198
|
const bound = await bindUnboundHistoricalThread(args, state, event);
|
|
@@ -734,7 +1229,7 @@ async function handleKandanChatEvent(
|
|
|
734
1229
|
return;
|
|
735
1230
|
}
|
|
736
1231
|
|
|
737
|
-
state.queue
|
|
1232
|
+
enqueuePendingKandanMessage(state.queue, {
|
|
738
1233
|
seq: event.seq,
|
|
739
1234
|
actorSlug: event.actorSlug,
|
|
740
1235
|
actorUserId: event.actorUserId,
|
|
@@ -744,7 +1239,7 @@ async function handleKandanChatEvent(
|
|
|
744
1239
|
seq: event.seq,
|
|
745
1240
|
actor_slug: event.actorSlug ?? null,
|
|
746
1241
|
actor_user_id: event.actorUserId ?? null,
|
|
747
|
-
queue_depth: state.queue
|
|
1242
|
+
queue_depth: pendingKandanMessageQueueLength(state.queue),
|
|
748
1243
|
});
|
|
749
1244
|
await publishKandanMessageState(args, event, { status: "queued" });
|
|
750
1245
|
await drainKandanMessageQueue(args, state, payloadContext);
|
|
@@ -852,7 +1347,7 @@ async function drainKandanMessageQueue(
|
|
|
852
1347
|
return;
|
|
853
1348
|
}
|
|
854
1349
|
|
|
855
|
-
const next = state.queue
|
|
1350
|
+
const next = dequeuePendingKandanMessage(state.queue);
|
|
856
1351
|
|
|
857
1352
|
if (next === undefined) {
|
|
858
1353
|
return;
|
|
@@ -865,7 +1360,7 @@ async function drainKandanMessageQueue(
|
|
|
865
1360
|
actor_slug: next.actorSlug ?? null,
|
|
866
1361
|
actor_user_id: next.actorUserId ?? null,
|
|
867
1362
|
codex_thread_id: state.codexThreadId ?? null,
|
|
868
|
-
queue_depth: state.queue
|
|
1363
|
+
queue_depth: pendingKandanMessageQueueLength(state.queue),
|
|
869
1364
|
});
|
|
870
1365
|
await publishQueuedMessageState(args, state, next, {
|
|
871
1366
|
status: "processing",
|
|
@@ -928,7 +1423,7 @@ async function drainKandanMessageQueue(
|
|
|
928
1423
|
oldCodexThreadId,
|
|
929
1424
|
newCodexThreadId,
|
|
930
1425
|
);
|
|
931
|
-
state.queue
|
|
1426
|
+
requeuePendingKandanMessageFront(state.queue, next);
|
|
932
1427
|
state.turn = { status: "idle" };
|
|
933
1428
|
await drainKandanMessageQueue(args, state, payloadContext);
|
|
934
1429
|
return;
|
|
@@ -1026,7 +1521,7 @@ async function requestKandanApproval(
|
|
|
1026
1521
|
turnId: string,
|
|
1027
1522
|
payloadContext: RunnerPayloadContext,
|
|
1028
1523
|
): Promise<JsonObject> {
|
|
1029
|
-
const sourceSeq = activeQueuedSeqForTurn(state, turnId);
|
|
1524
|
+
const sourceSeq = activeQueuedSeqForTurn(state.turn, turnId);
|
|
1030
1525
|
|
|
1031
1526
|
if (sourceSeq === undefined || state.kandanThreadId === undefined) {
|
|
1032
1527
|
const message = `Codex approval request has no active Kandan source message: ${request.method}`;
|
|
@@ -1050,43 +1545,23 @@ async function requestKandanApproval(
|
|
|
1050
1545
|
});
|
|
1051
1546
|
|
|
1052
1547
|
return new Promise<JsonObject>((resolve, reject) => {
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1548
|
+
const request = {
|
|
1549
|
+
requestId: approval.requestId,
|
|
1550
|
+
sourceSeq,
|
|
1551
|
+
turnId,
|
|
1552
|
+
resolve,
|
|
1553
|
+
reject,
|
|
1554
|
+
};
|
|
1555
|
+
state.pendingApprovalRequests.set(
|
|
1556
|
+
approvalRequestKey(request.requestId, request.sourceSeq),
|
|
1557
|
+
request,
|
|
1558
|
+
);
|
|
1063
1559
|
});
|
|
1064
1560
|
}
|
|
1065
1561
|
|
|
1066
|
-
function codexApprovalMessageState(request: JsonRpcRequest): CodexApprovalMessageState {
|
|
1067
|
-
const params = objectValue(request.params) ?? {};
|
|
1068
|
-
const command = stringValue(params.command) ?? stringValue(params.cmd);
|
|
1069
|
-
const filePath =
|
|
1070
|
-
stringValue(params.path) ??
|
|
1071
|
-
stringValue(params.filePath) ??
|
|
1072
|
-
stringValue(params.file);
|
|
1073
|
-
const summary =
|
|
1074
|
-
command ??
|
|
1075
|
-
filePath ??
|
|
1076
|
-
stringValue(params.reason) ??
|
|
1077
|
-
stringValue(params.summary) ??
|
|
1078
|
-
request.method;
|
|
1079
|
-
|
|
1080
|
-
return {
|
|
1081
|
-
requestId: String(request.id),
|
|
1082
|
-
kind: request.method,
|
|
1083
|
-
summary,
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
1562
|
function rejectPendingApprovalRequests(state: ChannelSessionState, error: Error): void {
|
|
1088
|
-
const pendingApprovals = state.pendingApprovalRequests;
|
|
1089
|
-
state.pendingApprovalRequests
|
|
1563
|
+
const pendingApprovals = [...state.pendingApprovalRequests.values()];
|
|
1564
|
+
state.pendingApprovalRequests.clear();
|
|
1090
1565
|
pendingApprovals.forEach(approval => approval.reject(error));
|
|
1091
1566
|
}
|
|
1092
1567
|
|
|
@@ -1095,11 +1570,12 @@ function rejectPendingApprovalRequestsForTurn(
|
|
|
1095
1570
|
turnId: string,
|
|
1096
1571
|
error: Error,
|
|
1097
1572
|
): void {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1573
|
+
for (const [key, approval] of state.pendingApprovalRequests) {
|
|
1574
|
+
if (approval.turnId === turnId) {
|
|
1575
|
+
state.pendingApprovalRequests.delete(key);
|
|
1576
|
+
approval.reject(error);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1103
1579
|
}
|
|
1104
1580
|
|
|
1105
1581
|
async function forwardCompletedCodexTurn(
|
|
@@ -1115,24 +1591,21 @@ async function forwardCompletedCodexTurn(
|
|
|
1115
1591
|
if (
|
|
1116
1592
|
state.kandanThreadId === undefined ||
|
|
1117
1593
|
state.codexThreadId === undefined ||
|
|
1118
|
-
state.forwardedTurnIds.
|
|
1119
|
-
state.forwardingTurnIds.
|
|
1594
|
+
state.forwardedTurnIds.has(turnId) ||
|
|
1595
|
+
state.forwardingTurnIds.has(turnId) ||
|
|
1120
1596
|
!turnCanForward(state, turnId)
|
|
1121
1597
|
) {
|
|
1122
1598
|
return;
|
|
1123
1599
|
}
|
|
1124
1600
|
|
|
1125
|
-
const completingQueuedSeq =
|
|
1126
|
-
state.turn.status === "active" && state.turn.turnId === turnId
|
|
1127
|
-
? state.turn.queuedSeq
|
|
1128
|
-
: undefined;
|
|
1601
|
+
const completingQueuedSeq = completingQueuedSeqForTurn(state.turn, turnId);
|
|
1129
1602
|
const completingActiveTurn = completingQueuedSeq !== undefined;
|
|
1130
1603
|
const completingLocalTuiTurn = isLocalTuiTurn(state, turnId);
|
|
1131
1604
|
if (completingQueuedSeq !== undefined) {
|
|
1132
1605
|
state.turn = { status: "completing", turnId, queuedSeq: completingQueuedSeq };
|
|
1133
1606
|
}
|
|
1134
1607
|
await waitForPendingTuiInputMirror(state, turnId);
|
|
1135
|
-
await waitForStreamingForwardChains(state);
|
|
1608
|
+
await waitForStreamingForwardChains(args, state, payloadContext);
|
|
1136
1609
|
rememberForwardingTurnId(state, turnId);
|
|
1137
1610
|
forgetRetryableTurnId(state, turnId);
|
|
1138
1611
|
|
|
@@ -1200,7 +1673,7 @@ async function forwardCompletedCodexTurn(
|
|
|
1200
1673
|
|
|
1201
1674
|
if (
|
|
1202
1675
|
stringValue(message.structured.kind) === "codex_terminal_input" &&
|
|
1203
|
-
state.forwardedTerminalInputKeys.
|
|
1676
|
+
state.forwardedTerminalInputKeys.has(message.itemKey)
|
|
1204
1677
|
) {
|
|
1205
1678
|
continue;
|
|
1206
1679
|
}
|
|
@@ -1311,13 +1784,22 @@ async function forwardAssistantDelta(
|
|
|
1311
1784
|
params: JsonObject,
|
|
1312
1785
|
payloadContext: RunnerPayloadContext,
|
|
1313
1786
|
): Promise<void> {
|
|
1314
|
-
|
|
1787
|
+
const delta = codexAssistantDeltaFromNotification(params);
|
|
1788
|
+
|
|
1789
|
+
if (delta === undefined) {
|
|
1315
1790
|
return;
|
|
1316
1791
|
}
|
|
1317
1792
|
|
|
1318
|
-
|
|
1793
|
+
await forwardAssistantDeltaPayload(args, state, delta, payloadContext);
|
|
1794
|
+
}
|
|
1319
1795
|
|
|
1320
|
-
|
|
1796
|
+
async function forwardAssistantDeltaPayload(
|
|
1797
|
+
args: ChannelSessionContext,
|
|
1798
|
+
state: ChannelSessionState,
|
|
1799
|
+
delta: CodexAssistantDelta,
|
|
1800
|
+
payloadContext: RunnerPayloadContext,
|
|
1801
|
+
): Promise<void> {
|
|
1802
|
+
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
1321
1803
|
return;
|
|
1322
1804
|
}
|
|
1323
1805
|
|
|
@@ -1421,17 +1903,17 @@ function enqueueAssistantDelta(
|
|
|
1421
1903
|
params: JsonObject,
|
|
1422
1904
|
payloadContext: RunnerPayloadContext,
|
|
1423
1905
|
): void {
|
|
1424
|
-
const
|
|
1425
|
-
const next = previous
|
|
1426
|
-
.catch(() => undefined)
|
|
1427
|
-
.then(() => forwardAssistantDelta(args, state, params, payloadContext));
|
|
1906
|
+
const delta = codexAssistantDeltaFromNotification(params);
|
|
1428
1907
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1908
|
+
if (delta === undefined) {
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
enqueueStreamDelta(
|
|
1913
|
+
state.assistantDeltaQueue,
|
|
1914
|
+
delta,
|
|
1915
|
+
assistantDeltaQueueRuntime(args, state, payloadContext),
|
|
1916
|
+
);
|
|
1435
1917
|
}
|
|
1436
1918
|
|
|
1437
1919
|
function enqueueReasoningDelta(
|
|
@@ -1440,17 +1922,17 @@ function enqueueReasoningDelta(
|
|
|
1440
1922
|
params: JsonObject,
|
|
1441
1923
|
payloadContext: RunnerPayloadContext,
|
|
1442
1924
|
): void {
|
|
1443
|
-
const
|
|
1444
|
-
const next = previous
|
|
1445
|
-
.catch(() => undefined)
|
|
1446
|
-
.then(() => forwardReasoningDelta(args, state, params, payloadContext));
|
|
1925
|
+
const delta = codexReasoningDeltaFromNotification(params);
|
|
1447
1926
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1927
|
+
if (delta === undefined) {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
enqueueStreamDelta(
|
|
1932
|
+
state.reasoningDeltaQueue,
|
|
1933
|
+
delta,
|
|
1934
|
+
reasoningDeltaQueueRuntime(args, state, payloadContext),
|
|
1935
|
+
);
|
|
1454
1936
|
}
|
|
1455
1937
|
|
|
1456
1938
|
function enqueueCommandOutputDelta(
|
|
@@ -1459,17 +1941,17 @@ function enqueueCommandOutputDelta(
|
|
|
1459
1941
|
params: JsonObject,
|
|
1460
1942
|
payloadContext: RunnerPayloadContext,
|
|
1461
1943
|
): void {
|
|
1462
|
-
const
|
|
1463
|
-
const next = previous
|
|
1464
|
-
.catch(() => undefined)
|
|
1465
|
-
.then(() => forwardCommandOutputDelta(args, state, params, payloadContext));
|
|
1944
|
+
const delta = codexCommandOutputDeltaFromNotification(params);
|
|
1466
1945
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1946
|
+
if (delta === undefined) {
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
enqueueStreamDelta(
|
|
1951
|
+
state.commandOutputQueue,
|
|
1952
|
+
delta,
|
|
1953
|
+
commandOutputQueueRuntime(args, state, payloadContext),
|
|
1954
|
+
);
|
|
1473
1955
|
}
|
|
1474
1956
|
|
|
1475
1957
|
function enqueueFileChangeDelta(
|
|
@@ -1478,17 +1960,97 @@ function enqueueFileChangeDelta(
|
|
|
1478
1960
|
params: JsonObject,
|
|
1479
1961
|
payloadContext: RunnerPayloadContext,
|
|
1480
1962
|
): void {
|
|
1481
|
-
const
|
|
1482
|
-
const next = previous
|
|
1483
|
-
.catch(() => undefined)
|
|
1484
|
-
.then(() => forwardFileChangeDelta(args, state, params, payloadContext));
|
|
1963
|
+
const delta = codexFileChangeDeltaFromNotification(params);
|
|
1485
1964
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1965
|
+
if (delta === undefined) {
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
enqueueStreamDelta(
|
|
1970
|
+
state.fileChangeQueue,
|
|
1971
|
+
delta,
|
|
1972
|
+
fileChangeQueueRuntime(args, state, payloadContext),
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function assistantDeltaQueueRuntime(
|
|
1977
|
+
args: ChannelSessionContext,
|
|
1978
|
+
state: ChannelSessionState,
|
|
1979
|
+
payloadContext: RunnerPayloadContext,
|
|
1980
|
+
): StreamDeltaQueueRuntime<CodexAssistantDelta> {
|
|
1981
|
+
return {
|
|
1982
|
+
flushIntervalMs: streamFlushIntervalMs(args),
|
|
1983
|
+
coalesce: coalesceAssistantDeltas,
|
|
1984
|
+
firstTurnId: firstDeltaTurnId,
|
|
1985
|
+
forward: delta => forwardAssistantDeltaPayload(args, state, delta, payloadContext),
|
|
1986
|
+
logFailure: (turnId, error) => {
|
|
1987
|
+
args.log("codex.delta_forward_failed", {
|
|
1988
|
+
turn_id: turnId ?? null,
|
|
1989
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1990
|
+
});
|
|
1991
|
+
},
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
function reasoningDeltaQueueRuntime(
|
|
1996
|
+
args: ChannelSessionContext,
|
|
1997
|
+
state: ChannelSessionState,
|
|
1998
|
+
payloadContext: RunnerPayloadContext,
|
|
1999
|
+
): StreamDeltaQueueRuntime<CodexReasoningDelta> {
|
|
2000
|
+
return {
|
|
2001
|
+
flushIntervalMs: streamFlushIntervalMs(args),
|
|
2002
|
+
coalesce: coalesceReasoningDeltas,
|
|
2003
|
+
firstTurnId: firstDeltaTurnId,
|
|
2004
|
+
forward: delta => forwardReasoningDeltaPayload(args, state, delta, payloadContext),
|
|
2005
|
+
logFailure: (turnId, error) => {
|
|
2006
|
+
args.log("codex.reasoning_delta_forward_failed", {
|
|
2007
|
+
turn_id: turnId ?? null,
|
|
2008
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2009
|
+
});
|
|
2010
|
+
},
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function commandOutputQueueRuntime(
|
|
2015
|
+
args: ChannelSessionContext,
|
|
2016
|
+
state: ChannelSessionState,
|
|
2017
|
+
payloadContext: RunnerPayloadContext,
|
|
2018
|
+
): StreamDeltaQueueRuntime<CodexCommandOutputDelta> {
|
|
2019
|
+
return {
|
|
2020
|
+
flushIntervalMs: streamFlushIntervalMs(args),
|
|
2021
|
+
coalesce: coalesceCommandOutputDeltas,
|
|
2022
|
+
firstTurnId: firstDeltaTurnId,
|
|
2023
|
+
forward: delta => forwardCommandOutputDeltaPayload(args, state, delta, payloadContext),
|
|
2024
|
+
logFailure: (turnId, error) => {
|
|
2025
|
+
args.log("codex.command_output_forward_failed", {
|
|
2026
|
+
turn_id: turnId ?? null,
|
|
2027
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2028
|
+
});
|
|
2029
|
+
},
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
function fileChangeQueueRuntime(
|
|
2034
|
+
args: ChannelSessionContext,
|
|
2035
|
+
state: ChannelSessionState,
|
|
2036
|
+
payloadContext: RunnerPayloadContext,
|
|
2037
|
+
): StreamDeltaQueueRuntime<CodexFileChangeDelta> {
|
|
2038
|
+
return {
|
|
2039
|
+
flushIntervalMs: streamFlushIntervalMs(args),
|
|
2040
|
+
coalesce: coalesceFileChangeDeltas,
|
|
2041
|
+
firstTurnId: firstDeltaTurnId,
|
|
2042
|
+
forward: delta => forwardFileChangeDeltaPayload(args, state, delta, payloadContext),
|
|
2043
|
+
logFailure: (turnId, error) => {
|
|
2044
|
+
args.log("codex.file_change_forward_failed", {
|
|
2045
|
+
turn_id: turnId ?? null,
|
|
2046
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2047
|
+
});
|
|
2048
|
+
},
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
function streamFlushIntervalMs(args: ChannelSessionContext): number {
|
|
2053
|
+
return args.options.channelSession.streamFlushMs ?? defaultStreamFlushIntervalMs;
|
|
1492
2054
|
}
|
|
1493
2055
|
|
|
1494
2056
|
function enqueueTerminalInput(
|
|
@@ -1552,12 +2114,12 @@ function enqueueFileChangeCompletion(
|
|
|
1552
2114
|
state: ChannelSessionState,
|
|
1553
2115
|
turnId: string,
|
|
1554
2116
|
): void {
|
|
1555
|
-
const previous = state.
|
|
2117
|
+
const previous = state.fileChangeQueue.chain;
|
|
1556
2118
|
const next = previous
|
|
1557
2119
|
.catch(() => undefined)
|
|
1558
2120
|
.then(() => completeFileChangeOutputs(args, state, turnId));
|
|
1559
2121
|
|
|
1560
|
-
state.
|
|
2122
|
+
state.fileChangeQueue.chain = next.catch(error => {
|
|
1561
2123
|
args.log("codex.file_change_completion_failed", {
|
|
1562
2124
|
turn_id: turnId,
|
|
1563
2125
|
message: error instanceof Error ? error.message : String(error),
|
|
@@ -1571,17 +2133,26 @@ async function forwardReasoningDelta(
|
|
|
1571
2133
|
params: JsonObject,
|
|
1572
2134
|
payloadContext: RunnerPayloadContext,
|
|
1573
2135
|
): Promise<void> {
|
|
1574
|
-
|
|
2136
|
+
const delta = codexReasoningDeltaFromNotification(params);
|
|
2137
|
+
|
|
2138
|
+
if (delta === undefined) {
|
|
1575
2139
|
return;
|
|
1576
2140
|
}
|
|
1577
2141
|
|
|
1578
|
-
|
|
2142
|
+
await forwardReasoningDeltaPayload(args, state, delta, payloadContext);
|
|
2143
|
+
}
|
|
1579
2144
|
|
|
1580
|
-
|
|
2145
|
+
async function forwardReasoningDeltaPayload(
|
|
2146
|
+
args: ChannelSessionContext,
|
|
2147
|
+
state: ChannelSessionState,
|
|
2148
|
+
delta: CodexReasoningDelta,
|
|
2149
|
+
payloadContext: RunnerPayloadContext,
|
|
2150
|
+
): Promise<void> {
|
|
2151
|
+
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
1581
2152
|
return;
|
|
1582
2153
|
}
|
|
1583
2154
|
|
|
1584
|
-
const turnId = delta.turnId ?? activeTurnId(state);
|
|
2155
|
+
const turnId = delta.turnId ?? activeTurnId(state.turn);
|
|
1585
2156
|
|
|
1586
2157
|
if (turnId === undefined || turnIsFinalizingOrForwarded(state, turnId)) {
|
|
1587
2158
|
return;
|
|
@@ -1662,17 +2233,26 @@ async function forwardCommandOutputDelta(
|
|
|
1662
2233
|
params: JsonObject,
|
|
1663
2234
|
payloadContext: RunnerPayloadContext,
|
|
1664
2235
|
): Promise<void> {
|
|
1665
|
-
|
|
2236
|
+
const delta = codexCommandOutputDeltaFromNotification(params);
|
|
2237
|
+
|
|
2238
|
+
if (delta === undefined) {
|
|
1666
2239
|
return;
|
|
1667
2240
|
}
|
|
1668
2241
|
|
|
1669
|
-
|
|
2242
|
+
await forwardCommandOutputDeltaPayload(args, state, delta, payloadContext);
|
|
2243
|
+
}
|
|
1670
2244
|
|
|
1671
|
-
|
|
2245
|
+
async function forwardCommandOutputDeltaPayload(
|
|
2246
|
+
args: ChannelSessionContext,
|
|
2247
|
+
state: ChannelSessionState,
|
|
2248
|
+
delta: CodexCommandOutputDelta,
|
|
2249
|
+
payloadContext: RunnerPayloadContext,
|
|
2250
|
+
): Promise<void> {
|
|
2251
|
+
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
1672
2252
|
return;
|
|
1673
2253
|
}
|
|
1674
2254
|
|
|
1675
|
-
const turnId = delta.turnId ?? activeTurnId(state);
|
|
2255
|
+
const turnId = delta.turnId ?? activeTurnId(state.turn);
|
|
1676
2256
|
|
|
1677
2257
|
if (turnId === undefined || turnIsFinalizingOrForwarded(state, turnId)) {
|
|
1678
2258
|
return;
|
|
@@ -1763,17 +2343,26 @@ async function forwardFileChangeDelta(
|
|
|
1763
2343
|
params: JsonObject,
|
|
1764
2344
|
payloadContext: RunnerPayloadContext,
|
|
1765
2345
|
): Promise<void> {
|
|
1766
|
-
|
|
2346
|
+
const delta = codexFileChangeDeltaFromNotification(params);
|
|
2347
|
+
|
|
2348
|
+
if (delta === undefined) {
|
|
1767
2349
|
return;
|
|
1768
2350
|
}
|
|
1769
2351
|
|
|
1770
|
-
|
|
2352
|
+
await forwardFileChangeDeltaPayload(args, state, delta, payloadContext);
|
|
2353
|
+
}
|
|
1771
2354
|
|
|
1772
|
-
|
|
2355
|
+
async function forwardFileChangeDeltaPayload(
|
|
2356
|
+
args: ChannelSessionContext,
|
|
2357
|
+
state: ChannelSessionState,
|
|
2358
|
+
delta: CodexFileChangeDelta,
|
|
2359
|
+
payloadContext: RunnerPayloadContext,
|
|
2360
|
+
): Promise<void> {
|
|
2361
|
+
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
1773
2362
|
return;
|
|
1774
2363
|
}
|
|
1775
2364
|
|
|
1776
|
-
const turnId = delta.turnId ?? activeTurnId(state);
|
|
2365
|
+
const turnId = delta.turnId ?? activeTurnId(state.turn);
|
|
1777
2366
|
|
|
1778
2367
|
if (turnId === undefined || turnIsFinalizingOrForwarded(state, turnId)) {
|
|
1779
2368
|
return;
|
|
@@ -1859,11 +2448,11 @@ async function forwardTerminalInput(
|
|
|
1859
2448
|
|
|
1860
2449
|
const terminal = codexTerminalInputFromNotification(params);
|
|
1861
2450
|
|
|
1862
|
-
if (terminal === undefined || state.forwardedTerminalInputKeys.
|
|
2451
|
+
if (terminal === undefined || state.forwardedTerminalInputKeys.has(terminal.itemKey)) {
|
|
1863
2452
|
return;
|
|
1864
2453
|
}
|
|
1865
2454
|
|
|
1866
|
-
const turnId = terminal.turnId ?? activeTurnId(state);
|
|
2455
|
+
const turnId = terminal.turnId ?? activeTurnId(state.turn);
|
|
1867
2456
|
|
|
1868
2457
|
if (turnId === undefined || turnIsFinalizingOrForwarded(state, turnId)) {
|
|
1869
2458
|
return;
|
|
@@ -2023,7 +2612,7 @@ async function completeFileChangeOutputs(
|
|
|
2023
2612
|
state: ChannelSessionState,
|
|
2024
2613
|
turnId: string,
|
|
2025
2614
|
): Promise<void> {
|
|
2026
|
-
const outputs = state.streamingFileChangeOutputs.filter(
|
|
2615
|
+
const outputs = boundedCacheValues(state.streamingFileChangeOutputs).filter(
|
|
2027
2616
|
output => output.turnId === turnId,
|
|
2028
2617
|
);
|
|
2029
2618
|
|
|
@@ -2215,21 +2804,18 @@ function findMirroredTuiInputProjection(
|
|
|
2215
2804
|
state: ChannelSessionState,
|
|
2216
2805
|
logicalKey: string,
|
|
2217
2806
|
): MirroredTuiInputProjection | undefined {
|
|
2218
|
-
return state.mirroredTuiInputProjections
|
|
2219
|
-
projection => projection.logicalKey === logicalKey,
|
|
2220
|
-
);
|
|
2807
|
+
return getBoundedCacheValue(state.mirroredTuiInputProjections, logicalKey);
|
|
2221
2808
|
}
|
|
2222
2809
|
|
|
2223
2810
|
function rememberMirroredTuiInputProjection(
|
|
2224
2811
|
state: ChannelSessionState,
|
|
2225
2812
|
projection: MirroredTuiInputProjection,
|
|
2226
2813
|
): void {
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
),
|
|
2814
|
+
rememberBoundedCacheValue(
|
|
2815
|
+
state.mirroredTuiInputProjections,
|
|
2816
|
+
projection.logicalKey,
|
|
2231
2817
|
projection,
|
|
2232
|
-
|
|
2818
|
+
);
|
|
2233
2819
|
}
|
|
2234
2820
|
|
|
2235
2821
|
function rememberMirroredTuiInputItemKey(
|
|
@@ -2253,28 +2839,28 @@ function findStreamingAssistantOutput(
|
|
|
2253
2839
|
state: ChannelSessionState,
|
|
2254
2840
|
itemKey: string,
|
|
2255
2841
|
): StreamingAssistantOutput | undefined {
|
|
2256
|
-
return state.streamingAssistantOutputs
|
|
2842
|
+
return getBoundedCacheValue(state.streamingAssistantOutputs, itemKey);
|
|
2257
2843
|
}
|
|
2258
2844
|
|
|
2259
2845
|
function findStreamingReasoningOutput(
|
|
2260
2846
|
state: ChannelSessionState,
|
|
2261
2847
|
itemKey: string,
|
|
2262
2848
|
): StreamingReasoningOutput | undefined {
|
|
2263
|
-
return state.streamingReasoningOutputs
|
|
2849
|
+
return getBoundedCacheValue(state.streamingReasoningOutputs, itemKey);
|
|
2264
2850
|
}
|
|
2265
2851
|
|
|
2266
2852
|
function findStreamingCommandOutput(
|
|
2267
2853
|
state: ChannelSessionState,
|
|
2268
2854
|
itemKey: string,
|
|
2269
2855
|
): StreamingCommandOutput | undefined {
|
|
2270
|
-
return state.streamingCommandOutputs
|
|
2856
|
+
return getBoundedCacheValue(state.streamingCommandOutputs, itemKey);
|
|
2271
2857
|
}
|
|
2272
2858
|
|
|
2273
2859
|
function findStreamingFileChangeOutput(
|
|
2274
2860
|
state: ChannelSessionState,
|
|
2275
2861
|
itemKey: string,
|
|
2276
2862
|
): StreamingFileChangeOutput | undefined {
|
|
2277
|
-
return state.streamingFileChangeOutputs
|
|
2863
|
+
return getBoundedCacheValue(state.streamingFileChangeOutputs, itemKey);
|
|
2278
2864
|
}
|
|
2279
2865
|
|
|
2280
2866
|
type StreamedStructuredOutput = {
|
|
@@ -2352,7 +2938,7 @@ function resolveStreamingAssistantOutputForCompletedMessage(
|
|
|
2352
2938
|
return { status: "none" };
|
|
2353
2939
|
}
|
|
2354
2940
|
|
|
2355
|
-
const candidates = state.streamingAssistantOutputs.filter(
|
|
2941
|
+
const candidates = boundedCacheValues(state.streamingAssistantOutputs).filter(
|
|
2356
2942
|
output => output.turnId === turnId,
|
|
2357
2943
|
);
|
|
2358
2944
|
|
|
@@ -2391,8 +2977,8 @@ function turnIsFinalizingOrForwarded(
|
|
|
2391
2977
|
turnId: string,
|
|
2392
2978
|
): boolean {
|
|
2393
2979
|
return (
|
|
2394
|
-
state.forwardingTurnIds.
|
|
2395
|
-
state.forwardedTurnIds.
|
|
2980
|
+
state.forwardingTurnIds.has(turnId) ||
|
|
2981
|
+
state.forwardedTurnIds.has(turnId)
|
|
2396
2982
|
);
|
|
2397
2983
|
}
|
|
2398
2984
|
|
|
@@ -2400,114 +2986,88 @@ function rememberStreamingAssistantOutput(
|
|
|
2400
2986
|
state: ChannelSessionState,
|
|
2401
2987
|
output: StreamingAssistantOutput,
|
|
2402
2988
|
): void {
|
|
2403
|
-
state.streamingAssistantOutputs
|
|
2404
|
-
...state.streamingAssistantOutputs.filter(existing => existing.itemKey !== output.itemKey),
|
|
2405
|
-
output,
|
|
2406
|
-
].slice(-maxForwardedTurnIds);
|
|
2989
|
+
rememberBoundedCacheValue(state.streamingAssistantOutputs, output.itemKey, output);
|
|
2407
2990
|
}
|
|
2408
2991
|
|
|
2409
2992
|
function forgetStreamingAssistantOutput(
|
|
2410
2993
|
state: ChannelSessionState,
|
|
2411
2994
|
itemKey: string,
|
|
2412
2995
|
): void {
|
|
2413
|
-
state.streamingAssistantOutputs
|
|
2414
|
-
output => output.itemKey !== itemKey,
|
|
2415
|
-
);
|
|
2996
|
+
forgetBoundedCacheValue(state.streamingAssistantOutputs, itemKey);
|
|
2416
2997
|
}
|
|
2417
2998
|
|
|
2418
2999
|
function rememberStreamingReasoningOutput(
|
|
2419
3000
|
state: ChannelSessionState,
|
|
2420
3001
|
output: StreamingReasoningOutput,
|
|
2421
3002
|
): void {
|
|
2422
|
-
state.streamingReasoningOutputs
|
|
2423
|
-
...state.streamingReasoningOutputs.filter(existing => existing.itemKey !== output.itemKey),
|
|
2424
|
-
output,
|
|
2425
|
-
].slice(-maxForwardedTurnIds);
|
|
3003
|
+
rememberBoundedCacheValue(state.streamingReasoningOutputs, output.itemKey, output);
|
|
2426
3004
|
}
|
|
2427
3005
|
|
|
2428
3006
|
function forgetStreamingReasoningOutput(
|
|
2429
3007
|
state: ChannelSessionState,
|
|
2430
3008
|
itemKey: string,
|
|
2431
3009
|
): void {
|
|
2432
|
-
state.streamingReasoningOutputs
|
|
2433
|
-
output => output.itemKey !== itemKey,
|
|
2434
|
-
);
|
|
3010
|
+
forgetBoundedCacheValue(state.streamingReasoningOutputs, itemKey);
|
|
2435
3011
|
}
|
|
2436
3012
|
|
|
2437
3013
|
function rememberStreamingCommandOutput(
|
|
2438
3014
|
state: ChannelSessionState,
|
|
2439
3015
|
output: StreamingCommandOutput,
|
|
2440
3016
|
): void {
|
|
2441
|
-
state.streamingCommandOutputs
|
|
2442
|
-
...state.streamingCommandOutputs.filter(existing => existing.itemKey !== output.itemKey),
|
|
2443
|
-
output,
|
|
2444
|
-
].slice(-maxForwardedTurnIds);
|
|
3017
|
+
rememberBoundedCacheValue(state.streamingCommandOutputs, output.itemKey, output);
|
|
2445
3018
|
}
|
|
2446
3019
|
|
|
2447
3020
|
function forgetStreamingCommandOutput(
|
|
2448
3021
|
state: ChannelSessionState,
|
|
2449
3022
|
itemKey: string,
|
|
2450
3023
|
): void {
|
|
2451
|
-
state.streamingCommandOutputs
|
|
2452
|
-
output => output.itemKey !== itemKey,
|
|
2453
|
-
);
|
|
3024
|
+
forgetBoundedCacheValue(state.streamingCommandOutputs, itemKey);
|
|
2454
3025
|
}
|
|
2455
3026
|
|
|
2456
3027
|
function rememberStreamingFileChangeOutput(
|
|
2457
3028
|
state: ChannelSessionState,
|
|
2458
3029
|
output: StreamingFileChangeOutput,
|
|
2459
3030
|
): void {
|
|
2460
|
-
state.streamingFileChangeOutputs
|
|
2461
|
-
...state.streamingFileChangeOutputs.filter(existing => existing.itemKey !== output.itemKey),
|
|
2462
|
-
output,
|
|
2463
|
-
].slice(-maxForwardedTurnIds);
|
|
3031
|
+
rememberBoundedCacheValue(state.streamingFileChangeOutputs, output.itemKey, output);
|
|
2464
3032
|
}
|
|
2465
3033
|
|
|
2466
3034
|
function forgetStreamingFileChangeOutput(
|
|
2467
3035
|
state: ChannelSessionState,
|
|
2468
3036
|
itemKey: string,
|
|
2469
3037
|
): void {
|
|
2470
|
-
state.streamingFileChangeOutputs
|
|
2471
|
-
output => output.itemKey !== itemKey,
|
|
2472
|
-
);
|
|
3038
|
+
forgetBoundedCacheValue(state.streamingFileChangeOutputs, itemKey);
|
|
2473
3039
|
}
|
|
2474
3040
|
|
|
2475
3041
|
function rememberForwardedTerminalInputKey(
|
|
2476
3042
|
state: ChannelSessionState,
|
|
2477
3043
|
itemKey: string,
|
|
2478
3044
|
): void {
|
|
2479
|
-
if (state.forwardedTerminalInputKeys.
|
|
3045
|
+
if (state.forwardedTerminalInputKeys.has(itemKey)) {
|
|
2480
3046
|
return;
|
|
2481
3047
|
}
|
|
2482
3048
|
|
|
2483
|
-
state.forwardedTerminalInputKeys
|
|
2484
|
-
.slice(-maxForwardedTurnIds);
|
|
3049
|
+
rememberBoundedStringSet(state.forwardedTerminalInputKeys, itemKey, maxForwardedTurnIds);
|
|
2485
3050
|
}
|
|
2486
3051
|
|
|
2487
3052
|
function findWebSearchProgressOutput(
|
|
2488
3053
|
state: ChannelSessionState,
|
|
2489
3054
|
turnId: string,
|
|
2490
3055
|
): WebSearchProgressOutput | undefined {
|
|
2491
|
-
return state.webSearchProgressOutputs
|
|
3056
|
+
return getBoundedCacheValue(state.webSearchProgressOutputs, turnId);
|
|
2492
3057
|
}
|
|
2493
3058
|
|
|
2494
3059
|
function rememberWebSearchProgressOutput(
|
|
2495
3060
|
state: ChannelSessionState,
|
|
2496
3061
|
output: WebSearchProgressOutput,
|
|
2497
3062
|
): void {
|
|
2498
|
-
state.webSearchProgressOutputs
|
|
2499
|
-
...state.webSearchProgressOutputs.filter(existing => existing.turnId !== output.turnId),
|
|
2500
|
-
output,
|
|
2501
|
-
].slice(-maxForwardedTurnIds);
|
|
3063
|
+
rememberBoundedCacheValue(state.webSearchProgressOutputs, output.turnId, output);
|
|
2502
3064
|
}
|
|
2503
3065
|
|
|
2504
3066
|
function forgetWebSearchProgressOutput(
|
|
2505
3067
|
state: ChannelSessionState,
|
|
2506
3068
|
turnId: string,
|
|
2507
3069
|
): void {
|
|
2508
|
-
state.webSearchProgressOutputs
|
|
2509
|
-
output => output.turnId !== turnId,
|
|
2510
|
-
);
|
|
3070
|
+
forgetBoundedCacheValue(state.webSearchProgressOutputs, turnId);
|
|
2511
3071
|
}
|
|
2512
3072
|
|
|
2513
3073
|
function mergeWebSearchQueries(
|
|
@@ -2560,7 +3120,7 @@ function rememberLocalTuiTurnIfNeeded(
|
|
|
2560
3120
|
return;
|
|
2561
3121
|
}
|
|
2562
3122
|
|
|
2563
|
-
|
|
3123
|
+
rememberBoundedStringSet(state.localTuiTurnIds, turnId, maxForwardedTurnIds);
|
|
2564
3124
|
if (state.kandanThreadId !== undefined) {
|
|
2565
3125
|
startCodexTypingHeartbeat(args, state, state.kandanThreadId);
|
|
2566
3126
|
}
|
|
@@ -2568,7 +3128,7 @@ function rememberLocalTuiTurnIfNeeded(
|
|
|
2568
3128
|
}
|
|
2569
3129
|
|
|
2570
3130
|
function isLocalTuiTurn(state: ChannelSessionState, turnId: string): boolean {
|
|
2571
|
-
return state.localTuiTurnIds.
|
|
3131
|
+
return state.localTuiTurnIds.has(turnId);
|
|
2572
3132
|
}
|
|
2573
3133
|
|
|
2574
3134
|
function ensureKandanThreadForLocalTuiTurn(
|
|
@@ -2585,7 +3145,7 @@ function forgetLocalTuiTurnId(
|
|
|
2585
3145
|
state: ChannelSessionState,
|
|
2586
3146
|
turnId: string,
|
|
2587
3147
|
): void {
|
|
2588
|
-
state.localTuiTurnIds
|
|
3148
|
+
state.localTuiTurnIds.delete(turnId);
|
|
2589
3149
|
}
|
|
2590
3150
|
|
|
2591
3151
|
function rememberPendingTuiInputMirror(
|
|
@@ -2593,63 +3153,88 @@ function rememberPendingTuiInputMirror(
|
|
|
2593
3153
|
turnId: string,
|
|
2594
3154
|
promise: Promise<void>,
|
|
2595
3155
|
): void {
|
|
2596
|
-
state.pendingTuiInputMirrors
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
].slice(-maxForwardedTurnIds);
|
|
3156
|
+
state.pendingTuiInputMirrors.delete(turnId);
|
|
3157
|
+
state.pendingTuiInputMirrors.set(turnId, promise);
|
|
3158
|
+
trimBoundedMap(state.pendingTuiInputMirrors, maxForwardedTurnIds);
|
|
2600
3159
|
}
|
|
2601
3160
|
|
|
2602
3161
|
function forgetPendingTuiInputMirror(
|
|
2603
3162
|
state: ChannelSessionState,
|
|
2604
3163
|
turnId: string,
|
|
2605
3164
|
): void {
|
|
2606
|
-
state.pendingTuiInputMirrors
|
|
2607
|
-
pending => pending.turnId !== turnId,
|
|
2608
|
-
);
|
|
3165
|
+
state.pendingTuiInputMirrors.delete(turnId);
|
|
2609
3166
|
}
|
|
2610
3167
|
|
|
2611
3168
|
async function waitForPendingTuiInputMirror(
|
|
2612
3169
|
state: ChannelSessionState,
|
|
2613
3170
|
turnId: string,
|
|
2614
3171
|
): Promise<void> {
|
|
2615
|
-
const pending = state.pendingTuiInputMirrors.
|
|
2616
|
-
mirror => mirror.turnId === turnId,
|
|
2617
|
-
);
|
|
3172
|
+
const pending = state.pendingTuiInputMirrors.get(turnId);
|
|
2618
3173
|
|
|
2619
3174
|
if (pending !== undefined) {
|
|
2620
|
-
await pending
|
|
3175
|
+
await pending;
|
|
2621
3176
|
}
|
|
2622
3177
|
}
|
|
2623
3178
|
|
|
2624
3179
|
async function waitForStreamingForwardChains(
|
|
3180
|
+
args: ChannelSessionContext,
|
|
2625
3181
|
state: ChannelSessionState,
|
|
3182
|
+
payloadContext: RunnerPayloadContext,
|
|
2626
3183
|
): Promise<void> {
|
|
3184
|
+
flushPendingStreamingDeltas(args, state, payloadContext);
|
|
2627
3185
|
await Promise.all([
|
|
2628
|
-
state.
|
|
2629
|
-
state.
|
|
2630
|
-
state.
|
|
2631
|
-
state.
|
|
3186
|
+
state.assistantDeltaQueue.chain.catch(() => undefined),
|
|
3187
|
+
state.reasoningDeltaQueue.chain.catch(() => undefined),
|
|
3188
|
+
state.commandOutputQueue.chain.catch(() => undefined),
|
|
3189
|
+
state.fileChangeQueue.chain.catch(() => undefined),
|
|
2632
3190
|
state.terminalInputForwardChain.catch(() => undefined),
|
|
2633
3191
|
state.webSearchProgressForwardChain.catch(() => undefined),
|
|
2634
3192
|
]);
|
|
2635
3193
|
}
|
|
2636
3194
|
|
|
3195
|
+
function flushPendingStreamingDeltas(
|
|
3196
|
+
args: ChannelSessionContext,
|
|
3197
|
+
state: ChannelSessionState,
|
|
3198
|
+
payloadContext: RunnerPayloadContext,
|
|
3199
|
+
): void {
|
|
3200
|
+
flushStreamDeltaQueue(
|
|
3201
|
+
state.assistantDeltaQueue,
|
|
3202
|
+
assistantDeltaQueueRuntime(args, state, payloadContext),
|
|
3203
|
+
);
|
|
3204
|
+
flushStreamDeltaQueue(
|
|
3205
|
+
state.reasoningDeltaQueue,
|
|
3206
|
+
reasoningDeltaQueueRuntime(args, state, payloadContext),
|
|
3207
|
+
);
|
|
3208
|
+
flushStreamDeltaQueue(
|
|
3209
|
+
state.commandOutputQueue,
|
|
3210
|
+
commandOutputQueueRuntime(args, state, payloadContext),
|
|
3211
|
+
);
|
|
3212
|
+
flushStreamDeltaQueue(
|
|
3213
|
+
state.fileChangeQueue,
|
|
3214
|
+
fileChangeQueueRuntime(args, state, payloadContext),
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
function clearPendingStreamFlushTimers(state: ChannelSessionState): void {
|
|
3219
|
+
clearStreamDeltaFlushTimer(state.assistantDeltaQueue);
|
|
3220
|
+
clearStreamDeltaFlushTimer(state.reasoningDeltaQueue);
|
|
3221
|
+
clearStreamDeltaFlushTimer(state.commandOutputQueue);
|
|
3222
|
+
clearStreamDeltaFlushTimer(state.fileChangeQueue);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
2637
3225
|
function rememberTurnReplyTarget(
|
|
2638
3226
|
state: ChannelSessionState,
|
|
2639
3227
|
turnId: string,
|
|
2640
3228
|
replyToSeq: number,
|
|
2641
3229
|
): void {
|
|
2642
|
-
state.turnReplyTargets
|
|
2643
|
-
...state.turnReplyTargets.filter(target => target.turnId !== turnId),
|
|
2644
|
-
{ turnId, replyToSeq },
|
|
2645
|
-
].slice(-maxForwardedTurnIds);
|
|
3230
|
+
rememberBoundedCacheValue(state.turnReplyTargets, turnId, { turnId, replyToSeq });
|
|
2646
3231
|
}
|
|
2647
3232
|
|
|
2648
3233
|
function sourceMessageSeqForTurn(
|
|
2649
3234
|
state: ChannelSessionState,
|
|
2650
3235
|
turnId: string,
|
|
2651
3236
|
): number | undefined {
|
|
2652
|
-
return state.turnReplyTargets
|
|
3237
|
+
return getBoundedCacheValue(state.turnReplyTargets, turnId)?.replyToSeq;
|
|
2653
3238
|
}
|
|
2654
3239
|
|
|
2655
3240
|
function fileChangePaths(structured: JsonObject): string[] {
|
|
@@ -2710,7 +3295,7 @@ function isRecoverableCodexThreadError(error: unknown): boolean {
|
|
|
2710
3295
|
}
|
|
2711
3296
|
|
|
2712
3297
|
function turnCanForward(state: ChannelSessionState, turnId: string): boolean {
|
|
2713
|
-
if (state.retryableTurnIds.
|
|
3298
|
+
if (state.retryableTurnIds.has(turnId)) {
|
|
2714
3299
|
return true;
|
|
2715
3300
|
}
|
|
2716
3301
|
|
|
@@ -2718,26 +3303,18 @@ function turnCanForward(state: ChannelSessionState, turnId: string): boolean {
|
|
|
2718
3303
|
return true;
|
|
2719
3304
|
}
|
|
2720
3305
|
|
|
2721
|
-
|
|
2722
|
-
case "active":
|
|
2723
|
-
return state.turn.turnId === turnId;
|
|
2724
|
-
case "completing":
|
|
2725
|
-
return state.turn.turnId === turnId;
|
|
2726
|
-
case "idle":
|
|
2727
|
-
case "starting":
|
|
2728
|
-
return false;
|
|
2729
|
-
}
|
|
3306
|
+
return turnCanForwardByState(state.turn, turnId);
|
|
2730
3307
|
}
|
|
2731
3308
|
|
|
2732
3309
|
function rememberForwardedTurnId(
|
|
2733
3310
|
state: ChannelSessionState,
|
|
2734
3311
|
turnId: string,
|
|
2735
3312
|
): void {
|
|
2736
|
-
if (state.forwardedTurnIds.
|
|
3313
|
+
if (state.forwardedTurnIds.has(turnId)) {
|
|
2737
3314
|
return;
|
|
2738
3315
|
}
|
|
2739
3316
|
|
|
2740
|
-
|
|
3317
|
+
rememberBoundedStringSet(state.forwardedTurnIds, turnId, maxForwardedTurnIds);
|
|
2741
3318
|
forgetRetryableTurnId(state, turnId);
|
|
2742
3319
|
}
|
|
2743
3320
|
|
|
@@ -2745,39 +3322,54 @@ function rememberForwardingTurnId(
|
|
|
2745
3322
|
state: ChannelSessionState,
|
|
2746
3323
|
turnId: string,
|
|
2747
3324
|
): void {
|
|
2748
|
-
|
|
3325
|
+
rememberBoundedStringSet(state.forwardingTurnIds, turnId, maxForwardedTurnIds);
|
|
2749
3326
|
}
|
|
2750
3327
|
|
|
2751
3328
|
function forgetForwardingTurnId(
|
|
2752
3329
|
state: ChannelSessionState,
|
|
2753
3330
|
turnId: string,
|
|
2754
3331
|
): void {
|
|
2755
|
-
state.forwardingTurnIds
|
|
3332
|
+
state.forwardingTurnIds.delete(turnId);
|
|
2756
3333
|
}
|
|
2757
3334
|
|
|
2758
3335
|
function rememberRetryableTurnId(
|
|
2759
3336
|
state: ChannelSessionState,
|
|
2760
3337
|
turnId: string,
|
|
2761
3338
|
): void {
|
|
2762
|
-
|
|
3339
|
+
rememberBoundedStringSet(state.retryableTurnIds, turnId, maxForwardedTurnIds);
|
|
2763
3340
|
}
|
|
2764
3341
|
|
|
2765
3342
|
function forgetRetryableTurnId(
|
|
2766
3343
|
state: ChannelSessionState,
|
|
2767
3344
|
turnId: string,
|
|
2768
3345
|
): void {
|
|
2769
|
-
state.retryableTurnIds
|
|
3346
|
+
state.retryableTurnIds.delete(turnId);
|
|
2770
3347
|
}
|
|
2771
3348
|
|
|
2772
|
-
function
|
|
2773
|
-
values
|
|
2774
|
-
|
|
2775
|
-
)
|
|
2776
|
-
|
|
2777
|
-
|
|
3349
|
+
function rememberBoundedStringSet(values: Set<string>, value: string, maxSize: number): void {
|
|
3350
|
+
values.delete(value);
|
|
3351
|
+
values.add(value);
|
|
3352
|
+
trimBoundedSet(values, maxSize);
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
function trimBoundedSet(values: Set<string>, maxSize: number): void {
|
|
3356
|
+
while (values.size > maxSize) {
|
|
3357
|
+
const oldest = values.values().next().value;
|
|
3358
|
+
if (oldest === undefined) {
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
values.delete(oldest);
|
|
2778
3362
|
}
|
|
3363
|
+
}
|
|
2779
3364
|
|
|
2780
|
-
|
|
3365
|
+
function trimBoundedMap<V>(values: Map<string, V>, maxSize: number): void {
|
|
3366
|
+
while (values.size > maxSize) {
|
|
3367
|
+
const oldest = values.keys().next().value;
|
|
3368
|
+
if (oldest === undefined) {
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
values.delete(oldest);
|
|
3372
|
+
}
|
|
2781
3373
|
}
|
|
2782
3374
|
|
|
2783
3375
|
async function stopCodexTyping(
|
|
@@ -2852,24 +3444,6 @@ function stopCodexTypingHeartbeat(state: ChannelSessionState): void {
|
|
|
2852
3444
|
}
|
|
2853
3445
|
}
|
|
2854
3446
|
|
|
2855
|
-
type LocalCodexProcessingReason =
|
|
2856
|
-
| "starting turn"
|
|
2857
|
-
| "streaming response"
|
|
2858
|
-
| "running terminal command"
|
|
2859
|
-
| "interrupt requested"
|
|
2860
|
-
| "awaiting approval";
|
|
2861
|
-
|
|
2862
|
-
type LocalCodexMessageState =
|
|
2863
|
-
| { readonly status: "queued" }
|
|
2864
|
-
| { readonly status: "processed" }
|
|
2865
|
-
| {
|
|
2866
|
-
readonly status: "processing";
|
|
2867
|
-
readonly reason: LocalCodexProcessingReason;
|
|
2868
|
-
readonly approval?: CodexApprovalMessageState | undefined;
|
|
2869
|
-
}
|
|
2870
|
-
| { readonly status: "ignored"; readonly reason: string }
|
|
2871
|
-
| { readonly status: "failed"; readonly reason: string };
|
|
2872
|
-
|
|
2873
3447
|
async function publishKandanMessageState(
|
|
2874
3448
|
args: ChannelSessionContext,
|
|
2875
3449
|
event: KandanChatEvent,
|
|
@@ -2930,6 +3504,18 @@ async function publishMessageState(
|
|
|
2930
3504
|
approval_request_id: state.approval.requestId,
|
|
2931
3505
|
approval_kind: state.approval.kind,
|
|
2932
3506
|
approval_summary: state.approval.summary,
|
|
3507
|
+
...(state.approval.reason === undefined
|
|
3508
|
+
? {}
|
|
3509
|
+
: { approval_reason: state.approval.reason }),
|
|
3510
|
+
...(state.approval.choices === undefined
|
|
3511
|
+
? {}
|
|
3512
|
+
: { approval_choices: state.approval.choices }),
|
|
3513
|
+
...(state.approval.allowedActorSlug === undefined
|
|
3514
|
+
? {}
|
|
3515
|
+
: { approval_allowed_actor_slug: state.approval.allowedActorSlug }),
|
|
3516
|
+
...(state.approval.allowedActorUserId === undefined
|
|
3517
|
+
? {}
|
|
3518
|
+
: { approval_allowed_actor_user_id: state.approval.allowedActorUserId }),
|
|
2933
3519
|
}
|
|
2934
3520
|
: {}),
|
|
2935
3521
|
...(actorSlug === undefined ? {} : { actor_slug: actorSlug }),
|
|
@@ -2939,51 +3525,6 @@ async function publishMessageState(
|
|
|
2939
3525
|
await pushOptional(args.kandan, args.topic, "message_state", payload, args.log);
|
|
2940
3526
|
}
|
|
2941
3527
|
|
|
2942
|
-
function processingReasonForCodexNotification(
|
|
2943
|
-
method: string,
|
|
2944
|
-
params: JsonObject,
|
|
2945
|
-
): Exclude<LocalCodexProcessingReason, "awaiting approval"> | undefined {
|
|
2946
|
-
if (method === "item/agentMessage/delta" || method === "item/reasoning/textDelta") {
|
|
2947
|
-
return "streaming response";
|
|
2948
|
-
}
|
|
2949
|
-
|
|
2950
|
-
if (
|
|
2951
|
-
method.startsWith("item/commandExecution/") ||
|
|
2952
|
-
method === "command/exec/outputDelta"
|
|
2953
|
-
) {
|
|
2954
|
-
return "running terminal command";
|
|
2955
|
-
}
|
|
2956
|
-
|
|
2957
|
-
const item = objectValue(params.item) ?? params;
|
|
2958
|
-
const itemType = stringValue(item.type);
|
|
2959
|
-
|
|
2960
|
-
switch (itemType) {
|
|
2961
|
-
case "commandExecution":
|
|
2962
|
-
case "terminalInput":
|
|
2963
|
-
return "running terminal command";
|
|
2964
|
-
case "agentMessage":
|
|
2965
|
-
case "reasoning":
|
|
2966
|
-
case "fileChange":
|
|
2967
|
-
case "web_search_call":
|
|
2968
|
-
case "webSearchCall":
|
|
2969
|
-
case "webSearch":
|
|
2970
|
-
return "streaming response";
|
|
2971
|
-
default:
|
|
2972
|
-
return undefined;
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
|
|
2976
|
-
function activeTurnId(state: ChannelSessionState): string | undefined {
|
|
2977
|
-
switch (state.turn.status) {
|
|
2978
|
-
case "active":
|
|
2979
|
-
case "completing":
|
|
2980
|
-
return state.turn.turnId;
|
|
2981
|
-
case "idle":
|
|
2982
|
-
case "starting":
|
|
2983
|
-
return undefined;
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
|
|
2987
3528
|
function abortReason(params: JsonObject): string {
|
|
2988
3529
|
return (
|
|
2989
3530
|
stringValue(params.reason) ??
|
|
@@ -3001,7 +3542,7 @@ async function failActiveCodexTurn(
|
|
|
3001
3542
|
payloadContext: RunnerPayloadContext,
|
|
3002
3543
|
): Promise<void> {
|
|
3003
3544
|
rejectPendingApprovalRequestsForTurn(state, turnId, new Error(reason));
|
|
3004
|
-
const seq = activeQueuedSeqForTurn(state, turnId);
|
|
3545
|
+
const seq = activeQueuedSeqForTurn(state.turn, turnId);
|
|
3005
3546
|
|
|
3006
3547
|
if (seq !== undefined && state.kandanThreadId !== undefined) {
|
|
3007
3548
|
await publishMessageState(args, state.kandanThreadId, seq, {
|
|
@@ -3014,10 +3555,7 @@ async function failActiveCodexTurn(
|
|
|
3014
3555
|
forgetLocalTuiTurnId(state, turnId);
|
|
3015
3556
|
rememberForwardedTurnId(state, turnId);
|
|
3016
3557
|
|
|
3017
|
-
if (
|
|
3018
|
-
state.turn.status !== "idle" &&
|
|
3019
|
-
(state.turn.status === "starting" || state.turn.turnId === turnId)
|
|
3020
|
-
) {
|
|
3558
|
+
if (shouldClearTurnAfterFailure(state.turn, turnId)) {
|
|
3021
3559
|
state.turn = { status: "idle" };
|
|
3022
3560
|
}
|
|
3023
3561
|
|
|
@@ -3025,27 +3563,13 @@ async function failActiveCodexTurn(
|
|
|
3025
3563
|
await drainKandanMessageQueue(args, state, payloadContext);
|
|
3026
3564
|
}
|
|
3027
3565
|
|
|
3028
|
-
function activeQueuedSeqForTurn(
|
|
3029
|
-
state: ChannelSessionState,
|
|
3030
|
-
turnId: string,
|
|
3031
|
-
): number | undefined {
|
|
3032
|
-
switch (state.turn.status) {
|
|
3033
|
-
case "active":
|
|
3034
|
-
case "completing":
|
|
3035
|
-
return state.turn.turnId === turnId ? state.turn.queuedSeq : undefined;
|
|
3036
|
-
case "idle":
|
|
3037
|
-
case "starting":
|
|
3038
|
-
return undefined;
|
|
3039
|
-
}
|
|
3040
|
-
}
|
|
3041
|
-
|
|
3042
3566
|
async function refreshActiveProcessingState(
|
|
3043
3567
|
args: ChannelSessionContext,
|
|
3044
3568
|
state: ChannelSessionState,
|
|
3045
3569
|
turnId: string,
|
|
3046
3570
|
reason: Exclude<LocalCodexProcessingReason, "awaiting approval">,
|
|
3047
3571
|
): Promise<void> {
|
|
3048
|
-
const seq = activeQueuedSeqForTurn(state, turnId);
|
|
3572
|
+
const seq = activeQueuedSeqForTurn(state.turn, turnId);
|
|
3049
3573
|
|
|
3050
3574
|
if (seq === undefined || state.kandanThreadId === undefined) {
|
|
3051
3575
|
return;
|
|
@@ -3092,19 +3616,6 @@ function clearActiveProcessingState(state: ChannelSessionState, seq: number): vo
|
|
|
3092
3616
|
}
|
|
3093
3617
|
}
|
|
3094
3618
|
|
|
3095
|
-
function processingMessageStateFromActive(
|
|
3096
|
-
state: ActiveProcessingState,
|
|
3097
|
-
): Extract<LocalCodexMessageState, { readonly status: "processing" }> {
|
|
3098
|
-
switch (state.reason) {
|
|
3099
|
-
case "awaiting approval":
|
|
3100
|
-
return { status: "processing", reason: state.reason, approval: state.approval };
|
|
3101
|
-
case "starting turn":
|
|
3102
|
-
case "streaming response":
|
|
3103
|
-
case "running terminal command":
|
|
3104
|
-
case "interrupt requested":
|
|
3105
|
-
return { status: "processing", reason: state.reason };
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
3619
|
|
|
3109
3620
|
function extractThreadIdFromResponse(response: JsonRpcResponse): string {
|
|
3110
3621
|
if ("error" in response) {
|
|
@@ -3152,63 +3663,6 @@ async function startCodexThread(
|
|
|
3152
3663
|
return extractThreadIdFromResponse(start);
|
|
3153
3664
|
}
|
|
3154
3665
|
|
|
3155
|
-
function codexThreadRuntimeOverrides(options: ChannelSessionRunnerOptions): JsonObject {
|
|
3156
|
-
const session = options.channelSession;
|
|
3157
|
-
|
|
3158
|
-
return {
|
|
3159
|
-
...(session.model === undefined ? {} : { model: session.model }),
|
|
3160
|
-
...(session.reasoningEffort === undefined
|
|
3161
|
-
? {}
|
|
3162
|
-
: { reasoningEffort: session.reasoningEffort }),
|
|
3163
|
-
...(options.fast === true ? { serviceTier: "fast" } : {}),
|
|
3164
|
-
...(session.approvalPolicy === undefined
|
|
3165
|
-
? {}
|
|
3166
|
-
: { approvalPolicy: session.approvalPolicy }),
|
|
3167
|
-
...(session.sandbox === undefined ? {} : { sandbox: session.sandbox }),
|
|
3168
|
-
};
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
|
-
function codexTurnRuntimeOverrides(options: ChannelSessionRunnerOptions): JsonObject {
|
|
3172
|
-
const session = options.channelSession;
|
|
3173
|
-
|
|
3174
|
-
return {
|
|
3175
|
-
cwd: options.cwd,
|
|
3176
|
-
...(session.model === undefined ? {} : { model: session.model }),
|
|
3177
|
-
...(session.reasoningEffort === undefined ? {} : { effort: session.reasoningEffort }),
|
|
3178
|
-
...(options.fast === true ? { serviceTier: "fast" } : {}),
|
|
3179
|
-
...(session.approvalPolicy === undefined
|
|
3180
|
-
? {}
|
|
3181
|
-
: { approvalPolicy: session.approvalPolicy }),
|
|
3182
|
-
...(session.sandbox === undefined
|
|
3183
|
-
? {}
|
|
3184
|
-
: { sandboxPolicy: codexSandboxPolicy(session.sandbox, options.cwd) }),
|
|
3185
|
-
};
|
|
3186
|
-
}
|
|
3187
|
-
|
|
3188
|
-
function codexSandboxPolicy(sandbox: string, cwd: string): JsonObject {
|
|
3189
|
-
switch (sandbox) {
|
|
3190
|
-
case "danger-full-access":
|
|
3191
|
-
return { type: "dangerFullAccess" };
|
|
3192
|
-
case "read-only":
|
|
3193
|
-
return {
|
|
3194
|
-
type: "readOnly",
|
|
3195
|
-
access: { type: "fullAccess" },
|
|
3196
|
-
networkAccess: false,
|
|
3197
|
-
};
|
|
3198
|
-
case "workspace-write":
|
|
3199
|
-
return {
|
|
3200
|
-
type: "workspaceWrite",
|
|
3201
|
-
writableRoots: [cwd],
|
|
3202
|
-
readOnlyAccess: { type: "fullAccess" },
|
|
3203
|
-
networkAccess: false,
|
|
3204
|
-
excludeTmpdirEnvVar: false,
|
|
3205
|
-
excludeSlashTmp: false,
|
|
3206
|
-
};
|
|
3207
|
-
default:
|
|
3208
|
-
throw new Error(`unsupported Codex sandbox mode: ${sandbox}`);
|
|
3209
|
-
}
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
3666
|
async function pushOk(
|
|
3213
3667
|
kandan: PhoenixClient,
|
|
3214
3668
|
topic: string,
|