@linzumi/cli 0.0.42-beta → 0.0.44-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 +1 -1
- package/dist/index.js +417 -18
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -2461,6 +2461,8 @@ function shellCommandTokens(command) {
|
|
|
2461
2461
|
var codexTypingHeartbeatMs = 5000;
|
|
2462
2462
|
var defaultStreamFlushIntervalMs = 150;
|
|
2463
2463
|
var maxForwardedTurnIds = 64;
|
|
2464
|
+
var maxClaimedKandanMessageKeys = 1024;
|
|
2465
|
+
var claimedKandanMessageKeys = new Set;
|
|
2464
2466
|
async function attachChannelSession(args) {
|
|
2465
2467
|
const session = args.options.channelSession;
|
|
2466
2468
|
const chatTopic = `chat:${session.workspaceSlug}:${session.channelSlug}`;
|
|
@@ -2609,6 +2611,7 @@ async function attachChannelSession(args) {
|
|
|
2609
2611
|
clearPendingStreamFlushTimers(state);
|
|
2610
2612
|
rejectPendingApprovalRequests(state, new Error("runner closed"));
|
|
2611
2613
|
await stopCodexTyping(args, state);
|
|
2614
|
+
releaseKandanMessageClaims(state);
|
|
2612
2615
|
}
|
|
2613
2616
|
};
|
|
2614
2617
|
}
|
|
@@ -2650,6 +2653,7 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
|
|
|
2650
2653
|
forwardedTurnIds: new Set,
|
|
2651
2654
|
forwardingTurnIds: new Set,
|
|
2652
2655
|
retryableTurnIds: new Set,
|
|
2656
|
+
claimedKandanMessageKeys: new Set,
|
|
2653
2657
|
localTuiTurnIds: new Set,
|
|
2654
2658
|
mirroredTuiInputProjections: createBoundedCache(maxForwardedTurnIds),
|
|
2655
2659
|
pendingTuiInputMirrors: new Map,
|
|
@@ -2667,6 +2671,7 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
|
|
|
2667
2671
|
pendingApprovalRequests: new Map,
|
|
2668
2672
|
approvalPromptChain: Promise.resolve(),
|
|
2669
2673
|
pendingPortForwardRequests: new Map,
|
|
2674
|
+
portForwardPreviousProcessingStates: new Map,
|
|
2670
2675
|
queuedPortForwardCandidates: new Map,
|
|
2671
2676
|
approvedForwardPorts: new Set,
|
|
2672
2677
|
approvedForwardTargets: new Map,
|
|
@@ -2991,6 +2996,9 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
2991
2996
|
state.pendingPortForwardRequests.delete(control.requestId);
|
|
2992
2997
|
if (control.decision === "deny") {
|
|
2993
2998
|
state.dismissedForwardTargets.set(request.port, approvedTargetFromRequest(request));
|
|
2999
|
+
await publishForwardPortResolvedEvent(args, request, {
|
|
3000
|
+
decision: "deny"
|
|
3001
|
+
});
|
|
2994
3002
|
await publishMessageStateForPortForwardResult(args, state, request, "failed");
|
|
2995
3003
|
args.log("port_forward.request_denied", {
|
|
2996
3004
|
request_id: control.requestId,
|
|
@@ -3010,7 +3018,10 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
3010
3018
|
processName: processIdentity?.appName ?? null,
|
|
3011
3019
|
processIconKey: processIdentity?.iconKey ?? null
|
|
3012
3020
|
});
|
|
3013
|
-
await publishForwardPortResolvedEvent(args, request,
|
|
3021
|
+
await publishForwardPortResolvedEvent(args, request, {
|
|
3022
|
+
decision: "approve",
|
|
3023
|
+
capabilities
|
|
3024
|
+
});
|
|
3014
3025
|
await publishMessageStateForPortForwardResult(args, state, request, "processed");
|
|
3015
3026
|
await publishPortForwardReadyMessage(args, state, payloadContext, request);
|
|
3016
3027
|
args.log("port_forward.request_approved", {
|
|
@@ -3095,12 +3106,14 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
3095
3106
|
candidate
|
|
3096
3107
|
});
|
|
3097
3108
|
state.pendingPortForwardRequests.set(requestId, request);
|
|
3109
|
+
if (state.activeProcessingState !== undefined) {
|
|
3110
|
+
state.portForwardPreviousProcessingStates.set(requestId, state.activeProcessingState);
|
|
3111
|
+
}
|
|
3098
3112
|
const processIdentity = guessCanonicalProcessFromCommand(candidate.command);
|
|
3099
3113
|
const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
|
|
3100
3114
|
const processName = processIdentity?.appName ?? label;
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
status: "processing",
|
|
3115
|
+
state.activeProcessingState = {
|
|
3116
|
+
seq: sourceSeq,
|
|
3104
3117
|
reason: "awaiting approval",
|
|
3105
3118
|
approval: {
|
|
3106
3119
|
requestId,
|
|
@@ -3129,7 +3142,13 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
3129
3142
|
allowedActorSlug: args.options.channelSession.listenUser,
|
|
3130
3143
|
allowedActorUserId: args.options.channelSession.listenUser.toLowerCase() === (payloadContext.runnerIdentity.actorUsername ?? "").toLowerCase() ? payloadContext.runnerIdentity.actorUserId : undefined
|
|
3131
3144
|
}
|
|
3145
|
+
};
|
|
3146
|
+
await publishMessageState(args, state.kandanThreadId, sourceSeq, {
|
|
3147
|
+
status: "processing",
|
|
3148
|
+
reason: "awaiting approval",
|
|
3149
|
+
approval: state.activeProcessingState.approval
|
|
3132
3150
|
});
|
|
3151
|
+
await publishForwardPortRequestedEvent(args, state, request, processIdentity);
|
|
3133
3152
|
args.log("port_forward.request_pending", {
|
|
3134
3153
|
request_id: requestId,
|
|
3135
3154
|
port: candidate.port,
|
|
@@ -3170,6 +3189,10 @@ async function expireLostPendingPortForwardRequest(args, state, payloadContext,
|
|
|
3170
3189
|
return;
|
|
3171
3190
|
}
|
|
3172
3191
|
state.pendingPortForwardRequests.delete(request.requestId);
|
|
3192
|
+
await publishForwardPortResolvedEvent(args, request, {
|
|
3193
|
+
decision: "expired",
|
|
3194
|
+
reason: "listener_exited"
|
|
3195
|
+
});
|
|
3173
3196
|
await publishMessageStateForPortForwardResult(args, state, request, "failed", "port_forward_listener_exited");
|
|
3174
3197
|
args.log("port_forward.pending_request_expired", {
|
|
3175
3198
|
request_id: request.requestId,
|
|
@@ -3216,17 +3239,23 @@ async function publishPortForwardReadyMessage(args, state, payloadContext, reque
|
|
|
3216
3239
|
}
|
|
3217
3240
|
}, args.log);
|
|
3218
3241
|
}
|
|
3219
|
-
async function publishForwardPortRequestedEvent(args, request) {
|
|
3242
|
+
async function publishForwardPortRequestedEvent(args, state, request, processIdentity) {
|
|
3220
3243
|
await pushOptional(args.kandan, args.topic, "forward_port_requested", {
|
|
3221
3244
|
instanceId: args.instanceId,
|
|
3222
3245
|
requestId: request.requestId,
|
|
3246
|
+
sourceSeq: request.sourceSeq,
|
|
3223
3247
|
port: request.port,
|
|
3224
3248
|
pid: request.pid,
|
|
3225
3249
|
command: request.command,
|
|
3250
|
+
codexThreadId: state.codexThreadId ?? null,
|
|
3251
|
+
kandanThreadId: state.kandanThreadId ?? null,
|
|
3252
|
+
channelSlug: args.options.channelSession.channelSlug ?? null,
|
|
3253
|
+
...processIdentity?.appName === undefined ? {} : { processName: processIdentity.appName },
|
|
3254
|
+
...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey },
|
|
3226
3255
|
...request.cwd === undefined ? {} : { cwd: request.cwd }
|
|
3227
3256
|
}, args.log);
|
|
3228
3257
|
}
|
|
3229
|
-
async function publishForwardPortResolvedEvent(args, request,
|
|
3258
|
+
async function publishForwardPortResolvedEvent(args, request, result) {
|
|
3230
3259
|
await pushOptional(args.kandan, args.topic, "forward_port_resolved", {
|
|
3231
3260
|
instanceId: args.instanceId,
|
|
3232
3261
|
requestId: request.requestId,
|
|
@@ -3234,18 +3263,52 @@ async function publishForwardPortResolvedEvent(args, request, capabilities) {
|
|
|
3234
3263
|
pid: request.pid,
|
|
3235
3264
|
command: request.command,
|
|
3236
3265
|
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
3237
|
-
|
|
3238
|
-
|
|
3266
|
+
sourceSeq: request.sourceSeq,
|
|
3267
|
+
decision: result.decision,
|
|
3268
|
+
...result.reason === undefined ? {} : { reason: result.reason },
|
|
3269
|
+
...result.capabilities === undefined ? {} : { capabilities: result.capabilities }
|
|
3239
3270
|
}, args.log);
|
|
3240
3271
|
}
|
|
3241
3272
|
async function publishMessageStateForPortForwardResult(args, state, request, status, failedReason = "port_forward_denied") {
|
|
3242
3273
|
if (state.kandanThreadId === undefined) {
|
|
3243
3274
|
return;
|
|
3244
3275
|
}
|
|
3276
|
+
const previousProcessingState = state.portForwardPreviousProcessingStates.get(request.requestId);
|
|
3277
|
+
state.portForwardPreviousProcessingStates.delete(request.requestId);
|
|
3245
3278
|
const activeProcessingState = state.activeProcessingState;
|
|
3246
3279
|
if (activeProcessingState !== undefined && activeProcessingState.seq === request.sourceSeq) {
|
|
3247
|
-
|
|
3248
|
-
|
|
3280
|
+
const resolvingActiveApproval = activeProcessingState.reason === "awaiting approval" && activeProcessingState.approval.requestId === request.requestId;
|
|
3281
|
+
if (resolvingActiveApproval) {
|
|
3282
|
+
state.activeProcessingState = undefined;
|
|
3283
|
+
if (previousProcessingState !== undefined && previousProcessingState.seq === request.sourceSeq) {
|
|
3284
|
+
state.activeProcessingState = previousProcessingState;
|
|
3285
|
+
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(previousProcessingState), undefined, undefined, state.codexThreadId);
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
switch (state.turn.status) {
|
|
3289
|
+
case "active":
|
|
3290
|
+
case "completing":
|
|
3291
|
+
if (state.turn.queuedSeq === request.sourceSeq) {
|
|
3292
|
+
state.activeProcessingState = {
|
|
3293
|
+
seq: request.sourceSeq,
|
|
3294
|
+
reason: "running terminal command"
|
|
3295
|
+
};
|
|
3296
|
+
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
|
|
3297
|
+
status: "processing",
|
|
3298
|
+
reason: "running terminal command"
|
|
3299
|
+
});
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
break;
|
|
3303
|
+
case "idle":
|
|
3304
|
+
case "starting":
|
|
3305
|
+
break;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
if (!resolvingActiveApproval) {
|
|
3309
|
+
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3249
3312
|
}
|
|
3250
3313
|
switch (status) {
|
|
3251
3314
|
case "processed":
|
|
@@ -3376,6 +3439,15 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
|
|
|
3376
3439
|
});
|
|
3377
3440
|
return;
|
|
3378
3441
|
}
|
|
3442
|
+
if (!claimKandanMessage(args, state, event)) {
|
|
3443
|
+
args.log("kandan.message_ignored", {
|
|
3444
|
+
seq: event.seq,
|
|
3445
|
+
actor_slug: event.actorSlug ?? null,
|
|
3446
|
+
actor_user_id: event.actorUserId ?? null,
|
|
3447
|
+
reason: "duplicate_message_claim"
|
|
3448
|
+
});
|
|
3449
|
+
return;
|
|
3450
|
+
}
|
|
3379
3451
|
enqueuePendingKandanMessage(state.queue, {
|
|
3380
3452
|
seq: event.seq,
|
|
3381
3453
|
actorSlug: event.actorSlug,
|
|
@@ -3392,6 +3464,44 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
|
|
|
3392
3464
|
await publishKandanMessageState(args, event, { status: "queued" });
|
|
3393
3465
|
await drainKandanMessageQueue(args, state, payloadContext);
|
|
3394
3466
|
}
|
|
3467
|
+
function kandanMessageClaimKey(args, threadId, seq) {
|
|
3468
|
+
const session = args.options.channelSession;
|
|
3469
|
+
return [
|
|
3470
|
+
args.options.runnerId,
|
|
3471
|
+
args.instanceId,
|
|
3472
|
+
session.workspaceSlug,
|
|
3473
|
+
session.channelSlug,
|
|
3474
|
+
threadId,
|
|
3475
|
+
String(seq)
|
|
3476
|
+
].join(":");
|
|
3477
|
+
}
|
|
3478
|
+
function claimKandanMessage(args, state, event) {
|
|
3479
|
+
const threadId = event.threadId;
|
|
3480
|
+
if (threadId === undefined) {
|
|
3481
|
+
return true;
|
|
3482
|
+
}
|
|
3483
|
+
const key = kandanMessageClaimKey(args, threadId, event.seq);
|
|
3484
|
+
if (claimedKandanMessageKeys.has(key)) {
|
|
3485
|
+
return false;
|
|
3486
|
+
}
|
|
3487
|
+
claimedKandanMessageKeys.add(key);
|
|
3488
|
+
state.claimedKandanMessageKeys.add(key);
|
|
3489
|
+
if (state.claimedKandanMessageKeys.size > maxClaimedKandanMessageKeys) {
|
|
3490
|
+
const overflow = state.claimedKandanMessageKeys.size - maxClaimedKandanMessageKeys;
|
|
3491
|
+
const expiredKeys = Array.from(state.claimedKandanMessageKeys).slice(0, overflow);
|
|
3492
|
+
for (const expiredKey of expiredKeys) {
|
|
3493
|
+
state.claimedKandanMessageKeys.delete(expiredKey);
|
|
3494
|
+
claimedKandanMessageKeys.delete(expiredKey);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
return true;
|
|
3498
|
+
}
|
|
3499
|
+
function releaseKandanMessageClaims(state) {
|
|
3500
|
+
for (const key of state.claimedKandanMessageKeys) {
|
|
3501
|
+
claimedKandanMessageKeys.delete(key);
|
|
3502
|
+
}
|
|
3503
|
+
state.claimedKandanMessageKeys.clear();
|
|
3504
|
+
}
|
|
3395
3505
|
async function startThreadMessageTurn(args, state, payloadContext, message) {
|
|
3396
3506
|
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
3397
3507
|
throw new Error("cannot start a local Codex turn before thread binding");
|
|
@@ -3554,7 +3664,16 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
3554
3664
|
new_codex_thread_id: newCodexThreadId
|
|
3555
3665
|
});
|
|
3556
3666
|
await postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId);
|
|
3557
|
-
|
|
3667
|
+
try {
|
|
3668
|
+
state.pendingReconnectContextInjection = await fetchReconnectContextInjection(args, state);
|
|
3669
|
+
} catch (contextError) {
|
|
3670
|
+
state.pendingReconnectContextInjection = undefined;
|
|
3671
|
+
args.log("codex.thread_reconnect_context_failed", {
|
|
3672
|
+
kandan_thread_id: state.kandanThreadId,
|
|
3673
|
+
codex_thread_id: newCodexThreadId,
|
|
3674
|
+
message: contextError instanceof Error ? contextError.message : String(contextError)
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3558
3677
|
requeuePendingKandanMessageFront(state.queue, next);
|
|
3559
3678
|
state.turn = { status: "idle" };
|
|
3560
3679
|
await drainKandanMessageQueue(args, state, payloadContext);
|
|
@@ -4913,6 +5032,9 @@ async function refreshActiveProcessingState(args, state, turnId, reason) {
|
|
|
4913
5032
|
if (state.activeProcessingState?.seq === seq && state.activeProcessingState.reason === reason) {
|
|
4914
5033
|
return;
|
|
4915
5034
|
}
|
|
5035
|
+
if (state.activeProcessingState?.seq === seq && state.activeProcessingState.reason === "awaiting approval") {
|
|
5036
|
+
return;
|
|
5037
|
+
}
|
|
4916
5038
|
state.activeProcessingState = { seq, reason };
|
|
4917
5039
|
await publishMessageState(args, state.kandanThreadId, seq, {
|
|
4918
5040
|
status: "processing",
|
|
@@ -4924,6 +5046,14 @@ async function refreshActiveProcessingHeartbeat(args, state) {
|
|
|
4924
5046
|
if (activeProcessingState === undefined || state.kandanThreadId === undefined) {
|
|
4925
5047
|
return;
|
|
4926
5048
|
}
|
|
5049
|
+
if (activeProcessingState.reason === "awaiting approval") {
|
|
5050
|
+
switch (activeProcessingState.approval.kind) {
|
|
5051
|
+
case "local_runner_port_forward":
|
|
5052
|
+
return;
|
|
5053
|
+
default:
|
|
5054
|
+
break;
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
4927
5057
|
await publishMessageState(args, state.kandanThreadId, activeProcessingState.seq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
|
|
4928
5058
|
}
|
|
4929
5059
|
function clearActiveProcessingState(state, seq) {
|
|
@@ -8133,7 +8263,7 @@ function realpathOrResolved(pathValue) {
|
|
|
8133
8263
|
}
|
|
8134
8264
|
|
|
8135
8265
|
// src/version.ts
|
|
8136
|
-
var linzumiCliVersion = "0.0.
|
|
8266
|
+
var linzumiCliVersion = "0.0.44-beta";
|
|
8137
8267
|
var linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
|
|
8138
8268
|
|
|
8139
8269
|
// src/runnerLock.ts
|
|
@@ -8502,6 +8632,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8502
8632
|
const allowedForwardPorts = options.allowedForwardPorts ?? [];
|
|
8503
8633
|
const liveForwardPorts = new Set(allowedForwardPorts);
|
|
8504
8634
|
const managedForwardPorts = new Set;
|
|
8635
|
+
const kandanControlPort = explicitUrlPort(options.kandanUrl);
|
|
8636
|
+
const suppressedForwardPorts = () => suppressedForwardPortsForRunner(kandanControlPort, managedForwardPorts);
|
|
8505
8637
|
const forwardPortAttributions = new Map;
|
|
8506
8638
|
const setForwardPortAttribution = (port, attribution) => {
|
|
8507
8639
|
forwardPortAttributions.set(port, {
|
|
@@ -8509,7 +8641,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8509
8641
|
codexThreadId: attribution.codexThreadId ?? null,
|
|
8510
8642
|
channelSlug: attribution.channelSlug ?? null,
|
|
8511
8643
|
processName: attribution.processName ?? null,
|
|
8512
|
-
processIconKey: attribution.processIconKey ?? null
|
|
8644
|
+
processIconKey: attribution.processIconKey ?? null,
|
|
8645
|
+
localEditor: attribution.localEditor === true
|
|
8513
8646
|
});
|
|
8514
8647
|
};
|
|
8515
8648
|
const clearForwardPortAttribution = (port) => {
|
|
@@ -8523,7 +8656,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8523
8656
|
codexThreadId: attribution?.codexThreadId ?? null,
|
|
8524
8657
|
channelSlug: attribution?.channelSlug ?? null,
|
|
8525
8658
|
processName: attribution?.processName ?? null,
|
|
8526
|
-
processIconKey: attribution?.processIconKey ?? null
|
|
8659
|
+
processIconKey: attribution?.processIconKey ?? null,
|
|
8660
|
+
localEditor: attribution?.localEditor === true
|
|
8527
8661
|
};
|
|
8528
8662
|
});
|
|
8529
8663
|
const allowedCwds = { value: [...options.allowedCwds] };
|
|
@@ -8534,11 +8668,35 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8534
8668
|
value: { status: "disabled" }
|
|
8535
8669
|
};
|
|
8536
8670
|
const localEditorGeneration = { value: 0 };
|
|
8671
|
+
const localEditorForwardState = {
|
|
8672
|
+
watcher: undefined,
|
|
8673
|
+
approvedPorts: new Set,
|
|
8674
|
+
approvedTargets: new Map,
|
|
8675
|
+
dismissedTargets: new Map,
|
|
8676
|
+
pendingRequests: new Map,
|
|
8677
|
+
queuedCandidates: new Map
|
|
8678
|
+
};
|
|
8537
8679
|
cleanup.actions.push(() => {
|
|
8538
8680
|
if (localEditorState.value.status === "running") {
|
|
8539
8681
|
localEditorState.value.process.kill("SIGINT");
|
|
8540
8682
|
localEditorState.value.collaboration?.process.kill("SIGINT");
|
|
8541
8683
|
}
|
|
8684
|
+
localEditorForwardState.watcher?.close();
|
|
8685
|
+
});
|
|
8686
|
+
const pendingEditorForwardPortRequests = () => Array.from(localEditorForwardState.pendingRequests.values()).sort((left, right) => left.port - right.port).map((request) => {
|
|
8687
|
+
const processIdentity = guessCanonicalProcessFromCommand(request.command);
|
|
8688
|
+
const label = processIdentity?.appName ?? portForwardPromptLabel(approvedTargetFromRequest(request));
|
|
8689
|
+
return {
|
|
8690
|
+
requestId: request.requestId,
|
|
8691
|
+
port: request.port,
|
|
8692
|
+
pid: request.pid,
|
|
8693
|
+
command: request.command,
|
|
8694
|
+
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
8695
|
+
summary: `Make ${label} on port ${request.port} accessible on Linzumi?`,
|
|
8696
|
+
reason: portForwardPromptReason(approvedTargetFromRequest(request)),
|
|
8697
|
+
...processIdentity?.appName === undefined ? {} : { processName: processIdentity.appName },
|
|
8698
|
+
...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey }
|
|
8699
|
+
};
|
|
8542
8700
|
});
|
|
8543
8701
|
const capabilitiesPayload = () => ({
|
|
8544
8702
|
codexAppServer: true,
|
|
@@ -8550,6 +8708,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8550
8708
|
portForwarding: liveForwardPorts.size > 0,
|
|
8551
8709
|
allowedPorts: Array.from(liveForwardPorts).sort((left, right) => left - right),
|
|
8552
8710
|
forwardedPortAttributions: buildForwardPortAttributionPayload(),
|
|
8711
|
+
pendingEditorForwardPortRequests: pendingEditorForwardPortRequests(),
|
|
8553
8712
|
toolStatus: options.dependencyStatus === undefined ? null : dependencyStatusPayload(options.dependencyStatus),
|
|
8554
8713
|
editorRuntime: options.dependencyStatus?.editorRuntime === undefined ? null : dependencyStatusPayload(options.dependencyStatus).editorRuntime,
|
|
8555
8714
|
...localEditorCapabilities(options.editorRuntime, allowedCwds.value, localEditorState.value)
|
|
@@ -8561,6 +8720,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8561
8720
|
const joinPayload = () => ({
|
|
8562
8721
|
clientName: "kandan-local-codex-runner",
|
|
8563
8722
|
clientId,
|
|
8723
|
+
runnerPid: process.pid,
|
|
8564
8724
|
version: linzumiCliVersion,
|
|
8565
8725
|
workspace: runnerWorkspaceSlug(options) ?? null,
|
|
8566
8726
|
channel: options.channelSession?.channelSlug ?? null,
|
|
@@ -8598,12 +8758,204 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8598
8758
|
});
|
|
8599
8759
|
});
|
|
8600
8760
|
};
|
|
8761
|
+
const pushEditorPortForwardEvent = (event, payload) => {
|
|
8762
|
+
kandan.push(topic, event, payload).catch((error) => {
|
|
8763
|
+
log(`kandan.${event}_push_failed`, {
|
|
8764
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8765
|
+
});
|
|
8766
|
+
});
|
|
8767
|
+
};
|
|
8768
|
+
const revokeEditorForwardPort = (port, reason) => {
|
|
8769
|
+
const target = localEditorForwardState.approvedTargets.get(port);
|
|
8770
|
+
localEditorForwardState.approvedPorts.delete(port);
|
|
8771
|
+
localEditorForwardState.approvedTargets.delete(port);
|
|
8772
|
+
liveForwardPorts.delete(port);
|
|
8773
|
+
clearForwardPortAttribution(port);
|
|
8774
|
+
pushEditorPortForwardEvent("forward_port_revoked", {
|
|
8775
|
+
instanceId,
|
|
8776
|
+
port,
|
|
8777
|
+
...target === undefined ? {} : {
|
|
8778
|
+
pid: target.pid,
|
|
8779
|
+
command: target.command,
|
|
8780
|
+
...target.cwd === undefined ? {} : { cwd: target.cwd }
|
|
8781
|
+
},
|
|
8782
|
+
reason,
|
|
8783
|
+
capabilities: revocationCapabilities(capabilitiesPayload(), port)
|
|
8784
|
+
});
|
|
8785
|
+
};
|
|
8786
|
+
const clearEditorPortForwarding = (reason) => {
|
|
8787
|
+
localEditorForwardState.watcher?.close();
|
|
8788
|
+
localEditorForwardState.watcher = undefined;
|
|
8789
|
+
localEditorForwardState.pendingRequests.clear();
|
|
8790
|
+
localEditorForwardState.queuedCandidates.clear();
|
|
8791
|
+
const revokedPorts = Array.from(localEditorForwardState.approvedPorts);
|
|
8792
|
+
for (const port of revokedPorts) {
|
|
8793
|
+
revokeEditorForwardPort(port, reason);
|
|
8794
|
+
}
|
|
8795
|
+
localEditorForwardState.dismissedTargets.clear();
|
|
8796
|
+
return revokedPorts;
|
|
8797
|
+
};
|
|
8798
|
+
const drainQueuedEditorPortForwardPrompt = () => {
|
|
8799
|
+
if (localEditorForwardState.pendingRequests.size > 0 || localEditorForwardState.queuedCandidates.size === 0) {
|
|
8800
|
+
return;
|
|
8801
|
+
}
|
|
8802
|
+
const next = Array.from(localEditorForwardState.queuedCandidates.values()).sort((left, right) => left.port - right.port)[0];
|
|
8803
|
+
if (next === undefined) {
|
|
8804
|
+
return;
|
|
8805
|
+
}
|
|
8806
|
+
localEditorForwardState.queuedCandidates.delete(next.port);
|
|
8807
|
+
publishEditorPortForwardPrompt(next);
|
|
8808
|
+
if (localEditorForwardState.pendingRequests.size === 0) {
|
|
8809
|
+
drainQueuedEditorPortForwardPrompt();
|
|
8810
|
+
}
|
|
8811
|
+
};
|
|
8812
|
+
const publishEditorPortForwardPrompt = (candidate) => {
|
|
8813
|
+
const review = reviewPortForwardCandidate({
|
|
8814
|
+
candidate,
|
|
8815
|
+
threadBound: true,
|
|
8816
|
+
suppressedPorts: new Set(suppressedForwardPorts()),
|
|
8817
|
+
approvedPorts: localEditorForwardState.approvedPorts,
|
|
8818
|
+
approvedTargets: localEditorForwardState.approvedTargets,
|
|
8819
|
+
dismissedTargets: localEditorForwardState.dismissedTargets,
|
|
8820
|
+
pendingRequests: Array.from(localEditorForwardState.pendingRequests.values())
|
|
8821
|
+
});
|
|
8822
|
+
switch (review.type) {
|
|
8823
|
+
case "skip":
|
|
8824
|
+
return;
|
|
8825
|
+
case "remember_approved_target":
|
|
8826
|
+
localEditorForwardState.approvedTargets.set(review.target.port, review.target);
|
|
8827
|
+
return;
|
|
8828
|
+
case "revoke_and_prompt":
|
|
8829
|
+
if (localEditorForwardState.pendingRequests.size > 0) {
|
|
8830
|
+
localEditorForwardState.queuedCandidates.set(candidate.port, candidate);
|
|
8831
|
+
return;
|
|
8832
|
+
}
|
|
8833
|
+
revokeEditorForwardPort(review.revoked.port, review.reason);
|
|
8834
|
+
break;
|
|
8835
|
+
case "prompt":
|
|
8836
|
+
if (localEditorForwardState.pendingRequests.size > 0) {
|
|
8837
|
+
localEditorForwardState.queuedCandidates.set(candidate.port, candidate);
|
|
8838
|
+
return;
|
|
8839
|
+
}
|
|
8840
|
+
break;
|
|
8841
|
+
}
|
|
8842
|
+
const requestId = `editor-port-forward-${randomUUID3()}`;
|
|
8843
|
+
const request = pendingRequestFromCandidate({
|
|
8844
|
+
requestId,
|
|
8845
|
+
sourceSeq: 0,
|
|
8846
|
+
candidate
|
|
8847
|
+
});
|
|
8848
|
+
localEditorForwardState.pendingRequests.set(request.port, request);
|
|
8849
|
+
const processIdentity = guessCanonicalProcessFromCommand(candidate.command);
|
|
8850
|
+
pushEditorPortForwardEvent("forward_port_requested", {
|
|
8851
|
+
instanceId,
|
|
8852
|
+
requestId,
|
|
8853
|
+
source: "local_editor",
|
|
8854
|
+
port: request.port,
|
|
8855
|
+
pid: request.pid,
|
|
8856
|
+
command: request.command,
|
|
8857
|
+
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
8858
|
+
...processIdentity?.appName === undefined ? {} : { processName: processIdentity.appName },
|
|
8859
|
+
...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey },
|
|
8860
|
+
capabilities: capabilitiesPayload()
|
|
8861
|
+
});
|
|
8862
|
+
};
|
|
8863
|
+
const expireLostEditorPortForwardCandidate = (candidate) => {
|
|
8864
|
+
const queuedCandidate = localEditorForwardState.queuedCandidates.get(candidate.port);
|
|
8865
|
+
if (queuedCandidate !== undefined && sameForwardCandidate(queuedCandidate, candidate)) {
|
|
8866
|
+
localEditorForwardState.queuedCandidates.delete(candidate.port);
|
|
8867
|
+
return;
|
|
8868
|
+
}
|
|
8869
|
+
const pendingRequest = localEditorForwardState.pendingRequests.get(candidate.port);
|
|
8870
|
+
if (pendingRequest !== undefined && sameForwardCandidate(approvedTargetFromRequest(pendingRequest), candidate)) {
|
|
8871
|
+
localEditorForwardState.pendingRequests.delete(candidate.port);
|
|
8872
|
+
pushEditorPortForwardEvent("forward_port_revoked", {
|
|
8873
|
+
instanceId,
|
|
8874
|
+
port: candidate.port,
|
|
8875
|
+
pid: candidate.pid,
|
|
8876
|
+
command: candidate.command,
|
|
8877
|
+
...candidate.cwd === undefined ? {} : { cwd: candidate.cwd },
|
|
8878
|
+
reason: "listener_exited",
|
|
8879
|
+
capabilities: revocationCapabilities(capabilitiesPayload(), candidate.port)
|
|
8880
|
+
});
|
|
8881
|
+
drainQueuedEditorPortForwardPrompt();
|
|
8882
|
+
return;
|
|
8883
|
+
}
|
|
8884
|
+
if (localEditorForwardState.approvedPorts.has(candidate.port)) {
|
|
8885
|
+
revokeEditorForwardPort(candidate.port, "listener_exited");
|
|
8886
|
+
}
|
|
8887
|
+
};
|
|
8888
|
+
const resolveEditorPortForwardRequest = (control) => {
|
|
8889
|
+
const request = Array.from(localEditorForwardState.pendingRequests.values()).find((request2) => request2.requestId === control.requestId);
|
|
8890
|
+
if (request === undefined) {
|
|
8891
|
+
return;
|
|
8892
|
+
}
|
|
8893
|
+
localEditorForwardState.pendingRequests.delete(request.port);
|
|
8894
|
+
if (control.decision === "deny") {
|
|
8895
|
+
localEditorForwardState.dismissedTargets.set(request.port, approvedTargetFromRequest(request));
|
|
8896
|
+
pushEditorPortForwardEvent("forward_port_resolved", {
|
|
8897
|
+
instanceId,
|
|
8898
|
+
requestId: request.requestId,
|
|
8899
|
+
source: "local_editor",
|
|
8900
|
+
port: request.port,
|
|
8901
|
+
pid: request.pid,
|
|
8902
|
+
command: request.command,
|
|
8903
|
+
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
8904
|
+
decision: "deny",
|
|
8905
|
+
capabilities: capabilitiesPayload()
|
|
8906
|
+
});
|
|
8907
|
+
drainQueuedEditorPortForwardPrompt();
|
|
8908
|
+
return { instanceId, ok: true };
|
|
8909
|
+
}
|
|
8910
|
+
localEditorForwardState.approvedPorts.add(request.port);
|
|
8911
|
+
localEditorForwardState.approvedTargets.set(request.port, approvedTargetFromRequest(request));
|
|
8912
|
+
liveForwardPorts.add(request.port);
|
|
8913
|
+
const processIdentity = guessCanonicalProcessFromCommand(request.command);
|
|
8914
|
+
setForwardPortAttribution(request.port, {
|
|
8915
|
+
localEditor: true,
|
|
8916
|
+
processName: processIdentity?.appName ?? null,
|
|
8917
|
+
processIconKey: processIdentity?.iconKey ?? null
|
|
8918
|
+
});
|
|
8919
|
+
pushEditorPortForwardEvent("forward_port_resolved", {
|
|
8920
|
+
instanceId,
|
|
8921
|
+
requestId: request.requestId,
|
|
8922
|
+
source: "local_editor",
|
|
8923
|
+
port: request.port,
|
|
8924
|
+
pid: request.pid,
|
|
8925
|
+
command: request.command,
|
|
8926
|
+
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
8927
|
+
decision: "approve",
|
|
8928
|
+
capabilities: capabilitiesPayload()
|
|
8929
|
+
});
|
|
8930
|
+
drainQueuedEditorPortForwardPrompt();
|
|
8931
|
+
return { instanceId, ok: true, port: request.port };
|
|
8932
|
+
};
|
|
8933
|
+
const startLocalEditorPortForwardWatcher = (state) => {
|
|
8934
|
+
if (state.process.pid === undefined) {
|
|
8935
|
+
log("port_forward.local_editor_watch_skipped", {
|
|
8936
|
+
reason: "editor_pid_missing"
|
|
8937
|
+
});
|
|
8938
|
+
return;
|
|
8939
|
+
}
|
|
8940
|
+
localEditorForwardState.watcher?.close();
|
|
8941
|
+
localEditorForwardState.watcher = startPortForwardWatcher({
|
|
8942
|
+
rootPid: state.process.pid,
|
|
8943
|
+
onCandidate: publishEditorPortForwardPrompt,
|
|
8944
|
+
onCandidateLost: expireLostEditorPortForwardCandidate,
|
|
8945
|
+
onError: (error) => {
|
|
8946
|
+
log("port_forward.local_editor_watch_failed", {
|
|
8947
|
+
message: error.message
|
|
8948
|
+
});
|
|
8949
|
+
}
|
|
8950
|
+
});
|
|
8951
|
+
};
|
|
8601
8952
|
const watchLocalEditorExit = (state, generation, initialStatusPushed) => {
|
|
8602
8953
|
const handleExit = () => {
|
|
8603
8954
|
if (localEditorGeneration.value !== generation || localEditorState.value.status !== "running" || localEditorState.value.process !== state.process) {
|
|
8604
8955
|
return;
|
|
8605
8956
|
}
|
|
8606
8957
|
localEditorState.value = { status: "disabled" };
|
|
8958
|
+
const revokedEditorPorts = clearEditorPortForwarding("editor_exited");
|
|
8607
8959
|
liveForwardPorts.delete(state.port);
|
|
8608
8960
|
managedForwardPorts.delete(state.port);
|
|
8609
8961
|
if (state.collaboration !== undefined) {
|
|
@@ -8616,7 +8968,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8616
8968
|
cwd: state.cwd,
|
|
8617
8969
|
capabilities: {
|
|
8618
8970
|
...capabilitiesPayload(),
|
|
8619
|
-
revokedPorts: state.collaboration === undefined ? [state.port] : [
|
|
8971
|
+
revokedPorts: state.collaboration === undefined ? [state.port, ...revokedEditorPorts] : [
|
|
8972
|
+
state.port,
|
|
8973
|
+
state.collaboration.serverPort,
|
|
8974
|
+
...revokedEditorPorts
|
|
8975
|
+
]
|
|
8620
8976
|
}
|
|
8621
8977
|
});
|
|
8622
8978
|
};
|
|
@@ -8674,7 +9030,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8674
9030
|
launchTui: options.launchTui,
|
|
8675
9031
|
enablePortForwardWatch: true,
|
|
8676
9032
|
initialForwardPorts: allowedForwardPorts,
|
|
8677
|
-
suppressedForwardPorts
|
|
9033
|
+
suppressedForwardPorts,
|
|
8678
9034
|
onForwardPortApproved: (port, attribution) => {
|
|
8679
9035
|
liveForwardPorts.add(port);
|
|
8680
9036
|
setForwardPortAttribution(port, attribution);
|
|
@@ -8729,7 +9085,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8729
9085
|
launchTui: false,
|
|
8730
9086
|
enablePortForwardWatch: true,
|
|
8731
9087
|
initialForwardPorts: allowedForwardPorts,
|
|
8732
|
-
suppressedForwardPorts
|
|
9088
|
+
suppressedForwardPorts,
|
|
8733
9089
|
onForwardPortApproved: (port, attribution) => {
|
|
8734
9090
|
liveForwardPorts.add(port);
|
|
8735
9091
|
setForwardPortAttribution(port, attribution);
|
|
@@ -8900,6 +9256,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8900
9256
|
if (result.ok) {
|
|
8901
9257
|
localEditorGeneration.value += 1;
|
|
8902
9258
|
const editorGeneration = localEditorGeneration.value;
|
|
9259
|
+
const restartedEditorRevokedPorts = localEditorState.value.status === "running" ? [
|
|
9260
|
+
localEditorState.value.port,
|
|
9261
|
+
...localEditorState.value.collaboration === undefined ? [] : [localEditorState.value.collaboration.serverPort],
|
|
9262
|
+
...clearEditorPortForwarding("editor_restarted")
|
|
9263
|
+
] : [];
|
|
8903
9264
|
if (localEditorState.value.status === "running") {
|
|
8904
9265
|
liveForwardPorts.delete(localEditorState.value.port);
|
|
8905
9266
|
managedForwardPorts.delete(localEditorState.value.port);
|
|
@@ -8919,10 +9280,14 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8919
9280
|
instanceId,
|
|
8920
9281
|
requestId: control.requestId,
|
|
8921
9282
|
ok: true,
|
|
8922
|
-
capabilities:
|
|
9283
|
+
capabilities: {
|
|
9284
|
+
...capabilitiesPayload(),
|
|
9285
|
+
...restartedEditorRevokedPorts.length === 0 ? {} : { revokedPorts: restartedEditorRevokedPorts }
|
|
9286
|
+
},
|
|
8923
9287
|
...result.event
|
|
8924
9288
|
});
|
|
8925
9289
|
if (result.state.status === "running") {
|
|
9290
|
+
startLocalEditorPortForwardWatcher(result.state);
|
|
8926
9291
|
watchLocalEditorExit(result.state, editorGeneration, initialStatusPushed);
|
|
8927
9292
|
}
|
|
8928
9293
|
return initialStatusPushed;
|
|
@@ -8955,6 +9320,17 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8955
9320
|
pushHeartbeat();
|
|
8956
9321
|
return;
|
|
8957
9322
|
}
|
|
9323
|
+
if (isResolvePortForwardRequestControl(control)) {
|
|
9324
|
+
const response = resolveEditorPortForwardRequest(control);
|
|
9325
|
+
if (response !== undefined) {
|
|
9326
|
+
kandan.push(topic, "codex_response", response).catch((error) => {
|
|
9327
|
+
log("kandan.control_response_push_failed", {
|
|
9328
|
+
message: error instanceof Error ? error.message : String(error)
|
|
9329
|
+
});
|
|
9330
|
+
});
|
|
9331
|
+
return;
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
8958
9334
|
if (isSetPortForwardEnabledControl(control)) {
|
|
8959
9335
|
switch (control.enabled) {
|
|
8960
9336
|
case true:
|
|
@@ -8964,6 +9340,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8964
9340
|
liveForwardPorts.delete(control.port);
|
|
8965
9341
|
managedForwardPorts.delete(control.port);
|
|
8966
9342
|
clearForwardPortAttribution(control.port);
|
|
9343
|
+
localEditorForwardState.approvedPorts.delete(control.port);
|
|
9344
|
+
localEditorForwardState.approvedTargets.delete(control.port);
|
|
8967
9345
|
kandan.push(topic, "forward_port_revoked", {
|
|
8968
9346
|
instanceId,
|
|
8969
9347
|
port: control.port,
|
|
@@ -9155,6 +9533,24 @@ function codexTuiArgs(codexUrl, codexThreadId, session, fast) {
|
|
|
9155
9533
|
const overrides = codexTuiConfigArgs(session, fast);
|
|
9156
9534
|
return codexThreadId === undefined ? ["--remote", codexUrl, ...overrides] : ["resume", "--remote", codexUrl, ...overrides, codexThreadId];
|
|
9157
9535
|
}
|
|
9536
|
+
function suppressedForwardPortsForRunner(kandanControlPort, managedForwardPorts) {
|
|
9537
|
+
const suppressedPorts = new Set(managedForwardPorts);
|
|
9538
|
+
if (kandanControlPort !== undefined) {
|
|
9539
|
+
suppressedPorts.add(kandanControlPort);
|
|
9540
|
+
}
|
|
9541
|
+
return Array.from(suppressedPorts).sort((left, right) => left - right);
|
|
9542
|
+
}
|
|
9543
|
+
function explicitUrlPort(url) {
|
|
9544
|
+
const parsed = new URL(url);
|
|
9545
|
+
if (parsed.port === "") {
|
|
9546
|
+
return;
|
|
9547
|
+
}
|
|
9548
|
+
const port = Number.parseInt(parsed.port, 10);
|
|
9549
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
9550
|
+
throw new Error(`invalid Kandan URL port: ${url}`);
|
|
9551
|
+
}
|
|
9552
|
+
return port;
|
|
9553
|
+
}
|
|
9158
9554
|
function codexTuiConfigArgs(session, fast) {
|
|
9159
9555
|
const modelArgs = session?.model === undefined ? [] : ["--model", session.model];
|
|
9160
9556
|
const reasoningArgs = session?.reasoningEffort === undefined ? [] : ["-c", `model_reasoning_effort="${session.reasoningEffort}"`];
|
|
@@ -9571,6 +9967,9 @@ function isUpdateRunnerConfigControl(control) {
|
|
|
9571
9967
|
function isSetPortForwardEnabledControl(control) {
|
|
9572
9968
|
return control.type === "set_port_forward_enabled" && Number.isInteger(control.port) && control.port > 0 && control.port <= 65535;
|
|
9573
9969
|
}
|
|
9970
|
+
function isResolvePortForwardRequestControl(control) {
|
|
9971
|
+
return control.type === "resolve_port_forward_request" && typeof control.requestId === "string" && (control.decision === "approve" || control.decision === "deny");
|
|
9972
|
+
}
|
|
9574
9973
|
function normalizeAllowedCwds(values) {
|
|
9575
9974
|
return Array.from(new Set(values.flatMap((value) => {
|
|
9576
9975
|
const normalized = value.trim();
|
package/package.json
CHANGED