@linzumi/cli 0.0.106-beta → 0.0.108-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 +1868 -171
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1439,6 +1439,57 @@ function parseKandanChatAttachments(attachments) {
|
|
|
1439
1439
|
];
|
|
1440
1440
|
});
|
|
1441
1441
|
}
|
|
1442
|
+
function kandanChatAttachmentsToWire(attachments) {
|
|
1443
|
+
return attachments.map((attachment) => ({
|
|
1444
|
+
...attachment.id === void 0 ? {} : { id: attachment.id },
|
|
1445
|
+
...attachment.kind === void 0 ? {} : { kind: attachment.kind },
|
|
1446
|
+
...attachment.fileName === void 0 ? {} : { file_name: attachment.fileName },
|
|
1447
|
+
...attachment.contentType === void 0 ? {} : { content_type: attachment.contentType },
|
|
1448
|
+
...attachment.sizeBytes === void 0 ? {} : { size_bytes: attachment.sizeBytes },
|
|
1449
|
+
...attachment.url === void 0 ? {} : { url: attachment.url }
|
|
1450
|
+
}));
|
|
1451
|
+
}
|
|
1452
|
+
function serializeKandanChatEventForQueue(event) {
|
|
1453
|
+
return {
|
|
1454
|
+
seq: event.seq,
|
|
1455
|
+
type: event.type,
|
|
1456
|
+
...event.actorKind === void 0 ? {} : { actor_kind: event.actorKind },
|
|
1457
|
+
...event.actorSlug === void 0 ? {} : { actor_slug: event.actorSlug },
|
|
1458
|
+
...event.actorUserId === void 0 ? {} : { actor_user_id: event.actorUserId },
|
|
1459
|
+
...event.threadId === void 0 ? {} : { thread_id: event.threadId },
|
|
1460
|
+
...event.threadTitle === void 0 ? {} : { thread_title: event.threadTitle },
|
|
1461
|
+
...event.replyToSeq === void 0 ? {} : { reply_to_seq: event.replyToSeq },
|
|
1462
|
+
...event.localRunnerEventType === void 0 ? {} : { local_runner_event_type: event.localRunnerEventType },
|
|
1463
|
+
body: event.body,
|
|
1464
|
+
attachments: kandanChatAttachmentsToWire(event.attachments)
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function deserializeKandanChatEventFromQueue(value) {
|
|
1468
|
+
const payload = objectValue(value);
|
|
1469
|
+
if (payload === void 0) {
|
|
1470
|
+
return void 0;
|
|
1471
|
+
}
|
|
1472
|
+
const seq = integerValue(payload.seq);
|
|
1473
|
+
const type = stringValue(payload.type);
|
|
1474
|
+
if (seq === void 0 || type === void 0) {
|
|
1475
|
+
return void 0;
|
|
1476
|
+
}
|
|
1477
|
+
return {
|
|
1478
|
+
seq,
|
|
1479
|
+
type,
|
|
1480
|
+
actorKind: stringValue(payload.actor_kind),
|
|
1481
|
+
actorSlug: stringValue(payload.actor_slug),
|
|
1482
|
+
actorUserId: integerValue(payload.actor_user_id),
|
|
1483
|
+
threadId: stringValue(payload.thread_id),
|
|
1484
|
+
threadTitle: stringValue(payload.thread_title),
|
|
1485
|
+
replyToSeq: integerValue(payload.reply_to_seq),
|
|
1486
|
+
localRunnerEventType: stringValue(payload.local_runner_event_type),
|
|
1487
|
+
body: stringValue(payload.body) ?? "",
|
|
1488
|
+
attachments: parseKandanChatAttachments(
|
|
1489
|
+
arrayValue(payload.attachments) ?? []
|
|
1490
|
+
)
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1442
1493
|
function isCodexAuthoredEvent(event) {
|
|
1443
1494
|
if (event.localRunnerEventType !== void 0) {
|
|
1444
1495
|
return true;
|
|
@@ -3716,7 +3767,9 @@ async function connectPhoenixClient(baseUrl, token, socketFactory = (url, protoc
|
|
|
3716
3767
|
controlBacklogFetchesInFlight.add(topic);
|
|
3717
3768
|
void fetchRemainingControlBacklog(topic).catch(() => void 0).finally(() => controlBacklogFetchesInFlight.delete(topic));
|
|
3718
3769
|
};
|
|
3770
|
+
const lastRejoinReplies = /* @__PURE__ */ new Map();
|
|
3719
3771
|
const replayJoins = async () => {
|
|
3772
|
+
lastRejoinReplies.clear();
|
|
3720
3773
|
for (const [topic, registrations] of joins) {
|
|
3721
3774
|
const reply = await pushOnOpenSocket(
|
|
3722
3775
|
topic,
|
|
@@ -3731,6 +3784,9 @@ async function connectPhoenixClient(baseUrl, token, socketFactory = (url, protoc
|
|
|
3731
3784
|
if (!isJoinReply(reply)) {
|
|
3732
3785
|
throw new Error(`phoenix rejoin failed for ${topic}`);
|
|
3733
3786
|
}
|
|
3787
|
+
if (isJsonObject(reply.response)) {
|
|
3788
|
+
lastRejoinReplies.set(topic, reply.response);
|
|
3789
|
+
}
|
|
3734
3790
|
}
|
|
3735
3791
|
};
|
|
3736
3792
|
const scheduleReconnect = () => {
|
|
@@ -3832,8 +3888,9 @@ async function connectPhoenixClient(baseUrl, token, socketFactory = (url, protoc
|
|
|
3832
3888
|
startHeartbeat();
|
|
3833
3889
|
markReady();
|
|
3834
3890
|
if (state.connectionGeneration > 1) {
|
|
3891
|
+
const rejoinReplies = new Map(lastRejoinReplies);
|
|
3835
3892
|
reconnectCallbacks.forEach((callback) => {
|
|
3836
|
-
void Promise.resolve(callback()).catch(() => void 0);
|
|
3893
|
+
void Promise.resolve(callback(rejoinReplies)).catch(() => void 0);
|
|
3837
3894
|
});
|
|
3838
3895
|
}
|
|
3839
3896
|
} catch (error) {
|
|
@@ -6900,6 +6957,11 @@ function createSessionPipelineBuilders(host) {
|
|
|
6900
6957
|
channel: wire.channelSlug,
|
|
6901
6958
|
thread_id: wire.kandanThreadId,
|
|
6902
6959
|
stream_key: streamKey,
|
|
6960
|
+
// SEAM 2: present the held lease epoch on the turn-execution write so
|
|
6961
|
+
// the server fences a stale, half-connected runner's duplicate output
|
|
6962
|
+
// (validate_turn_execution_lease_epoch). Omitted when unknown (the
|
|
6963
|
+
// server fence fails open on a nil epoch).
|
|
6964
|
+
...wire.leaseEpoch === void 0 ? {} : { lease_epoch: wire.leaseEpoch },
|
|
6903
6965
|
body: entryBody(data),
|
|
6904
6966
|
payload: {
|
|
6905
6967
|
...host.runnerPayload(
|
|
@@ -6946,7 +7008,14 @@ function createSessionPipelineBuilders(host) {
|
|
|
6946
7008
|
seq,
|
|
6947
7009
|
status,
|
|
6948
7010
|
...stringValue(data.reason) === void 0 ? {} : { reason: stringValue(data.reason) },
|
|
6949
|
-
...stringValue(data.turn_id) === void 0 ? {} : { turn_id: stringValue(data.turn_id) }
|
|
7011
|
+
...stringValue(data.turn_id) === void 0 ? {} : { turn_id: stringValue(data.turn_id) },
|
|
7012
|
+
// SEAM 2 (blocker #5): echo the held lease_epoch (snake_case) on the
|
|
7013
|
+
// message_state report too, not only the turn-EXECUTION write, so the
|
|
7014
|
+
// server fence (validate_turn_execution_lease_epoch) sees the current
|
|
7015
|
+
// epoch on the terminal report and does NOT fail open for a stale,
|
|
7016
|
+
// half-connected runner. Omitted for a pre-lease/old server (nil fence
|
|
7017
|
+
// fails open exactly as before).
|
|
7018
|
+
...wire.leaseEpoch === void 0 ? {} : { lease_epoch: wire.leaseEpoch }
|
|
6950
7019
|
}
|
|
6951
7020
|
};
|
|
6952
7021
|
}
|
|
@@ -8772,6 +8841,9 @@ function runnerActionErrorMessage(error) {
|
|
|
8772
8841
|
return userFacingBaseErrorMessage(message);
|
|
8773
8842
|
}
|
|
8774
8843
|
function userFacingBaseErrorMessage(message) {
|
|
8844
|
+
if (message.includes("daily_token_limit_exceeded") || message.includes("free Linzumi tokens")) {
|
|
8845
|
+
return "You've used all of your free Linzumi tokens for today. Your free allowance resets at midnight UTC - check back then, or email support@linzumi.com if you need more.";
|
|
8846
|
+
}
|
|
8775
8847
|
if (message === "phoenix join failed: workspace_not_found") {
|
|
8776
8848
|
return "Linzumi could not find this workspace. The command may be stale or copied from a different signup session. Return to setup and copy the latest command.";
|
|
8777
8849
|
}
|
|
@@ -9107,7 +9179,8 @@ function createChannelSessionPipeline(args, state, payloadContext) {
|
|
|
9107
9179
|
channelSlug: session.channelSlug,
|
|
9108
9180
|
kandanThreadId: state.kandanThreadId,
|
|
9109
9181
|
codexThreadId: state.codexThreadId,
|
|
9110
|
-
rootSeq: state.rootSeq
|
|
9182
|
+
rootSeq: state.rootSeq,
|
|
9183
|
+
leaseEpoch: state.kandanThreadId === void 0 ? void 0 : args.leaseEpochFor?.(state.kandanThreadId)
|
|
9111
9184
|
}),
|
|
9112
9185
|
runnerPayload: (eventType, codexThreadId, sourceMessageSeq, extra) => localRunnerPayload(
|
|
9113
9186
|
args.options,
|
|
@@ -11055,6 +11128,13 @@ async function drainKandanMessageQueueOnce(args, state, payloadContext) {
|
|
|
11055
11128
|
state.turn = { status: "active", turnId, queuedSeq: next.seq };
|
|
11056
11129
|
rememberTurnReplyTarget(state, turnId, next.seq);
|
|
11057
11130
|
args.log("codex.turn_started", { turn_id: turnId });
|
|
11131
|
+
if (state.kandanThreadId !== void 0) {
|
|
11132
|
+
args.options.onLiveSourceSeqAccepted?.({
|
|
11133
|
+
channel: args.options.channelSession.channelSlug,
|
|
11134
|
+
threadId: state.kandanThreadId,
|
|
11135
|
+
sourceSeq: next.seq
|
|
11136
|
+
});
|
|
11137
|
+
}
|
|
11058
11138
|
if (interruptAfterStart) {
|
|
11059
11139
|
await recoverInterruptedCodexTurn(
|
|
11060
11140
|
args,
|
|
@@ -15076,6 +15156,15 @@ import { createServer } from "node:net";
|
|
|
15076
15156
|
import { homedir as homedir8 } from "node:os";
|
|
15077
15157
|
import { join as join10 } from "node:path";
|
|
15078
15158
|
import { WebSocket as NodeWebSocket } from "ws";
|
|
15159
|
+
function pingWebSocketIfSupported(websocket) {
|
|
15160
|
+
const candidate = websocket;
|
|
15161
|
+
if (typeof candidate.ping === "function" && candidate.readyState === WebSocket.OPEN) {
|
|
15162
|
+
try {
|
|
15163
|
+
candidate.ping();
|
|
15164
|
+
} catch {
|
|
15165
|
+
}
|
|
15166
|
+
}
|
|
15167
|
+
}
|
|
15079
15168
|
async function chooseLoopbackPort() {
|
|
15080
15169
|
return new Promise((resolve12, reject) => {
|
|
15081
15170
|
const server = createServer();
|
|
@@ -15363,7 +15452,13 @@ async function connectCodexAppServer(websocketUrl, socketFactory = (url) => new
|
|
|
15363
15452
|
pending.forEach((pendingRequest) => pendingRequest.reject(error));
|
|
15364
15453
|
pending.clear();
|
|
15365
15454
|
};
|
|
15455
|
+
const keepalive = setInterval(
|
|
15456
|
+
() => pingWebSocketIfSupported(websocket),
|
|
15457
|
+
codexAppServerKeepaliveIntervalMs
|
|
15458
|
+
);
|
|
15459
|
+
keepalive.unref?.();
|
|
15366
15460
|
const handleConnectionClosed = (message) => {
|
|
15461
|
+
clearInterval(keepalive);
|
|
15367
15462
|
rejectPending(message);
|
|
15368
15463
|
if (state.closeError !== void 0) {
|
|
15369
15464
|
return;
|
|
@@ -15443,7 +15538,10 @@ async function connectCodexAppServer(websocketUrl, socketFactory = (url) => new
|
|
|
15443
15538
|
}
|
|
15444
15539
|
closeCallbacks.add(callback);
|
|
15445
15540
|
},
|
|
15446
|
-
close: () =>
|
|
15541
|
+
close: () => {
|
|
15542
|
+
clearInterval(keepalive);
|
|
15543
|
+
websocket.close();
|
|
15544
|
+
}
|
|
15447
15545
|
};
|
|
15448
15546
|
}
|
|
15449
15547
|
function parseCodexAppServerFrame(data) {
|
|
@@ -15653,7 +15751,7 @@ function readyzUrlForWebsocket(websocketUrl) {
|
|
|
15653
15751
|
parsed.hash = "";
|
|
15654
15752
|
return parsed.toString();
|
|
15655
15753
|
}
|
|
15656
|
-
var codexAppServerWatchdogPollMs, blockedCodexAppServerEnvKeys, codexDisableAppsConfigArgs;
|
|
15754
|
+
var codexAppServerWatchdogPollMs, codexAppServerKeepaliveIntervalMs, blockedCodexAppServerEnvKeys, codexDisableAppsConfigArgs;
|
|
15657
15755
|
var init_codexAppServer = __esm({
|
|
15658
15756
|
"src/codexAppServer.ts"() {
|
|
15659
15757
|
"use strict";
|
|
@@ -15663,6 +15761,14 @@ var init_codexAppServer = __esm({
|
|
|
15663
15761
|
init_engineChildReaper();
|
|
15664
15762
|
init_engineParentDeathWatchdog();
|
|
15665
15763
|
codexAppServerWatchdogPollMs = 2e3;
|
|
15764
|
+
codexAppServerKeepaliveIntervalMs = (() => {
|
|
15765
|
+
const raw = process.env.LINZUMI_CODEX_WS_KEEPALIVE_MS;
|
|
15766
|
+
if (raw === void 0) {
|
|
15767
|
+
return 2e4;
|
|
15768
|
+
}
|
|
15769
|
+
const parsed = Number.parseInt(raw, 10);
|
|
15770
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 2e4;
|
|
15771
|
+
})();
|
|
15666
15772
|
blockedCodexAppServerEnvKeys = [
|
|
15667
15773
|
"LINZUMI_MCP_ACCESS_TOKEN",
|
|
15668
15774
|
"LINZUMI_MCP_OWNER_USERNAME"
|
|
@@ -19972,7 +20078,7 @@ var linzumiCliVersion, linzumiCliVersionText;
|
|
|
19972
20078
|
var init_version = __esm({
|
|
19973
20079
|
"src/version.ts"() {
|
|
19974
20080
|
"use strict";
|
|
19975
|
-
linzumiCliVersion = "0.0.
|
|
20081
|
+
linzumiCliVersion = "0.0.108-beta";
|
|
19976
20082
|
linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
|
|
19977
20083
|
}
|
|
19978
20084
|
});
|
|
@@ -22702,6 +22808,9 @@ var init_authCache = __esm({
|
|
|
22702
22808
|
});
|
|
22703
22809
|
|
|
22704
22810
|
// src/threadCodexWorkerIpc.ts
|
|
22811
|
+
function isThreadCodexWorkerSessionClosedMessage(message) {
|
|
22812
|
+
return isJsonObject(message) && message.type === "linzumi_thread_codex_worker_session_closed";
|
|
22813
|
+
}
|
|
22705
22814
|
function bindThreadCodexWorkerIpc(codex, ipc = process) {
|
|
22706
22815
|
const pendingServerRequests = /* @__PURE__ */ new Map();
|
|
22707
22816
|
const state = { nextServerRequestId: 1, sendDropLogged: false };
|
|
@@ -23557,33 +23666,901 @@ var init_remoteCodexSandboxRunner = __esm({
|
|
|
23557
23666
|
}
|
|
23558
23667
|
});
|
|
23559
23668
|
|
|
23669
|
+
// src/durableInboundQueue.ts
|
|
23670
|
+
import {
|
|
23671
|
+
closeSync as closeSync3,
|
|
23672
|
+
fsyncSync,
|
|
23673
|
+
mkdirSync as mkdirSync13,
|
|
23674
|
+
openSync as openSync4,
|
|
23675
|
+
readFileSync as readFileSync17,
|
|
23676
|
+
renameSync as renameSync4,
|
|
23677
|
+
writeSync as writeSync2
|
|
23678
|
+
} from "node:fs";
|
|
23679
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
23680
|
+
import { dirname as dirname14, join as join21 } from "node:path";
|
|
23681
|
+
import { homedir as homedir14 } from "node:os";
|
|
23682
|
+
function crc32Hex(bytes) {
|
|
23683
|
+
let crc = 4294967295;
|
|
23684
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
23685
|
+
crc ^= bytes[i];
|
|
23686
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
23687
|
+
const mask = -(crc & 1);
|
|
23688
|
+
crc = crc >>> 1 ^ 3988292384 & mask;
|
|
23689
|
+
}
|
|
23690
|
+
}
|
|
23691
|
+
return ((crc ^ 4294967295) >>> 0).toString(16).padStart(8, "0");
|
|
23692
|
+
}
|
|
23693
|
+
function frameRecord(record) {
|
|
23694
|
+
const json = JSON.stringify(record);
|
|
23695
|
+
const bytes = Buffer.from(json, "utf8");
|
|
23696
|
+
return `${bytes.length}:${crc32Hex(bytes)}:${json}
|
|
23697
|
+
`;
|
|
23698
|
+
}
|
|
23699
|
+
function parseFramedLine(line) {
|
|
23700
|
+
const firstColon = line.indexOf(":");
|
|
23701
|
+
const secondColon = line.indexOf(":", firstColon + 1);
|
|
23702
|
+
if (firstColon <= 0 || secondColon <= firstColon) {
|
|
23703
|
+
return void 0;
|
|
23704
|
+
}
|
|
23705
|
+
const declaredLen = Number.parseInt(line.slice(0, firstColon), 10);
|
|
23706
|
+
const checksum = line.slice(firstColon + 1, secondColon);
|
|
23707
|
+
const json = line.slice(secondColon + 1);
|
|
23708
|
+
if (!Number.isInteger(declaredLen) || declaredLen <= 0) {
|
|
23709
|
+
return void 0;
|
|
23710
|
+
}
|
|
23711
|
+
const bytes = Buffer.from(json, "utf8");
|
|
23712
|
+
if (bytes.length !== declaredLen) {
|
|
23713
|
+
return void 0;
|
|
23714
|
+
}
|
|
23715
|
+
if (crc32Hex(bytes) !== checksum) {
|
|
23716
|
+
return void 0;
|
|
23717
|
+
}
|
|
23718
|
+
let parsed;
|
|
23719
|
+
try {
|
|
23720
|
+
parsed = JSON.parse(json);
|
|
23721
|
+
} catch {
|
|
23722
|
+
return void 0;
|
|
23723
|
+
}
|
|
23724
|
+
if (!isJsonObject(parsed)) {
|
|
23725
|
+
return void 0;
|
|
23726
|
+
}
|
|
23727
|
+
return coerceRecord(parsed);
|
|
23728
|
+
}
|
|
23729
|
+
function coerceRecord(value) {
|
|
23730
|
+
const op = value.op;
|
|
23731
|
+
const sourceSeq = value.sourceSeq;
|
|
23732
|
+
if (typeof sourceSeq !== "number" || !Number.isInteger(sourceSeq)) {
|
|
23733
|
+
return void 0;
|
|
23734
|
+
}
|
|
23735
|
+
if (op === "ack") {
|
|
23736
|
+
return { op: "ack", sourceSeq };
|
|
23737
|
+
}
|
|
23738
|
+
if (op === "dead") {
|
|
23739
|
+
return { op: "dead", sourceSeq };
|
|
23740
|
+
}
|
|
23741
|
+
if (op === "attempt") {
|
|
23742
|
+
const attempts = value.attempts;
|
|
23743
|
+
if (typeof attempts !== "number" || !Number.isInteger(attempts)) {
|
|
23744
|
+
return void 0;
|
|
23745
|
+
}
|
|
23746
|
+
return { op: "attempt", sourceSeq, attempts };
|
|
23747
|
+
}
|
|
23748
|
+
if (op === "enqueue") {
|
|
23749
|
+
const kind = value.kind === "replay" ? "replay" : "live";
|
|
23750
|
+
const payload = isJsonObject(value.payload) ? value.payload : void 0;
|
|
23751
|
+
const enqueuedAtMs = typeof value.enqueuedAtMs === "number" && Number.isFinite(value.enqueuedAtMs) ? value.enqueuedAtMs : void 0;
|
|
23752
|
+
if (payload === void 0 || enqueuedAtMs === void 0) {
|
|
23753
|
+
return void 0;
|
|
23754
|
+
}
|
|
23755
|
+
return { op: "enqueue", sourceSeq, kind, payload, enqueuedAtMs };
|
|
23756
|
+
}
|
|
23757
|
+
return void 0;
|
|
23758
|
+
}
|
|
23759
|
+
function foldQueueRecords(lines) {
|
|
23760
|
+
const enqueued = /* @__PURE__ */ new Map();
|
|
23761
|
+
const dead = /* @__PURE__ */ new Set();
|
|
23762
|
+
let highWater;
|
|
23763
|
+
let tornTail = false;
|
|
23764
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
23765
|
+
const line = lines[i];
|
|
23766
|
+
if (line === "") {
|
|
23767
|
+
continue;
|
|
23768
|
+
}
|
|
23769
|
+
const record = parseFramedLine(line);
|
|
23770
|
+
if (record === void 0) {
|
|
23771
|
+
tornTail = true;
|
|
23772
|
+
break;
|
|
23773
|
+
}
|
|
23774
|
+
switch (record.op) {
|
|
23775
|
+
case "enqueue": {
|
|
23776
|
+
highWater = highWater === void 0 ? record.sourceSeq : Math.max(highWater, record.sourceSeq);
|
|
23777
|
+
if (dead.has(record.sourceSeq)) {
|
|
23778
|
+
break;
|
|
23779
|
+
}
|
|
23780
|
+
enqueued.set(record.sourceSeq, {
|
|
23781
|
+
sourceSeq: record.sourceSeq,
|
|
23782
|
+
kind: record.kind,
|
|
23783
|
+
payload: record.payload,
|
|
23784
|
+
enqueuedAtMs: record.enqueuedAtMs,
|
|
23785
|
+
attempts: 0
|
|
23786
|
+
});
|
|
23787
|
+
break;
|
|
23788
|
+
}
|
|
23789
|
+
case "ack": {
|
|
23790
|
+
enqueued.delete(record.sourceSeq);
|
|
23791
|
+
highWater = highWater === void 0 ? record.sourceSeq : Math.max(highWater, record.sourceSeq);
|
|
23792
|
+
break;
|
|
23793
|
+
}
|
|
23794
|
+
case "dead": {
|
|
23795
|
+
enqueued.delete(record.sourceSeq);
|
|
23796
|
+
dead.add(record.sourceSeq);
|
|
23797
|
+
highWater = highWater === void 0 ? record.sourceSeq : Math.max(highWater, record.sourceSeq);
|
|
23798
|
+
break;
|
|
23799
|
+
}
|
|
23800
|
+
case "attempt": {
|
|
23801
|
+
const existing = enqueued.get(record.sourceSeq);
|
|
23802
|
+
if (existing !== void 0) {
|
|
23803
|
+
enqueued.set(record.sourceSeq, {
|
|
23804
|
+
...existing,
|
|
23805
|
+
attempts: record.attempts
|
|
23806
|
+
});
|
|
23807
|
+
}
|
|
23808
|
+
break;
|
|
23809
|
+
}
|
|
23810
|
+
}
|
|
23811
|
+
}
|
|
23812
|
+
const entries = [...enqueued.values()].sort(
|
|
23813
|
+
(a, b) => a.sourceSeq - b.sourceSeq
|
|
23814
|
+
);
|
|
23815
|
+
return { entries, highWaterSourceSeq: highWater, tornTail };
|
|
23816
|
+
}
|
|
23817
|
+
function shouldDeadLetter(entry, policy, nowMs) {
|
|
23818
|
+
if (policy.maxAttempts > 0 && entry.attempts >= policy.maxAttempts) {
|
|
23819
|
+
return true;
|
|
23820
|
+
}
|
|
23821
|
+
if (policy.maxAgeMs > 0 && nowMs - entry.enqueuedAtMs >= policy.maxAgeMs) {
|
|
23822
|
+
return true;
|
|
23823
|
+
}
|
|
23824
|
+
return false;
|
|
23825
|
+
}
|
|
23826
|
+
function createDurableInboundThreadQueue(path2, log2, deps = {}) {
|
|
23827
|
+
const fsyncFd = deps.fsyncFd ?? ((fd) => fsyncSync(fd));
|
|
23828
|
+
const fsyncDirFd = deps.fsyncDirFd ?? ((fd) => fsyncSync(fd));
|
|
23829
|
+
const folded = foldFile(path2, log2);
|
|
23830
|
+
const liveEntries = /* @__PURE__ */ new Map();
|
|
23831
|
+
for (const entry of folded.entries) {
|
|
23832
|
+
liveEntries.set(entry.sourceSeq, entry);
|
|
23833
|
+
}
|
|
23834
|
+
let highWater = folded.highWaterSourceSeq;
|
|
23835
|
+
let tornTail = folded.tornTail;
|
|
23836
|
+
const append = (record) => {
|
|
23837
|
+
try {
|
|
23838
|
+
mkdirSync13(dirname14(path2), { recursive: true });
|
|
23839
|
+
const fd = openSync4(path2, "a");
|
|
23840
|
+
try {
|
|
23841
|
+
writeSync2(fd, frameRecord(record), null, "utf8");
|
|
23842
|
+
fsyncFd(fd);
|
|
23843
|
+
} finally {
|
|
23844
|
+
closeSync3(fd);
|
|
23845
|
+
}
|
|
23846
|
+
} catch (error) {
|
|
23847
|
+
log2("inbound_queue.append_failed", {
|
|
23848
|
+
path: path2,
|
|
23849
|
+
op: record.op,
|
|
23850
|
+
sourceSeq: record.sourceSeq,
|
|
23851
|
+
message: error instanceof Error ? error.message : String(error)
|
|
23852
|
+
});
|
|
23853
|
+
}
|
|
23854
|
+
};
|
|
23855
|
+
const fsyncDir = () => {
|
|
23856
|
+
try {
|
|
23857
|
+
const dirFd = openSync4(dirname14(path2), "r");
|
|
23858
|
+
try {
|
|
23859
|
+
fsyncDirFd(dirFd);
|
|
23860
|
+
} finally {
|
|
23861
|
+
closeSync3(dirFd);
|
|
23862
|
+
}
|
|
23863
|
+
} catch {
|
|
23864
|
+
}
|
|
23865
|
+
};
|
|
23866
|
+
return {
|
|
23867
|
+
enqueue: (sourceSeq, kind, payload, nowMs) => {
|
|
23868
|
+
if (highWater !== void 0 && sourceSeq <= highWater) {
|
|
23869
|
+
return { accepted: false, reason: "already_seen" };
|
|
23870
|
+
}
|
|
23871
|
+
const entry = {
|
|
23872
|
+
sourceSeq,
|
|
23873
|
+
kind,
|
|
23874
|
+
payload,
|
|
23875
|
+
enqueuedAtMs: nowMs,
|
|
23876
|
+
attempts: 0
|
|
23877
|
+
};
|
|
23878
|
+
append({ op: "enqueue", sourceSeq, kind, payload, enqueuedAtMs: nowMs });
|
|
23879
|
+
liveEntries.set(sourceSeq, entry);
|
|
23880
|
+
highWater = sourceSeq;
|
|
23881
|
+
return { accepted: true, entry };
|
|
23882
|
+
},
|
|
23883
|
+
entries: () => [...liveEntries.values()].sort((a, b) => a.sourceSeq - b.sourceSeq),
|
|
23884
|
+
head: () => {
|
|
23885
|
+
let head;
|
|
23886
|
+
for (const entry of liveEntries.values()) {
|
|
23887
|
+
if (head === void 0 || entry.sourceSeq < head.sourceSeq) {
|
|
23888
|
+
head = entry;
|
|
23889
|
+
}
|
|
23890
|
+
}
|
|
23891
|
+
return head;
|
|
23892
|
+
},
|
|
23893
|
+
recordAttempt: (sourceSeq) => {
|
|
23894
|
+
const existing = liveEntries.get(sourceSeq);
|
|
23895
|
+
if (existing === void 0) {
|
|
23896
|
+
return 0;
|
|
23897
|
+
}
|
|
23898
|
+
const attempts = existing.attempts + 1;
|
|
23899
|
+
liveEntries.set(sourceSeq, { ...existing, attempts });
|
|
23900
|
+
append({ op: "attempt", sourceSeq, attempts });
|
|
23901
|
+
return attempts;
|
|
23902
|
+
},
|
|
23903
|
+
ack: (sourceSeq) => {
|
|
23904
|
+
if (!liveEntries.has(sourceSeq)) {
|
|
23905
|
+
return;
|
|
23906
|
+
}
|
|
23907
|
+
append({ op: "ack", sourceSeq });
|
|
23908
|
+
liveEntries.delete(sourceSeq);
|
|
23909
|
+
},
|
|
23910
|
+
deadLetter: (sourceSeq) => {
|
|
23911
|
+
const entry = liveEntries.get(sourceSeq);
|
|
23912
|
+
if (entry === void 0) {
|
|
23913
|
+
return void 0;
|
|
23914
|
+
}
|
|
23915
|
+
append({ op: "dead", sourceSeq });
|
|
23916
|
+
liveEntries.delete(sourceSeq);
|
|
23917
|
+
log2("inbound_queue.dead_lettered", {
|
|
23918
|
+
path: path2,
|
|
23919
|
+
sourceSeq,
|
|
23920
|
+
attempts: entry.attempts,
|
|
23921
|
+
ageMs: Date.now() - entry.enqueuedAtMs
|
|
23922
|
+
});
|
|
23923
|
+
return entry;
|
|
23924
|
+
},
|
|
23925
|
+
highWaterSourceSeq: () => highWater,
|
|
23926
|
+
tornTail: () => tornTail,
|
|
23927
|
+
reconciledTornTail: () => {
|
|
23928
|
+
tornTail = false;
|
|
23929
|
+
},
|
|
23930
|
+
compact: () => {
|
|
23931
|
+
const records = [...liveEntries.values()].sort((a, b) => a.sourceSeq - b.sourceSeq).flatMap((entry) => {
|
|
23932
|
+
const out = [
|
|
23933
|
+
{
|
|
23934
|
+
op: "enqueue",
|
|
23935
|
+
sourceSeq: entry.sourceSeq,
|
|
23936
|
+
kind: entry.kind,
|
|
23937
|
+
payload: entry.payload,
|
|
23938
|
+
enqueuedAtMs: entry.enqueuedAtMs
|
|
23939
|
+
}
|
|
23940
|
+
];
|
|
23941
|
+
if (entry.attempts > 0) {
|
|
23942
|
+
out.push({
|
|
23943
|
+
op: "attempt",
|
|
23944
|
+
sourceSeq: entry.sourceSeq,
|
|
23945
|
+
attempts: entry.attempts
|
|
23946
|
+
});
|
|
23947
|
+
}
|
|
23948
|
+
return out;
|
|
23949
|
+
});
|
|
23950
|
+
if (highWater !== void 0 && !liveEntries.has(highWater)) {
|
|
23951
|
+
records.push({ op: "ack", sourceSeq: highWater });
|
|
23952
|
+
}
|
|
23953
|
+
try {
|
|
23954
|
+
mkdirSync13(dirname14(path2), { recursive: true });
|
|
23955
|
+
const tmpPath = `${path2}.tmp`;
|
|
23956
|
+
const tmpFd = openSync4(tmpPath, "w");
|
|
23957
|
+
try {
|
|
23958
|
+
writeSync2(tmpFd, records.map(frameRecord).join(""), null, "utf8");
|
|
23959
|
+
fsyncFd(tmpFd);
|
|
23960
|
+
} finally {
|
|
23961
|
+
closeSync3(tmpFd);
|
|
23962
|
+
}
|
|
23963
|
+
renameSync4(tmpPath, path2);
|
|
23964
|
+
fsyncDir();
|
|
23965
|
+
} catch (error) {
|
|
23966
|
+
log2("inbound_queue.compact_failed", {
|
|
23967
|
+
path: path2,
|
|
23968
|
+
message: error instanceof Error ? error.message : String(error)
|
|
23969
|
+
});
|
|
23970
|
+
}
|
|
23971
|
+
}
|
|
23972
|
+
};
|
|
23973
|
+
}
|
|
23974
|
+
function foldFile(path2, log2) {
|
|
23975
|
+
let raw;
|
|
23976
|
+
try {
|
|
23977
|
+
raw = readFileSync17(path2, "utf8");
|
|
23978
|
+
} catch (error) {
|
|
23979
|
+
if (error?.code === "ENOENT") {
|
|
23980
|
+
return { entries: [], highWaterSourceSeq: void 0, tornTail: false };
|
|
23981
|
+
}
|
|
23982
|
+
log2("inbound_queue.read_failed", {
|
|
23983
|
+
path: path2,
|
|
23984
|
+
message: error instanceof Error ? error.message : String(error)
|
|
23985
|
+
});
|
|
23986
|
+
return { entries: [], highWaterSourceSeq: void 0, tornTail: false };
|
|
23987
|
+
}
|
|
23988
|
+
const folded = foldQueueRecords(raw.split("\n"));
|
|
23989
|
+
if (folded.tornTail) {
|
|
23990
|
+
log2("inbound_queue.torn_tail_detected", {
|
|
23991
|
+
path: path2,
|
|
23992
|
+
liveEntries: folded.entries.length,
|
|
23993
|
+
highWaterSourceSeq: folded.highWaterSourceSeq ?? null
|
|
23994
|
+
});
|
|
23995
|
+
}
|
|
23996
|
+
return folded;
|
|
23997
|
+
}
|
|
23998
|
+
function durableInboundQueueDir() {
|
|
23999
|
+
const override = process.env.LINZUMI_INBOUND_QUEUE_DIR?.trim();
|
|
24000
|
+
if (override !== void 0 && override !== "") {
|
|
24001
|
+
return override;
|
|
24002
|
+
}
|
|
24003
|
+
return join21(homedir14(), ".linzumi", "inbound-queue");
|
|
24004
|
+
}
|
|
24005
|
+
function durableInboundQueuePath(workspace, channel, threadId) {
|
|
24006
|
+
const composite = `${workspace}:${channel}:${threadId}`;
|
|
24007
|
+
const sanitized = composite.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
|
|
24008
|
+
const digest = createHash6("sha256").update(composite).digest("hex").slice(0, 12);
|
|
24009
|
+
const stem = sanitized === "" ? "thread" : sanitized;
|
|
24010
|
+
return join21(durableInboundQueueDir(), `${stem}.${digest}.log`);
|
|
24011
|
+
}
|
|
24012
|
+
var defaultDeadLetterPolicy;
|
|
24013
|
+
var init_durableInboundQueue = __esm({
|
|
24014
|
+
"src/durableInboundQueue.ts"() {
|
|
24015
|
+
"use strict";
|
|
24016
|
+
init_protocol();
|
|
24017
|
+
defaultDeadLetterPolicy = {
|
|
24018
|
+
maxAttempts: 8,
|
|
24019
|
+
maxAgeMs: 30 * 60 * 1e3
|
|
24020
|
+
};
|
|
24021
|
+
}
|
|
24022
|
+
});
|
|
24023
|
+
|
|
24024
|
+
// src/appliedSourceSeqWatermark.ts
|
|
24025
|
+
function threadSourceSeqWatermarkKey(workspace, channel, threadId) {
|
|
24026
|
+
return `${workspace} ${channel} ${threadId}`;
|
|
24027
|
+
}
|
|
24028
|
+
function shouldApplySourceSeq(appliedSourceSeq, sourceSeq) {
|
|
24029
|
+
return appliedSourceSeq === void 0 || sourceSeq > appliedSourceSeq;
|
|
24030
|
+
}
|
|
24031
|
+
function commitAppliedSourceSeq(store, key, sourceSeq) {
|
|
24032
|
+
store.onAdvance(key, sourceSeq);
|
|
24033
|
+
store.flush();
|
|
24034
|
+
}
|
|
24035
|
+
function highestProcessedSourceSeq(rows) {
|
|
24036
|
+
let highest;
|
|
24037
|
+
for (const row of rows) {
|
|
24038
|
+
if (row.status === "processed" || row.status === "failed") {
|
|
24039
|
+
highest = highest === void 0 ? row.sourceSeq : Math.max(highest, row.sourceSeq);
|
|
24040
|
+
}
|
|
24041
|
+
}
|
|
24042
|
+
return highest;
|
|
24043
|
+
}
|
|
24044
|
+
function reconcileAppliedSourceSeq(localWatermark, serverRows) {
|
|
24045
|
+
const serverHigh = highestProcessedSourceSeq(serverRows);
|
|
24046
|
+
if (localWatermark === void 0) {
|
|
24047
|
+
return serverHigh;
|
|
24048
|
+
}
|
|
24049
|
+
if (serverHigh === void 0) {
|
|
24050
|
+
return localWatermark;
|
|
24051
|
+
}
|
|
24052
|
+
return Math.max(localWatermark, serverHigh);
|
|
24053
|
+
}
|
|
24054
|
+
function parseResumeReconcileEntries(value) {
|
|
24055
|
+
if (!Array.isArray(value)) {
|
|
24056
|
+
return [];
|
|
24057
|
+
}
|
|
24058
|
+
const entries = [];
|
|
24059
|
+
for (const item of value) {
|
|
24060
|
+
if (!isJsonObject(item)) {
|
|
24061
|
+
continue;
|
|
24062
|
+
}
|
|
24063
|
+
const threadId = item.threadId;
|
|
24064
|
+
if (typeof threadId !== "string") {
|
|
24065
|
+
continue;
|
|
24066
|
+
}
|
|
24067
|
+
const rawChannelId = item.channelId;
|
|
24068
|
+
const channelId = typeof rawChannelId === "string" ? rawChannelId : typeof rawChannelId === "number" ? String(rawChannelId) : "";
|
|
24069
|
+
const highest = item.highestProcessedSourceSeq;
|
|
24070
|
+
const leaseEpoch = item.leaseEpoch;
|
|
24071
|
+
entries.push({
|
|
24072
|
+
channelId,
|
|
24073
|
+
threadId,
|
|
24074
|
+
highestProcessedSourceSeq: typeof highest === "number" && Number.isInteger(highest) ? highest : void 0,
|
|
24075
|
+
leaseEpoch: typeof leaseEpoch === "number" && Number.isInteger(leaseEpoch) ? leaseEpoch : void 0
|
|
24076
|
+
});
|
|
24077
|
+
}
|
|
24078
|
+
return entries;
|
|
24079
|
+
}
|
|
24080
|
+
function resumeReconcileServerRows(entry) {
|
|
24081
|
+
if (entry.highestProcessedSourceSeq === void 0) {
|
|
24082
|
+
return [];
|
|
24083
|
+
}
|
|
24084
|
+
return [{ sourceSeq: entry.highestProcessedSourceSeq, status: "processed" }];
|
|
24085
|
+
}
|
|
24086
|
+
var init_appliedSourceSeqWatermark = __esm({
|
|
24087
|
+
"src/appliedSourceSeqWatermark.ts"() {
|
|
24088
|
+
"use strict";
|
|
24089
|
+
init_protocol();
|
|
24090
|
+
}
|
|
24091
|
+
});
|
|
24092
|
+
|
|
24093
|
+
// src/threadReconcilerLock.ts
|
|
24094
|
+
function createThreadReconcilerLock() {
|
|
24095
|
+
const tails = /* @__PURE__ */ new Map();
|
|
24096
|
+
const depth = /* @__PURE__ */ new Map();
|
|
24097
|
+
const runExclusive = (threadId, fn, deadlineMs) => {
|
|
24098
|
+
const prior = tails.get(threadId) ?? Promise.resolve();
|
|
24099
|
+
depth.set(threadId, (depth.get(threadId) ?? 0) + 1);
|
|
24100
|
+
const run = prior.catch(() => void 0).then(() => fn());
|
|
24101
|
+
let caller;
|
|
24102
|
+
if (deadlineMs === void 0 || deadlineMs <= 0) {
|
|
24103
|
+
caller = run;
|
|
24104
|
+
} else {
|
|
24105
|
+
caller = new Promise((resolve12, reject) => {
|
|
24106
|
+
const timer = setTimeout(() => {
|
|
24107
|
+
reject(new ReconcilerLockTimeoutError(threadId, deadlineMs));
|
|
24108
|
+
}, deadlineMs);
|
|
24109
|
+
timer.unref?.();
|
|
24110
|
+
run.then(
|
|
24111
|
+
(value) => {
|
|
24112
|
+
clearTimeout(timer);
|
|
24113
|
+
resolve12(value);
|
|
24114
|
+
},
|
|
24115
|
+
(error) => {
|
|
24116
|
+
clearTimeout(timer);
|
|
24117
|
+
reject(error);
|
|
24118
|
+
}
|
|
24119
|
+
);
|
|
24120
|
+
});
|
|
24121
|
+
}
|
|
24122
|
+
const settled = run.then(
|
|
24123
|
+
() => void 0,
|
|
24124
|
+
() => void 0
|
|
24125
|
+
);
|
|
24126
|
+
tails.set(threadId, settled);
|
|
24127
|
+
void settled.then(() => {
|
|
24128
|
+
const remaining = (depth.get(threadId) ?? 1) - 1;
|
|
24129
|
+
if (remaining <= 0) {
|
|
24130
|
+
depth.delete(threadId);
|
|
24131
|
+
if (tails.get(threadId) === settled) {
|
|
24132
|
+
tails.delete(threadId);
|
|
24133
|
+
}
|
|
24134
|
+
} else {
|
|
24135
|
+
depth.set(threadId, remaining);
|
|
24136
|
+
}
|
|
24137
|
+
});
|
|
24138
|
+
return caller;
|
|
24139
|
+
};
|
|
24140
|
+
return {
|
|
24141
|
+
runExclusive,
|
|
24142
|
+
isBusy: (threadId) => (depth.get(threadId) ?? 0) > 0,
|
|
24143
|
+
activeThreadCount: () => tails.size
|
|
24144
|
+
};
|
|
24145
|
+
}
|
|
24146
|
+
var ReconcilerLockTimeoutError;
|
|
24147
|
+
var init_threadReconcilerLock = __esm({
|
|
24148
|
+
"src/threadReconcilerLock.ts"() {
|
|
24149
|
+
"use strict";
|
|
24150
|
+
ReconcilerLockTimeoutError = class extends Error {
|
|
24151
|
+
threadId;
|
|
24152
|
+
timeoutMs;
|
|
24153
|
+
constructor(threadId, timeoutMs) {
|
|
24154
|
+
super(
|
|
24155
|
+
`reconciler section for thread ${threadId} exceeded ${timeoutMs}ms`
|
|
24156
|
+
);
|
|
24157
|
+
this.name = "ReconcilerLockTimeoutError";
|
|
24158
|
+
this.threadId = threadId;
|
|
24159
|
+
this.timeoutMs = timeoutMs;
|
|
24160
|
+
}
|
|
24161
|
+
};
|
|
24162
|
+
}
|
|
24163
|
+
});
|
|
24164
|
+
|
|
24165
|
+
// src/runnerLifecycleGuards.ts
|
|
24166
|
+
async function runDrain(steps, deadlineMs, log2) {
|
|
24167
|
+
let timedOut = false;
|
|
24168
|
+
const deadline = new Promise((resolve12) => {
|
|
24169
|
+
const timer = setTimeout(() => resolve12("timeout"), deadlineMs);
|
|
24170
|
+
timer.unref?.();
|
|
24171
|
+
});
|
|
24172
|
+
const ordered = (async () => {
|
|
24173
|
+
await steps.stopAccepting();
|
|
24174
|
+
await steps.checkpointCurrentTurn();
|
|
24175
|
+
await steps.flushInboundQueues();
|
|
24176
|
+
await steps.flushOutbox();
|
|
24177
|
+
return "done";
|
|
24178
|
+
})();
|
|
24179
|
+
const outcome = await Promise.race([
|
|
24180
|
+
ordered.then(() => "done"),
|
|
24181
|
+
deadline
|
|
24182
|
+
]);
|
|
24183
|
+
if (outcome === "timeout") {
|
|
24184
|
+
timedOut = true;
|
|
24185
|
+
log2("runner.drain_timed_out", { deadlineMs });
|
|
24186
|
+
} else {
|
|
24187
|
+
log2("runner.drain_complete", {});
|
|
24188
|
+
}
|
|
24189
|
+
return { drained: outcome === "done", timedOut };
|
|
24190
|
+
}
|
|
24191
|
+
function installDrainOnSigterm(args) {
|
|
24192
|
+
const exit = args.exit ?? ((code) => process.exit(code));
|
|
24193
|
+
const signals2 = args.signals ?? ["SIGTERM", "SIGINT"];
|
|
24194
|
+
let draining = false;
|
|
24195
|
+
const onSignal = (signal) => {
|
|
24196
|
+
if (draining) {
|
|
24197
|
+
args.log("runner.drain_signal_ignored", { signal });
|
|
24198
|
+
return;
|
|
24199
|
+
}
|
|
24200
|
+
draining = true;
|
|
24201
|
+
args.log("runner.drain_started", { signal });
|
|
24202
|
+
void runDrain(args.steps, args.deadlineMs, args.log).catch((error) => {
|
|
24203
|
+
args.log("runner.drain_failed", {
|
|
24204
|
+
message: error instanceof Error ? error.message : String(error)
|
|
24205
|
+
});
|
|
24206
|
+
return { drained: false, timedOut: false };
|
|
24207
|
+
}).finally(() => exit(0));
|
|
24208
|
+
};
|
|
24209
|
+
const handlers = signals2.map((signal) => {
|
|
24210
|
+
const handler = () => onSignal(signal);
|
|
24211
|
+
process.on(signal, handler);
|
|
24212
|
+
return { signal, handler };
|
|
24213
|
+
});
|
|
24214
|
+
return () => {
|
|
24215
|
+
for (const { signal, handler } of handlers) {
|
|
24216
|
+
process.off(signal, handler);
|
|
24217
|
+
}
|
|
24218
|
+
};
|
|
24219
|
+
}
|
|
24220
|
+
var init_runnerLifecycleGuards = __esm({
|
|
24221
|
+
"src/runnerLifecycleGuards.ts"() {
|
|
24222
|
+
"use strict";
|
|
24223
|
+
}
|
|
24224
|
+
});
|
|
24225
|
+
|
|
24226
|
+
// src/leaseEpochGuard.ts
|
|
24227
|
+
function createLeaseEpochRegistry() {
|
|
24228
|
+
const epochs = /* @__PURE__ */ new Map();
|
|
24229
|
+
return {
|
|
24230
|
+
current: (threadId) => epochs.get(threadId),
|
|
24231
|
+
adopt: (threadId, leaseEpoch) => {
|
|
24232
|
+
if (!Number.isInteger(leaseEpoch)) {
|
|
24233
|
+
return false;
|
|
24234
|
+
}
|
|
24235
|
+
const existing = epochs.get(threadId);
|
|
24236
|
+
if (existing !== void 0 && leaseEpoch < existing) {
|
|
24237
|
+
return false;
|
|
24238
|
+
}
|
|
24239
|
+
if (existing === leaseEpoch) {
|
|
24240
|
+
return false;
|
|
24241
|
+
}
|
|
24242
|
+
epochs.set(threadId, leaseEpoch);
|
|
24243
|
+
return true;
|
|
24244
|
+
},
|
|
24245
|
+
release: (threadId) => {
|
|
24246
|
+
epochs.delete(threadId);
|
|
24247
|
+
},
|
|
24248
|
+
snapshot: () => [...epochs.entries()].map(([threadId, leaseEpoch]) => ({
|
|
24249
|
+
threadId,
|
|
24250
|
+
leaseEpoch
|
|
24251
|
+
}))
|
|
24252
|
+
};
|
|
24253
|
+
}
|
|
24254
|
+
function isFencedClaim(heldEpoch, controlEpoch) {
|
|
24255
|
+
if (controlEpoch === void 0) {
|
|
24256
|
+
return false;
|
|
24257
|
+
}
|
|
24258
|
+
if (heldEpoch === void 0) {
|
|
24259
|
+
return false;
|
|
24260
|
+
}
|
|
24261
|
+
return controlEpoch < heldEpoch;
|
|
24262
|
+
}
|
|
24263
|
+
function readLeaseEpoch(value) {
|
|
24264
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
24265
|
+
return void 0;
|
|
24266
|
+
}
|
|
24267
|
+
const epoch = value.lease_epoch;
|
|
24268
|
+
return typeof epoch === "number" && Number.isInteger(epoch) ? epoch : void 0;
|
|
24269
|
+
}
|
|
24270
|
+
var init_leaseEpochGuard = __esm({
|
|
24271
|
+
"src/leaseEpochGuard.ts"() {
|
|
24272
|
+
"use strict";
|
|
24273
|
+
}
|
|
24274
|
+
});
|
|
24275
|
+
|
|
24276
|
+
// src/resumeKeystone.ts
|
|
24277
|
+
function commitLiveAcceptedSourceSeq(args) {
|
|
24278
|
+
const applied = args.store.initial(args.watermarkKey);
|
|
24279
|
+
if (!shouldApplySourceSeq(applied, args.sourceSeq)) {
|
|
24280
|
+
return false;
|
|
24281
|
+
}
|
|
24282
|
+
commitAppliedSourceSeq(args.store, args.watermarkKey, args.sourceSeq);
|
|
24283
|
+
return true;
|
|
24284
|
+
}
|
|
24285
|
+
async function applyResumeDispatchOutcome(args) {
|
|
24286
|
+
const policy = args.policy ?? defaultDeadLetterPolicy;
|
|
24287
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
24288
|
+
if (args.outcome === "dispatched") {
|
|
24289
|
+
commitAppliedSourceSeq(args.store, args.watermarkKey, args.sourceSeq);
|
|
24290
|
+
args.queue.ack(args.sourceSeq);
|
|
24291
|
+
return;
|
|
24292
|
+
}
|
|
24293
|
+
if (args.outcome === "ignored") {
|
|
24294
|
+
args.queue.ack(args.sourceSeq);
|
|
24295
|
+
return;
|
|
24296
|
+
}
|
|
24297
|
+
if (args.outcome === "permanent_failed") {
|
|
24298
|
+
const attempts2 = findEntryAttempts(args.queue, args.sourceSeq);
|
|
24299
|
+
args.queue.deadLetter(args.sourceSeq);
|
|
24300
|
+
await args.onDeadLetter?.({
|
|
24301
|
+
threadId: args.threadId,
|
|
24302
|
+
sourceSeq: args.sourceSeq,
|
|
24303
|
+
reason: "permanent",
|
|
24304
|
+
attempts: attempts2,
|
|
24305
|
+
alreadyPublished: true
|
|
24306
|
+
});
|
|
24307
|
+
return;
|
|
24308
|
+
}
|
|
24309
|
+
const attempts = args.queue.recordAttempt(args.sourceSeq);
|
|
24310
|
+
const entry = args.queue.entries().find((candidate) => candidate.sourceSeq === args.sourceSeq);
|
|
24311
|
+
if (entry !== void 0 && shouldDeadLetter(entry, policy, nowMs)) {
|
|
24312
|
+
args.log("resume_keystone.dead_lettered", {
|
|
24313
|
+
threadId: args.threadId,
|
|
24314
|
+
sourceSeq: args.sourceSeq,
|
|
24315
|
+
attempts
|
|
24316
|
+
});
|
|
24317
|
+
args.queue.deadLetter(args.sourceSeq);
|
|
24318
|
+
await args.onDeadLetter?.({
|
|
24319
|
+
threadId: args.threadId,
|
|
24320
|
+
sourceSeq: args.sourceSeq,
|
|
24321
|
+
reason: "exhausted",
|
|
24322
|
+
attempts,
|
|
24323
|
+
alreadyPublished: false
|
|
24324
|
+
});
|
|
24325
|
+
}
|
|
24326
|
+
}
|
|
24327
|
+
function findEntryAttempts(queue, sourceSeq) {
|
|
24328
|
+
return queue.entries().find((candidate) => candidate.sourceSeq === sourceSeq)?.attempts ?? 0;
|
|
24329
|
+
}
|
|
24330
|
+
async function drainQueueHeadInOrder(args) {
|
|
24331
|
+
const dispatched = [];
|
|
24332
|
+
let headOutcome = "ignored";
|
|
24333
|
+
let firstOutcomeRecorded = false;
|
|
24334
|
+
let guard = args.queue.entries().length + 1;
|
|
24335
|
+
while (guard > 0) {
|
|
24336
|
+
guard -= 1;
|
|
24337
|
+
const head = args.queue.head();
|
|
24338
|
+
if (head === void 0) {
|
|
24339
|
+
break;
|
|
24340
|
+
}
|
|
24341
|
+
const applied = args.store.initial(args.watermarkKey);
|
|
24342
|
+
if (!shouldApplySourceSeq(applied, head.sourceSeq)) {
|
|
24343
|
+
args.queue.ack(head.sourceSeq);
|
|
24344
|
+
continue;
|
|
24345
|
+
}
|
|
24346
|
+
if (args.onlyDispatchUnattempted && args.serverRows.length === 0 && head.attempts > 0) {
|
|
24347
|
+
args.log("resume_keystone.attempted_entry_withheld", {
|
|
24348
|
+
threadId: args.threadId,
|
|
24349
|
+
sourceSeq: head.sourceSeq,
|
|
24350
|
+
attempts: head.attempts
|
|
24351
|
+
});
|
|
24352
|
+
args.queue.deadLetter(head.sourceSeq);
|
|
24353
|
+
await args.onDeadLetter?.({
|
|
24354
|
+
threadId: args.threadId,
|
|
24355
|
+
sourceSeq: head.sourceSeq,
|
|
24356
|
+
reason: "withheld",
|
|
24357
|
+
attempts: head.attempts,
|
|
24358
|
+
alreadyPublished: false
|
|
24359
|
+
});
|
|
24360
|
+
continue;
|
|
24361
|
+
}
|
|
24362
|
+
const outcome = await args.dispatch(head.sourceSeq, head.payload);
|
|
24363
|
+
await applyResumeDispatchOutcome({
|
|
24364
|
+
queue: args.queue,
|
|
24365
|
+
store: args.store,
|
|
24366
|
+
watermarkKey: args.watermarkKey,
|
|
24367
|
+
sourceSeq: head.sourceSeq,
|
|
24368
|
+
threadId: args.threadId,
|
|
24369
|
+
outcome,
|
|
24370
|
+
log: args.log,
|
|
24371
|
+
policy: args.policy,
|
|
24372
|
+
nowMs: args.nowMs,
|
|
24373
|
+
onDeadLetter: args.onDeadLetter
|
|
24374
|
+
});
|
|
24375
|
+
if (!firstOutcomeRecorded) {
|
|
24376
|
+
headOutcome = outcome;
|
|
24377
|
+
firstOutcomeRecorded = true;
|
|
24378
|
+
}
|
|
24379
|
+
if (outcome === "dispatched") {
|
|
24380
|
+
dispatched.push(head.sourceSeq);
|
|
24381
|
+
}
|
|
24382
|
+
if (outcome === "transient_failed") {
|
|
24383
|
+
break;
|
|
24384
|
+
}
|
|
24385
|
+
}
|
|
24386
|
+
return { dispatched, headOutcome };
|
|
24387
|
+
}
|
|
24388
|
+
async function receiveResumeMessage(args) {
|
|
24389
|
+
args.queue.enqueue(
|
|
24390
|
+
args.sourceSeq,
|
|
24391
|
+
args.kind,
|
|
24392
|
+
args.payload,
|
|
24393
|
+
args.nowMs ?? Date.now()
|
|
24394
|
+
);
|
|
24395
|
+
const heldEpoch = args.leases.current(args.threadId);
|
|
24396
|
+
if (isFencedClaim(heldEpoch, args.leaseEpoch)) {
|
|
24397
|
+
args.log("resume_keystone.fenced", {
|
|
24398
|
+
threadId: args.threadId,
|
|
24399
|
+
heldLeaseEpoch: heldEpoch ?? null,
|
|
24400
|
+
controlLeaseEpoch: args.leaseEpoch ?? null,
|
|
24401
|
+
sourceSeq: args.sourceSeq
|
|
24402
|
+
});
|
|
24403
|
+
args.queue.ack(args.sourceSeq);
|
|
24404
|
+
return "ignored";
|
|
24405
|
+
}
|
|
24406
|
+
if (args.leaseEpoch !== void 0) {
|
|
24407
|
+
args.leases.adopt(args.threadId, args.leaseEpoch);
|
|
24408
|
+
}
|
|
24409
|
+
const serverRows = args.serverRows ?? [];
|
|
24410
|
+
if (serverRows.length > 0) {
|
|
24411
|
+
const localWatermark = args.store.initial(args.watermarkKey);
|
|
24412
|
+
const reconciled = reconcileAppliedSourceSeq(localWatermark, serverRows);
|
|
24413
|
+
if (reconciled !== void 0 && reconciled !== localWatermark) {
|
|
24414
|
+
commitAppliedSourceSeq(args.store, args.watermarkKey, reconciled);
|
|
24415
|
+
args.log("resume_keystone.live_miss_reconciled", {
|
|
24416
|
+
threadId: args.threadId,
|
|
24417
|
+
reconciledWatermark: reconciled,
|
|
24418
|
+
sourceSeq: args.sourceSeq
|
|
24419
|
+
});
|
|
24420
|
+
}
|
|
24421
|
+
}
|
|
24422
|
+
const applied = args.store.initial(args.watermarkKey);
|
|
24423
|
+
if (!shouldApplySourceSeq(applied, args.sourceSeq)) {
|
|
24424
|
+
args.log("resume_keystone.already_applied", {
|
|
24425
|
+
threadId: args.threadId,
|
|
24426
|
+
sourceSeq: args.sourceSeq,
|
|
24427
|
+
appliedSourceSeq: applied ?? null
|
|
24428
|
+
});
|
|
24429
|
+
args.queue.ack(args.sourceSeq);
|
|
24430
|
+
return "ignored";
|
|
24431
|
+
}
|
|
24432
|
+
const deadlineMs = args.reconcilerDeadlineMs ?? defaultResumeReconcilerDeadlineMs;
|
|
24433
|
+
try {
|
|
24434
|
+
const result = await args.lock.runExclusive(
|
|
24435
|
+
args.threadId,
|
|
24436
|
+
() => drainQueueHeadInOrder({
|
|
24437
|
+
queue: args.queue,
|
|
24438
|
+
store: args.store,
|
|
24439
|
+
watermarkKey: args.watermarkKey,
|
|
24440
|
+
threadId: args.threadId,
|
|
24441
|
+
serverRows,
|
|
24442
|
+
dispatch: args.dispatch,
|
|
24443
|
+
log: args.log,
|
|
24444
|
+
nowMs: args.nowMs,
|
|
24445
|
+
policy: args.policy,
|
|
24446
|
+
onlyDispatchUnattempted: args.onlyDispatchUnattempted ?? false,
|
|
24447
|
+
onDeadLetter: args.onDeadLetter
|
|
24448
|
+
}),
|
|
24449
|
+
deadlineMs
|
|
24450
|
+
);
|
|
24451
|
+
return result.headOutcome;
|
|
24452
|
+
} catch (error) {
|
|
24453
|
+
if (error instanceof ReconcilerLockTimeoutError) {
|
|
24454
|
+
args.log("resume_keystone.spawn_deadline_exceeded", {
|
|
24455
|
+
threadId: args.threadId,
|
|
24456
|
+
sourceSeq: args.sourceSeq,
|
|
24457
|
+
deadlineMs
|
|
24458
|
+
});
|
|
24459
|
+
return "transient_failed";
|
|
24460
|
+
}
|
|
24461
|
+
throw error;
|
|
24462
|
+
}
|
|
24463
|
+
}
|
|
24464
|
+
async function drainResumeQueue(args) {
|
|
24465
|
+
let dispatched = [];
|
|
24466
|
+
const serverRows = args.serverRows ?? [];
|
|
24467
|
+
const deadlineMs = args.reconcilerDeadlineMs ?? defaultResumeReconcilerDeadlineMs;
|
|
24468
|
+
const runDrain2 = async () => {
|
|
24469
|
+
const localWatermark = args.store.initial(args.watermarkKey);
|
|
24470
|
+
const reconciled = reconcileAppliedSourceSeq(localWatermark, serverRows);
|
|
24471
|
+
if (reconciled !== void 0 && reconciled !== localWatermark) {
|
|
24472
|
+
commitAppliedSourceSeq(args.store, args.watermarkKey, reconciled);
|
|
24473
|
+
}
|
|
24474
|
+
if (args.queue.tornTail()) {
|
|
24475
|
+
if (serverRows.length > 0) {
|
|
24476
|
+
args.log("resume_keystone.torn_tail", {
|
|
24477
|
+
threadId: args.threadId,
|
|
24478
|
+
reconciledWatermark: reconciled ?? null,
|
|
24479
|
+
recovery: "server_reconcile"
|
|
24480
|
+
});
|
|
24481
|
+
args.queue.reconciledTornTail();
|
|
24482
|
+
} else {
|
|
24483
|
+
args.log("resume_keystone.torn_tail_unrecoverable", {
|
|
24484
|
+
threadId: args.threadId,
|
|
24485
|
+
reconciledWatermark: reconciled ?? null
|
|
24486
|
+
});
|
|
24487
|
+
if (args.onTornTailUnrecoverable !== void 0) {
|
|
24488
|
+
await args.onTornTailUnrecoverable(args.threadId);
|
|
24489
|
+
}
|
|
24490
|
+
args.queue.reconciledTornTail();
|
|
24491
|
+
}
|
|
24492
|
+
}
|
|
24493
|
+
const result = await drainQueueHeadInOrder({
|
|
24494
|
+
queue: args.queue,
|
|
24495
|
+
store: args.store,
|
|
24496
|
+
watermarkKey: args.watermarkKey,
|
|
24497
|
+
threadId: args.threadId,
|
|
24498
|
+
serverRows,
|
|
24499
|
+
dispatch: args.dispatch,
|
|
24500
|
+
log: args.log,
|
|
24501
|
+
nowMs: args.nowMs,
|
|
24502
|
+
policy: args.policy,
|
|
24503
|
+
onlyDispatchUnattempted: args.onlyDispatchUnattempted ?? false,
|
|
24504
|
+
onDeadLetter: args.onDeadLetter
|
|
24505
|
+
});
|
|
24506
|
+
dispatched = result.dispatched;
|
|
24507
|
+
};
|
|
24508
|
+
try {
|
|
24509
|
+
await args.lock.runExclusive(args.threadId, runDrain2, deadlineMs);
|
|
24510
|
+
} catch (error) {
|
|
24511
|
+
if (error instanceof ReconcilerLockTimeoutError) {
|
|
24512
|
+
args.log("resume_keystone.drain_deadline_exceeded", {
|
|
24513
|
+
threadId: args.threadId,
|
|
24514
|
+
deadlineMs
|
|
24515
|
+
});
|
|
24516
|
+
return dispatched;
|
|
24517
|
+
}
|
|
24518
|
+
throw error;
|
|
24519
|
+
}
|
|
24520
|
+
return dispatched;
|
|
24521
|
+
}
|
|
24522
|
+
var defaultResumeReconcilerDeadlineMs;
|
|
24523
|
+
var init_resumeKeystone = __esm({
|
|
24524
|
+
"src/resumeKeystone.ts"() {
|
|
24525
|
+
"use strict";
|
|
24526
|
+
init_durableInboundQueue();
|
|
24527
|
+
init_appliedSourceSeqWatermark();
|
|
24528
|
+
init_leaseEpochGuard();
|
|
24529
|
+
init_threadReconcilerLock();
|
|
24530
|
+
defaultResumeReconcilerDeadlineMs = 12e4;
|
|
24531
|
+
}
|
|
24532
|
+
});
|
|
24533
|
+
|
|
23560
24534
|
// src/runner.ts
|
|
23561
24535
|
import { spawn as spawn9, spawnSync as spawnSync5 } from "node:child_process";
|
|
23562
|
-
import { createHash as
|
|
24536
|
+
import { createHash as createHash7, randomUUID as randomUUID4 } from "node:crypto";
|
|
23563
24537
|
import {
|
|
23564
24538
|
chmodSync as chmodSync2,
|
|
24539
|
+
closeSync as closeSync4,
|
|
23565
24540
|
existsSync as existsSync13,
|
|
24541
|
+
fsyncSync as fsyncSync2,
|
|
23566
24542
|
lstatSync,
|
|
23567
|
-
mkdirSync as
|
|
24543
|
+
mkdirSync as mkdirSync14,
|
|
23568
24544
|
mkdtempSync as mkdtempSync4,
|
|
24545
|
+
openSync as openSync5,
|
|
23569
24546
|
readdirSync as readdirSync4,
|
|
23570
|
-
readFileSync as
|
|
24547
|
+
readFileSync as readFileSync18,
|
|
23571
24548
|
realpathSync as realpathSync6,
|
|
23572
|
-
renameSync as
|
|
24549
|
+
renameSync as renameSync5,
|
|
23573
24550
|
rmSync as rmSync5,
|
|
23574
24551
|
statSync as statSync3,
|
|
23575
24552
|
writeFileSync as writeFileSync12
|
|
23576
24553
|
} from "node:fs";
|
|
23577
24554
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
23578
24555
|
import { createServer as createServer3 } from "node:http";
|
|
23579
|
-
import { homedir as
|
|
24556
|
+
import { homedir as homedir15, hostname as hostname2, tmpdir as tmpdir3 } from "node:os";
|
|
23580
24557
|
import { createInterface } from "node:readline";
|
|
23581
24558
|
import {
|
|
23582
24559
|
basename as basename8,
|
|
23583
|
-
dirname as
|
|
24560
|
+
dirname as dirname15,
|
|
23584
24561
|
extname as extname2,
|
|
23585
24562
|
isAbsolute as isAbsolute5,
|
|
23586
|
-
join as
|
|
24563
|
+
join as join22,
|
|
23587
24564
|
resolve as resolve8
|
|
23588
24565
|
} from "node:path";
|
|
23589
24566
|
async function runLocalCodexRunner(options) {
|
|
@@ -23594,7 +24571,11 @@ async function runLocalCodexRunner(options) {
|
|
|
23594
24571
|
removeHandlers: void 0
|
|
23595
24572
|
};
|
|
23596
24573
|
const close = () => closeCleanupStack(cleanup);
|
|
23597
|
-
|
|
24574
|
+
const runnerDrainHooks = {
|
|
24575
|
+
stopAccepting: () => void 0,
|
|
24576
|
+
flushInboundQueues: () => void 0
|
|
24577
|
+
};
|
|
24578
|
+
cleanup.removeHandlers = installCleanupHandlers(close, runnerDrainHooks, log2);
|
|
23598
24579
|
log2("runner.starting", {
|
|
23599
24580
|
runnerId: options.runnerId,
|
|
23600
24581
|
cwd: options.cwd,
|
|
@@ -23638,7 +24619,13 @@ async function runLocalCodexRunner(options) {
|
|
|
23638
24619
|
runnerId: options.runnerId
|
|
23639
24620
|
});
|
|
23640
24621
|
}
|
|
23641
|
-
return await openLocalCodexRunner(
|
|
24622
|
+
return await openLocalCodexRunner(
|
|
24623
|
+
options,
|
|
24624
|
+
log2,
|
|
24625
|
+
cleanup,
|
|
24626
|
+
close,
|
|
24627
|
+
runnerDrainHooks
|
|
24628
|
+
);
|
|
23642
24629
|
} catch (error) {
|
|
23643
24630
|
await close().catch(() => void 0);
|
|
23644
24631
|
throw error;
|
|
@@ -23654,26 +24641,113 @@ async function runThreadCodexWorker(options) {
|
|
|
23654
24641
|
const log2 = makeRunnerLogger(options);
|
|
23655
24642
|
const started = await startOwnedCodexAppServer(options);
|
|
23656
24643
|
const codex = await connectCodexAppServer(started.url);
|
|
24644
|
+
const kandanThreadId = options.threadProcess.kandanThreadId;
|
|
23657
24645
|
const stop = once(() => {
|
|
23658
24646
|
codex.close();
|
|
23659
24647
|
started.stop();
|
|
23660
24648
|
log2.close();
|
|
23661
24649
|
});
|
|
23662
24650
|
bindThreadCodexWorkerIpc(codex);
|
|
24651
|
+
codex.onClose?.((error) => {
|
|
24652
|
+
log2("runner.thread_codex_worker_session_closed", {
|
|
24653
|
+
kandanThreadId,
|
|
24654
|
+
reason: error.message
|
|
24655
|
+
});
|
|
24656
|
+
if (process.send !== void 0 && process.connected !== false) {
|
|
24657
|
+
try {
|
|
24658
|
+
process.send({
|
|
24659
|
+
type: "linzumi_thread_codex_worker_session_closed",
|
|
24660
|
+
kandanThreadId,
|
|
24661
|
+
reason: error.message
|
|
24662
|
+
});
|
|
24663
|
+
} catch {
|
|
24664
|
+
}
|
|
24665
|
+
}
|
|
24666
|
+
stop();
|
|
24667
|
+
});
|
|
23663
24668
|
process.send({
|
|
23664
24669
|
type: "linzumi_thread_codex_worker_ready",
|
|
23665
|
-
kandanThreadId
|
|
24670
|
+
kandanThreadId,
|
|
23666
24671
|
commanderManagedPorts: commanderManagedPortsForStartedCodex(started)
|
|
23667
24672
|
});
|
|
23668
24673
|
await waitForThreadCodexWorkerStop(started.process);
|
|
23669
24674
|
stop();
|
|
23670
24675
|
}
|
|
23671
|
-
|
|
24676
|
+
function codexAuthStatusFromLoginStatus(exitCode, stdout, stderr) {
|
|
24677
|
+
const output = `${stdout}
|
|
24678
|
+
${stderr}`.toLowerCase();
|
|
24679
|
+
const loggedOut = output.includes("not logged in") || output.includes("not authenticated") || output.includes("login required") || output.includes("please log in");
|
|
24680
|
+
if (loggedOut) {
|
|
24681
|
+
return "not_logged_in";
|
|
24682
|
+
}
|
|
24683
|
+
if (exitCode === 0) {
|
|
24684
|
+
return "logged_in";
|
|
24685
|
+
}
|
|
24686
|
+
return "probe_failed";
|
|
24687
|
+
}
|
|
24688
|
+
function errorMatchesCode(error, code) {
|
|
24689
|
+
return error.code === code;
|
|
24690
|
+
}
|
|
24691
|
+
function probeCodexAuthStatus(codexBin, cwd) {
|
|
24692
|
+
return new Promise((resolve12) => {
|
|
24693
|
+
const stdoutChunks = [];
|
|
24694
|
+
const stderrChunks = [];
|
|
24695
|
+
let resolved = false;
|
|
24696
|
+
let timeout;
|
|
24697
|
+
const resolveOnce = (status) => {
|
|
24698
|
+
if (resolved) {
|
|
24699
|
+
return;
|
|
24700
|
+
}
|
|
24701
|
+
resolved = true;
|
|
24702
|
+
if (timeout !== void 0) {
|
|
24703
|
+
clearTimeout(timeout);
|
|
24704
|
+
}
|
|
24705
|
+
resolve12(status);
|
|
24706
|
+
};
|
|
24707
|
+
const child = spawn9(codexBin, ["login", "status"], {
|
|
24708
|
+
cwd,
|
|
24709
|
+
env: process.env,
|
|
24710
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
24711
|
+
});
|
|
24712
|
+
timeout = setTimeout(() => {
|
|
24713
|
+
child.kill("SIGTERM");
|
|
24714
|
+
resolveOnce("probe_failed");
|
|
24715
|
+
}, CODEX_AUTH_STATUS_PROBE_TIMEOUT_MS);
|
|
24716
|
+
child.stdout?.on("data", (chunk) => {
|
|
24717
|
+
stdoutChunks.push(chunk);
|
|
24718
|
+
});
|
|
24719
|
+
child.stderr?.on("data", (chunk) => {
|
|
24720
|
+
stderrChunks.push(chunk);
|
|
24721
|
+
});
|
|
24722
|
+
child.once("error", (error) => {
|
|
24723
|
+
resolveOnce(
|
|
24724
|
+
errorMatchesCode(error, "ENOENT") ? "binary_missing" : "probe_failed"
|
|
24725
|
+
);
|
|
24726
|
+
});
|
|
24727
|
+
child.once("exit", (code) => {
|
|
24728
|
+
resolveOnce(
|
|
24729
|
+
codexAuthStatusFromLoginStatus(
|
|
24730
|
+
code,
|
|
24731
|
+
Buffer.concat(stdoutChunks).toString("utf8"),
|
|
24732
|
+
Buffer.concat(stderrChunks).toString("utf8")
|
|
24733
|
+
)
|
|
24734
|
+
);
|
|
24735
|
+
});
|
|
24736
|
+
});
|
|
24737
|
+
}
|
|
24738
|
+
async function openLocalCodexRunner(options, log2, cleanup, close, runnerDrainHooks) {
|
|
23672
24739
|
const agentProviders = availableRunnerAgentProviders(options);
|
|
23673
24740
|
const localCodexAppServerAvailable = hasLocalCodexAppServerCapability(options);
|
|
23674
24741
|
const dependencyStatusRef = {
|
|
23675
24742
|
value: options.dependencyStatus
|
|
23676
24743
|
};
|
|
24744
|
+
const codexAuthStatusRef = {
|
|
24745
|
+
value: await probeCodexAuthStatus(options.codexBin, options.cwd)
|
|
24746
|
+
};
|
|
24747
|
+
log2("runner.codex_auth_status_probed", {
|
|
24748
|
+
runnerId: options.runnerId,
|
|
24749
|
+
codex_auth_status: codexAuthStatusRef.value
|
|
24750
|
+
});
|
|
23677
24751
|
const refreshDependencyStatus = async () => {
|
|
23678
24752
|
if (dependencyStatusRef.value === void 0) {
|
|
23679
24753
|
return;
|
|
@@ -23699,6 +24773,27 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
23699
24773
|
});
|
|
23700
24774
|
}
|
|
23701
24775
|
};
|
|
24776
|
+
const refreshCodexAuthStatus = async () => {
|
|
24777
|
+
try {
|
|
24778
|
+
const refreshed = await probeCodexAuthStatus(
|
|
24779
|
+
options.codexBin,
|
|
24780
|
+
options.cwd
|
|
24781
|
+
);
|
|
24782
|
+
const previous = codexAuthStatusRef.value;
|
|
24783
|
+
codexAuthStatusRef.value = refreshed;
|
|
24784
|
+
log2("runner.codex_auth_status_reprobed", {
|
|
24785
|
+
runnerId: options.runnerId,
|
|
24786
|
+
codex_auth_status: refreshed,
|
|
24787
|
+
codex_auth_status_changed: previous !== refreshed
|
|
24788
|
+
});
|
|
24789
|
+
} catch (error) {
|
|
24790
|
+
codexAuthStatusRef.value = "probe_failed";
|
|
24791
|
+
log2("runner.codex_auth_status_reprobe_failed", {
|
|
24792
|
+
runnerId: options.runnerId,
|
|
24793
|
+
message: error instanceof Error ? error.message : String(error)
|
|
24794
|
+
});
|
|
24795
|
+
}
|
|
24796
|
+
};
|
|
23702
24797
|
const allowedForwardPorts = options.allowedForwardPorts ?? [];
|
|
23703
24798
|
const liveForwardPorts = new Set(allowedForwardPorts);
|
|
23704
24799
|
const managedForwardPorts = /* @__PURE__ */ new Set();
|
|
@@ -23852,6 +24947,16 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
23852
24947
|
// would silently drop it (and run against the user's personal account).
|
|
23853
24948
|
// Old servers ignore the extra key.
|
|
23854
24949
|
modelProviders: [...ADVERTISED_MODEL_PROVIDERS],
|
|
24950
|
+
codexAuthStatus: codexAuthStatusRef.value,
|
|
24951
|
+
// Launch policy from plans/2026-06-22-wafer-fallback-implementation-note.md:
|
|
24952
|
+
// Wafer is globally offered by shipped Electron runners during launch, so
|
|
24953
|
+
// this is intentionally an unconditional `true`. The server keeps a
|
|
24954
|
+
// defensive fallback (`"wafer" in modelProviders`) for older CLIs that
|
|
24955
|
+
// never advertised this key; that fallback is deliberately unreachable from
|
|
24956
|
+
// current builds. If a future use case needs to disable Wafer per runner
|
|
24957
|
+
// (e.g. enterprise/self-hosted credentials), make this conditional here
|
|
24958
|
+
// rather than relying on the server inferring it from modelProviders.
|
|
24959
|
+
waferAvailable: true,
|
|
23855
24960
|
defaultAgentProvider: "codex",
|
|
23856
24961
|
startInstance: allowedCwds.value.length > 0,
|
|
23857
24962
|
durableClaudeSessionStore: true,
|
|
@@ -23900,6 +25005,123 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
23900
25005
|
runnerThreadSessionRegistryPath(options.runnerId),
|
|
23901
25006
|
log2
|
|
23902
25007
|
);
|
|
25008
|
+
const appliedSourceSeqFileStore = createControlCursorFileStore(
|
|
25009
|
+
appliedSourceSeqStorePath(options.runnerId),
|
|
25010
|
+
log2,
|
|
25011
|
+
// POWER-LOSS DURABLE (SEAM 1): this watermark is the SOLE fence that stops a
|
|
25012
|
+
// confirmed-dispatched turn from being re-dispatched on restart, and it is
|
|
25013
|
+
// paired with the fsynced durable inbound queue. fsync it on every flush so
|
|
25014
|
+
// a power loss cannot lose a dispatch-confirm advance while the queue ack
|
|
25015
|
+
// survived (which would double-execute the turn on reopen).
|
|
25016
|
+
{ fsyncOnWrite: true }
|
|
25017
|
+
);
|
|
25018
|
+
cleanup.actions.push(() => appliedSourceSeqFileStore.flush());
|
|
25019
|
+
const appliedSourceSeqStore = {
|
|
25020
|
+
initial: (key) => appliedSourceSeqFileStore.initial(key),
|
|
25021
|
+
onAdvance: (key, sourceSeq) => appliedSourceSeqFileStore.onAdvance(key, sourceSeq),
|
|
25022
|
+
flush: () => appliedSourceSeqFileStore.flush()
|
|
25023
|
+
};
|
|
25024
|
+
const commitLiveWorkerAcceptedSourceSeq = (args) => {
|
|
25025
|
+
if (args.sourceSeq === void 0) {
|
|
25026
|
+
return;
|
|
25027
|
+
}
|
|
25028
|
+
const watermarkKey = threadSourceSeqWatermarkKey(
|
|
25029
|
+
inboundQueueWorkspace(),
|
|
25030
|
+
args.channel,
|
|
25031
|
+
args.threadId
|
|
25032
|
+
);
|
|
25033
|
+
const advanced = commitLiveAcceptedSourceSeq({
|
|
25034
|
+
store: appliedSourceSeqStore,
|
|
25035
|
+
watermarkKey,
|
|
25036
|
+
sourceSeq: args.sourceSeq
|
|
25037
|
+
});
|
|
25038
|
+
if (advanced) {
|
|
25039
|
+
log2("runner.live_worker_source_seq_committed", {
|
|
25040
|
+
kandanThreadId: args.threadId,
|
|
25041
|
+
channel: args.channel,
|
|
25042
|
+
sourceSeq: args.sourceSeq
|
|
25043
|
+
});
|
|
25044
|
+
}
|
|
25045
|
+
capturedLiveFollowUpEvents.delete(
|
|
25046
|
+
capturedFollowUpKey(args.channel, args.threadId, args.sourceSeq)
|
|
25047
|
+
);
|
|
25048
|
+
};
|
|
25049
|
+
const liveSourceSeqAlreadyApplied = (args) => {
|
|
25050
|
+
if (args.sourceSeq === void 0) {
|
|
25051
|
+
return false;
|
|
25052
|
+
}
|
|
25053
|
+
const watermarkKey = threadSourceSeqWatermarkKey(
|
|
25054
|
+
inboundQueueWorkspace(),
|
|
25055
|
+
args.channel,
|
|
25056
|
+
args.threadId
|
|
25057
|
+
);
|
|
25058
|
+
const applied = appliedSourceSeqStore.initial(watermarkKey);
|
|
25059
|
+
return !shouldApplySourceSeq(applied, args.sourceSeq);
|
|
25060
|
+
};
|
|
25061
|
+
const threadReconcilerLock = createThreadReconcilerLock();
|
|
25062
|
+
const leaseEpochRegistry = createLeaseEpochRegistry();
|
|
25063
|
+
const durableInboundQueues = /* @__PURE__ */ new Map();
|
|
25064
|
+
const latestResumeReconcileByThread = /* @__PURE__ */ new Map();
|
|
25065
|
+
let acceptingInbound = true;
|
|
25066
|
+
const inboundQueueWorkspace = () => runnerWorkspaceSlug(options) ?? "default";
|
|
25067
|
+
const durableInboundQueueFor = (channel, kandanThreadId) => {
|
|
25068
|
+
const workspace = inboundQueueWorkspace();
|
|
25069
|
+
const cacheKey = `${workspace}:${channel}:${kandanThreadId}`;
|
|
25070
|
+
const existing = durableInboundQueues.get(cacheKey);
|
|
25071
|
+
if (existing !== void 0) {
|
|
25072
|
+
return existing;
|
|
25073
|
+
}
|
|
25074
|
+
const queue = createDurableInboundThreadQueue(
|
|
25075
|
+
durableInboundQueuePath(workspace, channel, kandanThreadId),
|
|
25076
|
+
log2
|
|
25077
|
+
);
|
|
25078
|
+
durableInboundQueues.set(cacheKey, queue);
|
|
25079
|
+
return queue;
|
|
25080
|
+
};
|
|
25081
|
+
const capturedLiveFollowUpEvents = /* @__PURE__ */ new Map();
|
|
25082
|
+
const capturedFollowUpKey = (channel, threadId, seq2) => `${inboundQueueWorkspace()}:${channel}:${threadId}:${seq2}`;
|
|
25083
|
+
const captureLiveFollowUpEvent = (channel, threadId, event) => {
|
|
25084
|
+
capturedLiveFollowUpEvents.set(
|
|
25085
|
+
capturedFollowUpKey(channel, threadId, event.seq),
|
|
25086
|
+
event
|
|
25087
|
+
);
|
|
25088
|
+
};
|
|
25089
|
+
const reEnqueueUndeliveredFollowUps = (args) => {
|
|
25090
|
+
const queue = durableInboundQueueFor(args.channel, args.threadId);
|
|
25091
|
+
for (const seq2 of args.sourceSeqs) {
|
|
25092
|
+
const key = capturedFollowUpKey(args.channel, args.threadId, seq2);
|
|
25093
|
+
const event = capturedLiveFollowUpEvents.get(key);
|
|
25094
|
+
if (event === void 0) {
|
|
25095
|
+
log2("runner.live_follow_up_recapture_missing", {
|
|
25096
|
+
kandanThreadId: args.threadId,
|
|
25097
|
+
channel: args.channel,
|
|
25098
|
+
sourceSeq: seq2
|
|
25099
|
+
});
|
|
25100
|
+
continue;
|
|
25101
|
+
}
|
|
25102
|
+
const enqueued = queue.enqueue(
|
|
25103
|
+
seq2,
|
|
25104
|
+
"replay",
|
|
25105
|
+
serializeKandanChatEventForQueue(event),
|
|
25106
|
+
Date.now()
|
|
25107
|
+
);
|
|
25108
|
+
log2("runner.live_follow_up_re_enqueued_on_close", {
|
|
25109
|
+
kandanThreadId: args.threadId,
|
|
25110
|
+
channel: args.channel,
|
|
25111
|
+
sourceSeq: seq2,
|
|
25112
|
+
enqueued: enqueued.accepted
|
|
25113
|
+
});
|
|
25114
|
+
capturedLiveFollowUpEvents.delete(key);
|
|
25115
|
+
}
|
|
25116
|
+
};
|
|
25117
|
+
runnerDrainHooks.stopAccepting = () => {
|
|
25118
|
+
acceptingInbound = false;
|
|
25119
|
+
};
|
|
25120
|
+
runnerDrainHooks.flushInboundQueues = () => {
|
|
25121
|
+
for (const queue of durableInboundQueues.values()) {
|
|
25122
|
+
queue.compact();
|
|
25123
|
+
}
|
|
25124
|
+
};
|
|
23903
25125
|
const controlEpochStore = createRunnerControlEpochStore(
|
|
23904
25126
|
controlCursorStore,
|
|
23905
25127
|
appliedStartTurnStore,
|
|
@@ -24741,6 +25963,8 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
24741
25963
|
codex,
|
|
24742
25964
|
topic,
|
|
24743
25965
|
instanceId,
|
|
25966
|
+
// SEAM 2: source the held lease epoch for the stream-write fence echo.
|
|
25967
|
+
leaseEpochFor: (threadId) => leaseEpochRegistry.current(threadId),
|
|
24744
25968
|
options: {
|
|
24745
25969
|
kandanUrl: options.kandanUrl,
|
|
24746
25970
|
token: options.token,
|
|
@@ -24777,6 +26001,10 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
24777
26001
|
// session is attached (the durable thread's worker fully died). Late-
|
|
24778
26002
|
// bound to dodge the const's temporal dead zone (defined below).
|
|
24779
26003
|
onUnroutedThreadMessage: (event) => handleUnroutedThreadMessage(event),
|
|
26004
|
+
// BLOCKER B: codex live dispatch-confirm watermark commit on the main
|
|
26005
|
+
// channel session too, off the same persisted authority the keystone
|
|
26006
|
+
// gates on.
|
|
26007
|
+
onLiveSourceSeqAccepted: commitLiveWorkerAcceptedSourceSeq,
|
|
24780
26008
|
channelSession: options.channelSession
|
|
24781
26009
|
},
|
|
24782
26010
|
log: log2
|
|
@@ -25127,6 +26355,8 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25127
26355
|
codex: sessionCodex,
|
|
25128
26356
|
topic,
|
|
25129
26357
|
instanceId,
|
|
26358
|
+
// SEAM 2: dynamic per-thread session also echoes the held lease epoch.
|
|
26359
|
+
leaseEpochFor: (threadId) => leaseEpochRegistry.current(threadId),
|
|
25130
26360
|
options: {
|
|
25131
26361
|
kandanUrl: options.kandanUrl,
|
|
25132
26362
|
token: options.token,
|
|
@@ -25141,6 +26371,11 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25141
26371
|
// it out of the const's temporal dead zone while still resolving the
|
|
25142
26372
|
// live handler when an unrouted chat event actually arrives.
|
|
25143
26373
|
onUnroutedThreadMessage: (event) => handleUnroutedThreadMessage(event),
|
|
26374
|
+
// BLOCKER B: codex live dispatch-confirm watermark commit (the
|
|
26375
|
+
// production-default double-exec fix). Fires from drainKandanMessageQueue
|
|
26376
|
+
// when turn/start replies, off the SAME persisted authority the keystone
|
|
26377
|
+
// gates on, so a codex worker-death + replay is gated 'ignored'.
|
|
26378
|
+
onLiveSourceSeqAccepted: commitLiveWorkerAcceptedSourceSeq,
|
|
25144
26379
|
pipelinePersistDir: commanderOutboxPersistDir(),
|
|
25145
26380
|
enablePortForwardWatch: true,
|
|
25146
26381
|
initialForwardPorts: allowedForwardPorts,
|
|
@@ -25519,7 +26754,12 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25519
26754
|
control,
|
|
25520
26755
|
log2,
|
|
25521
26756
|
onStartedThread,
|
|
25522
|
-
void 0
|
|
26757
|
+
void 0,
|
|
26758
|
+
(id) => leaseEpochRegistry.current(id),
|
|
26759
|
+
commitLiveWorkerAcceptedSourceSeq,
|
|
26760
|
+
captureLiveFollowUpEvent,
|
|
26761
|
+
reEnqueueUndeliveredFollowUps,
|
|
26762
|
+
liveSourceSeqAlreadyApplied
|
|
25523
26763
|
)
|
|
25524
26764
|
);
|
|
25525
26765
|
}
|
|
@@ -25562,6 +26802,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25562
26802
|
attachments: event.attachments,
|
|
25563
26803
|
boundStartTimeout: true
|
|
25564
26804
|
});
|
|
26805
|
+
return "dispatched";
|
|
25565
26806
|
} catch (error) {
|
|
25566
26807
|
log2("runner.spawn_on_miss_failed", {
|
|
25567
26808
|
kandanThreadId: event.threadId ?? null,
|
|
@@ -25575,6 +26816,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25575
26816
|
`could not resume the thread's coding session: ${error instanceof Error ? error.message : String(error)}`,
|
|
25576
26817
|
record.codexThreadId
|
|
25577
26818
|
);
|
|
26819
|
+
return "transient_failed";
|
|
25578
26820
|
}
|
|
25579
26821
|
};
|
|
25580
26822
|
const handleUnroutedThreadMessage = async (event) => {
|
|
@@ -25605,6 +26847,246 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25605
26847
|
if (record === void 0) {
|
|
25606
26848
|
return "ignored";
|
|
25607
26849
|
}
|
|
26850
|
+
const queueChannel = record.channel;
|
|
26851
|
+
const watermarkKey = threadSourceSeqWatermarkKey(
|
|
26852
|
+
inboundQueueWorkspace(),
|
|
26853
|
+
queueChannel,
|
|
26854
|
+
kandanThreadId
|
|
26855
|
+
);
|
|
26856
|
+
const queue = durableInboundQueueFor(queueChannel, kandanThreadId);
|
|
26857
|
+
if (!acceptingInbound) {
|
|
26858
|
+
const enqueued = queue.enqueue(
|
|
26859
|
+
event.seq,
|
|
26860
|
+
event.localRunnerEventType === void 0 ? "live" : "replay",
|
|
26861
|
+
serializeKandanChatEventForQueue(event),
|
|
26862
|
+
Date.now()
|
|
26863
|
+
);
|
|
26864
|
+
log2("runner.spawn_on_miss_drain_in_progress", {
|
|
26865
|
+
kandanThreadId,
|
|
26866
|
+
seq: event.seq,
|
|
26867
|
+
enqueued: enqueued.accepted
|
|
26868
|
+
});
|
|
26869
|
+
return "spawned";
|
|
26870
|
+
}
|
|
26871
|
+
const outcome = await receiveResumeMessage({
|
|
26872
|
+
queue,
|
|
26873
|
+
store: appliedSourceSeqStore,
|
|
26874
|
+
lock: threadReconcilerLock,
|
|
26875
|
+
leases: leaseEpochRegistry,
|
|
26876
|
+
threadId: kandanThreadId,
|
|
26877
|
+
watermarkKey,
|
|
26878
|
+
sourceSeq: event.seq,
|
|
26879
|
+
kind: event.localRunnerEventType === void 0 ? "live" : "replay",
|
|
26880
|
+
payload: serializeKandanChatEventForQueue(event),
|
|
26881
|
+
leaseEpoch: readLeaseEpoch(event),
|
|
26882
|
+
// Blocker #2: the keystone drains the queue HEAD in source_seq order, so
|
|
26883
|
+
// the dispatch is keyed by (sourceSeq, payload) - it may run a LOWER
|
|
26884
|
+
// pending seq before this arrived one. Reconstruct the event from the
|
|
26885
|
+
// head's payload, exactly as the boot/rejoin drain does.
|
|
26886
|
+
dispatch: async (sourceSeq, payload) => {
|
|
26887
|
+
const drained = deserializeKandanChatEventFromQueue(payload);
|
|
26888
|
+
if (drained === void 0) {
|
|
26889
|
+
log2("runner.spawn_on_miss_bad_payload", {
|
|
26890
|
+
kandanThreadId,
|
|
26891
|
+
seq: sourceSeq
|
|
26892
|
+
});
|
|
26893
|
+
return "permanent_failed";
|
|
26894
|
+
}
|
|
26895
|
+
return await dispatchResumeForOwnedThread(drained, record);
|
|
26896
|
+
},
|
|
26897
|
+
// BLOCKER #4: pass the server's per-thread processed floor (the most-recent
|
|
26898
|
+
// resume_reconcile rows) so the live-miss path reconciles
|
|
26899
|
+
// highestProcessedSourceSeq into the watermark BEFORE dispatching - a
|
|
26900
|
+
// completed-but-not-locally-acked turn that replays is gated 'ignored', not
|
|
26901
|
+
// re-run. Empty until the server SEAM 1 half ships; then the floor is
|
|
26902
|
+
// authoritative. The live-miss path leaves onlyDispatchUnattempted at its
|
|
26903
|
+
// default (false): the arrived message is attempts===0 and the blind
|
|
26904
|
+
// post-completion fence is owned by the boot/rejoin recovery drain, not this
|
|
26905
|
+
// on-receipt path (changing it here would alter the existing live behavior).
|
|
26906
|
+
serverRows: latestResumeReconcileByThread.get(kandanThreadId) ?? [],
|
|
26907
|
+
// Blocker #3: surface a LOUD terminal `failed` state on any dead-letter.
|
|
26908
|
+
onDeadLetter: deadLetterPublisherFor(record),
|
|
26909
|
+
log: log2
|
|
26910
|
+
});
|
|
26911
|
+
return outcome === "ignored" ? "ignored" : "spawned";
|
|
26912
|
+
};
|
|
26913
|
+
const drainOwnedThreadQueue = async (record, serverRows = []) => {
|
|
26914
|
+
if (!acceptingInbound) {
|
|
26915
|
+
return;
|
|
26916
|
+
}
|
|
26917
|
+
const kandanThreadId = record.kandanThreadId;
|
|
26918
|
+
const queueChannel = record.channel;
|
|
26919
|
+
const watermarkKey = threadSourceSeqWatermarkKey(
|
|
26920
|
+
inboundQueueWorkspace(),
|
|
26921
|
+
queueChannel,
|
|
26922
|
+
kandanThreadId
|
|
26923
|
+
);
|
|
26924
|
+
const queue = durableInboundQueueFor(queueChannel, kandanThreadId);
|
|
26925
|
+
await drainResumeQueue({
|
|
26926
|
+
queue,
|
|
26927
|
+
store: appliedSourceSeqStore,
|
|
26928
|
+
lock: threadReconcilerLock,
|
|
26929
|
+
threadId: kandanThreadId,
|
|
26930
|
+
watermarkKey,
|
|
26931
|
+
serverRows,
|
|
26932
|
+
log: log2,
|
|
26933
|
+
// Until the SERVER message_state reconcile half (SEAM 1 server) ships, the
|
|
26934
|
+
// drain runs with serverRows=[] in production. In that mode a post-
|
|
26935
|
+
// completion-kill (turn confirmed-dispatched, then SIGKILL before the
|
|
26936
|
+
// watermark flush + ack) leaves an attempted-but-unacked entry. Blindly
|
|
26937
|
+
// re-dispatching it would double-execute the turn (real edits/commits), so
|
|
26938
|
+
// gate the drain to UNATTEMPTED entries only: an attempted entry is dead-
|
|
26939
|
+
// lettered with a loud `failed` state rather than re-run. Once serverRows
|
|
26940
|
+
// is populated the reconcile fences already-run seqs and this gate is moot.
|
|
26941
|
+
onlyDispatchUnattempted: serverRows.length === 0,
|
|
26942
|
+
// Blocker #3: surface a LOUD terminal `failed` state on any dead-letter
|
|
26943
|
+
// (exhausted retries, a withheld post-completion-kill entry, or a permanent
|
|
26944
|
+
// failure) so a persistently-unbindable thread is never silently dropped.
|
|
26945
|
+
onDeadLetter: deadLetterPublisherFor(record),
|
|
26946
|
+
// A torn tail with no server reconcile cannot be re-derived on this branch
|
|
26947
|
+
// (no server outbox/replay). Publish a loud "please resend" terminal state
|
|
26948
|
+
// for the thread instead of silently dropping the lost tail.
|
|
26949
|
+
onTornTailUnrecoverable: async (threadId) => {
|
|
26950
|
+
const control = rehydrationReconnectControl(record);
|
|
26951
|
+
await publishSpawnOnMissMessageState(
|
|
26952
|
+
control,
|
|
26953
|
+
"failed",
|
|
26954
|
+
"a message may have been lost during an unexpected shutdown (power loss). Please resend your last message to continue this job.",
|
|
26955
|
+
record.codexThreadId
|
|
26956
|
+
);
|
|
26957
|
+
log2("runner.inbound_torn_tail_resend_published", {
|
|
26958
|
+
kandanThreadId: threadId
|
|
26959
|
+
});
|
|
26960
|
+
},
|
|
26961
|
+
dispatch: async (sourceSeq, payload) => {
|
|
26962
|
+
const event = deserializeKandanChatEventFromQueue(payload);
|
|
26963
|
+
if (event === void 0) {
|
|
26964
|
+
log2("runner.inbound_drain_bad_payload", {
|
|
26965
|
+
kandanThreadId,
|
|
26966
|
+
seq: sourceSeq
|
|
26967
|
+
});
|
|
26968
|
+
return "permanent_failed";
|
|
26969
|
+
}
|
|
26970
|
+
return await dispatchResumeForOwnedThread(event, record);
|
|
26971
|
+
}
|
|
26972
|
+
});
|
|
26973
|
+
};
|
|
26974
|
+
const consumeResumeReconcile = (reconcileValue) => {
|
|
26975
|
+
const byThread = /* @__PURE__ */ new Map();
|
|
26976
|
+
const entries = parseResumeReconcileEntries(reconcileValue);
|
|
26977
|
+
for (const entry of entries) {
|
|
26978
|
+
if (entry.leaseEpoch !== void 0) {
|
|
26979
|
+
const adopted = leaseEpochRegistry.adopt(entry.threadId, entry.leaseEpoch);
|
|
26980
|
+
if (adopted) {
|
|
26981
|
+
log2("runner.resume_reconcile_lease_adopted", {
|
|
26982
|
+
kandanThreadId: entry.threadId,
|
|
26983
|
+
leaseEpoch: entry.leaseEpoch
|
|
26984
|
+
});
|
|
26985
|
+
}
|
|
26986
|
+
}
|
|
26987
|
+
const rows = resumeReconcileServerRows(entry);
|
|
26988
|
+
byThread.set(entry.threadId, rows);
|
|
26989
|
+
latestResumeReconcileByThread.set(entry.threadId, rows);
|
|
26990
|
+
}
|
|
26991
|
+
if (entries.length > 0) {
|
|
26992
|
+
log2("runner.resume_reconcile_consumed", { threadCount: entries.length });
|
|
26993
|
+
}
|
|
26994
|
+
return byThread;
|
|
26995
|
+
};
|
|
26996
|
+
const drainOrphanedThreadQueues = async () => {
|
|
26997
|
+
const dir = durableInboundQueueDir();
|
|
26998
|
+
let files;
|
|
26999
|
+
try {
|
|
27000
|
+
files = readdirSync4(dir).filter((name) => name.endsWith(".log"));
|
|
27001
|
+
} catch (error) {
|
|
27002
|
+
if (error?.code === "ENOENT") {
|
|
27003
|
+
return;
|
|
27004
|
+
}
|
|
27005
|
+
log2("runner.inbound_orphan_scan_failed", {
|
|
27006
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27007
|
+
});
|
|
27008
|
+
return;
|
|
27009
|
+
}
|
|
27010
|
+
const workspace = inboundQueueWorkspace();
|
|
27011
|
+
const owned = new Set(
|
|
27012
|
+
threadSessionRegistry.list().map(
|
|
27013
|
+
(record) => durableInboundQueuePath(
|
|
27014
|
+
workspace,
|
|
27015
|
+
record.channel,
|
|
27016
|
+
record.kandanThreadId
|
|
27017
|
+
)
|
|
27018
|
+
)
|
|
27019
|
+
);
|
|
27020
|
+
for (const name of files) {
|
|
27021
|
+
const path2 = join22(dir, name);
|
|
27022
|
+
if (owned.has(path2)) {
|
|
27023
|
+
continue;
|
|
27024
|
+
}
|
|
27025
|
+
try {
|
|
27026
|
+
const queue = createDurableInboundThreadQueue(path2, log2);
|
|
27027
|
+
const stranded = queue.entries();
|
|
27028
|
+
if (stranded.length === 0) {
|
|
27029
|
+
continue;
|
|
27030
|
+
}
|
|
27031
|
+
for (const entry of stranded) {
|
|
27032
|
+
queue.deadLetter(entry.sourceSeq);
|
|
27033
|
+
}
|
|
27034
|
+
queue.compact();
|
|
27035
|
+
log2("runner.inbound_orphan_dead_lettered", {
|
|
27036
|
+
path: path2,
|
|
27037
|
+
strandedCount: stranded.length,
|
|
27038
|
+
strandedSeqs: stranded.map((e) => e.sourceSeq)
|
|
27039
|
+
});
|
|
27040
|
+
} catch (error) {
|
|
27041
|
+
log2("runner.inbound_orphan_drain_failed", {
|
|
27042
|
+
path: path2,
|
|
27043
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27044
|
+
});
|
|
27045
|
+
}
|
|
27046
|
+
}
|
|
27047
|
+
};
|
|
27048
|
+
const drainAllOwnedThreadQueues = async (reconcileByThread) => {
|
|
27049
|
+
if (!acceptingInbound) {
|
|
27050
|
+
return;
|
|
27051
|
+
}
|
|
27052
|
+
const records = threadSessionRegistry.list();
|
|
27053
|
+
await Promise.all(
|
|
27054
|
+
records.map(
|
|
27055
|
+
(record) => drainOwnedThreadQueue(
|
|
27056
|
+
record,
|
|
27057
|
+
reconcileByThread?.get(record.kandanThreadId) ?? []
|
|
27058
|
+
).catch((error) => {
|
|
27059
|
+
log2("runner.inbound_drain_failed", {
|
|
27060
|
+
kandanThreadId: record.kandanThreadId,
|
|
27061
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27062
|
+
});
|
|
27063
|
+
})
|
|
27064
|
+
)
|
|
27065
|
+
);
|
|
27066
|
+
await drainOrphanedThreadQueues();
|
|
27067
|
+
};
|
|
27068
|
+
const bootReconcile = consumeResumeReconcile(
|
|
27069
|
+
objectValue(joinResponse)?.resume_reconcile
|
|
27070
|
+
);
|
|
27071
|
+
const bootDrain = threadSessionRehydration.then(() => drainAllOwnedThreadQueues(bootReconcile)).catch((error) => {
|
|
27072
|
+
log2("runner.inbound_boot_drain_failed", {
|
|
27073
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27074
|
+
});
|
|
27075
|
+
});
|
|
27076
|
+
cleanup.actions.push(() => bootDrain);
|
|
27077
|
+
kandan.onReconnect((rejoinReplies) => {
|
|
27078
|
+
const rejoinReply = rejoinReplies.get(topic);
|
|
27079
|
+
const reconcile = consumeResumeReconcile(
|
|
27080
|
+
rejoinReply === void 0 ? void 0 : rejoinReply.resume_reconcile
|
|
27081
|
+
);
|
|
27082
|
+
void drainAllOwnedThreadQueues(reconcile).catch((error) => {
|
|
27083
|
+
log2("runner.inbound_rejoin_drain_failed", {
|
|
27084
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27085
|
+
});
|
|
27086
|
+
});
|
|
27087
|
+
});
|
|
27088
|
+
const dispatchResumeForOwnedThread = async (event, record) => {
|
|
27089
|
+
const kandanThreadId = record.kandanThreadId;
|
|
25608
27090
|
const baseControl = rehydrationReconnectControl(record);
|
|
25609
27091
|
const providerBinding = threadModelProviderBindings.get(kandanThreadId);
|
|
25610
27092
|
const reconnectControl = {
|
|
@@ -25634,7 +27116,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25634
27116
|
`could not resume the thread's coding session: working directory ${record.cwd} is not an allowed root (${cwd.reason})`,
|
|
25635
27117
|
record.codexThreadId
|
|
25636
27118
|
);
|
|
25637
|
-
return "
|
|
27119
|
+
return "permanent_failed";
|
|
25638
27120
|
}
|
|
25639
27121
|
const resolvedCwd = cwd.cwd;
|
|
25640
27122
|
const inFlightSpawn = spawnOnMissInFlight.get(kandanThreadId);
|
|
@@ -25654,14 +27136,13 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25654
27136
|
`could not resume the thread's coding session: no live session after spawn`,
|
|
25655
27137
|
record.codexThreadId
|
|
25656
27138
|
);
|
|
25657
|
-
return "
|
|
27139
|
+
return "transient_failed";
|
|
25658
27140
|
}
|
|
25659
|
-
await replaySpawnOnMissMessageOnLiveSession(
|
|
27141
|
+
return await replaySpawnOnMissMessageOnLiveSession(
|
|
25660
27142
|
event,
|
|
25661
27143
|
record,
|
|
25662
27144
|
reconnectControl
|
|
25663
27145
|
);
|
|
25664
|
-
return "spawned";
|
|
25665
27146
|
}
|
|
25666
27147
|
log2("runner.spawn_on_miss_resuming", {
|
|
25667
27148
|
kandanThreadId,
|
|
@@ -25682,12 +27163,11 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25682
27163
|
);
|
|
25683
27164
|
if (threadStartDelegatedToSibling(startResult)) {
|
|
25684
27165
|
spawnSettle("spawned");
|
|
25685
|
-
await replaySpawnOnMissMessageOnLiveSession(
|
|
27166
|
+
return await replaySpawnOnMissMessageOnLiveSession(
|
|
25686
27167
|
event,
|
|
25687
27168
|
record,
|
|
25688
27169
|
reconnectControl
|
|
25689
27170
|
);
|
|
25690
|
-
return "spawned";
|
|
25691
27171
|
}
|
|
25692
27172
|
if (startResult === void 0 || startResult.ok === false) {
|
|
25693
27173
|
spawnSettle("no_session");
|
|
@@ -25704,10 +27184,10 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25704
27184
|
`could not resume the thread's coding session: ${startError}`,
|
|
25705
27185
|
record.codexThreadId
|
|
25706
27186
|
);
|
|
25707
|
-
return "
|
|
27187
|
+
return "transient_failed";
|
|
25708
27188
|
}
|
|
25709
27189
|
spawnSettle("spawned");
|
|
25710
|
-
return "
|
|
27190
|
+
return "dispatched";
|
|
25711
27191
|
} catch (error) {
|
|
25712
27192
|
spawnSettle("no_session");
|
|
25713
27193
|
log2("runner.spawn_on_miss_failed", {
|
|
@@ -25722,7 +27202,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25722
27202
|
`could not resume the thread's coding session: ${error instanceof Error ? error.message : String(error)}`,
|
|
25723
27203
|
record.codexThreadId
|
|
25724
27204
|
);
|
|
25725
|
-
return "
|
|
27205
|
+
return "transient_failed";
|
|
25726
27206
|
} finally {
|
|
25727
27207
|
spawnSettle("spawned");
|
|
25728
27208
|
if (spawnOnMissInFlight.get(kandanThreadId) === spawnPromise) {
|
|
@@ -25748,6 +27228,26 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25748
27228
|
});
|
|
25749
27229
|
}
|
|
25750
27230
|
};
|
|
27231
|
+
const deadLetterPublisherFor = (record) => {
|
|
27232
|
+
return async ({ sourceSeq, reason, attempts, alreadyPublished }) => {
|
|
27233
|
+
log2("runner.inbound_dead_lettered", {
|
|
27234
|
+
kandanThreadId: record.kandanThreadId,
|
|
27235
|
+
seq: sourceSeq,
|
|
27236
|
+
reason,
|
|
27237
|
+
attempts,
|
|
27238
|
+
alreadyPublished
|
|
27239
|
+
});
|
|
27240
|
+
if (alreadyPublished) {
|
|
27241
|
+
return;
|
|
27242
|
+
}
|
|
27243
|
+
await publishSpawnOnMissMessageState(
|
|
27244
|
+
rehydrationReconnectControl(record),
|
|
27245
|
+
"failed",
|
|
27246
|
+
deadLetterFailedReason(reason),
|
|
27247
|
+
record.codexThreadId
|
|
27248
|
+
);
|
|
27249
|
+
};
|
|
27250
|
+
};
|
|
25751
27251
|
const heartbeatPayload = () => ({
|
|
25752
27252
|
instanceId,
|
|
25753
27253
|
clientId,
|
|
@@ -26238,7 +27738,12 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
26238
27738
|
control,
|
|
26239
27739
|
log2,
|
|
26240
27740
|
attachThreadSession,
|
|
26241
|
-
shouldUseThreadProcesses(options) ? startThreadRunnerProcess : void 0
|
|
27741
|
+
shouldUseThreadProcesses(options) ? startThreadRunnerProcess : void 0,
|
|
27742
|
+
(id) => leaseEpochRegistry.current(id),
|
|
27743
|
+
commitLiveWorkerAcceptedSourceSeq,
|
|
27744
|
+
captureLiveFollowUpEvent,
|
|
27745
|
+
reEnqueueUndeliveredFollowUps,
|
|
27746
|
+
liveSourceSeqAlreadyApplied
|
|
26242
27747
|
);
|
|
26243
27748
|
}),
|
|
26244
27749
|
commitStartTurnWatermark
|
|
@@ -26269,7 +27774,10 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
26269
27774
|
pendingControls,
|
|
26270
27775
|
controlDispatcher,
|
|
26271
27776
|
dispatchControl,
|
|
26272
|
-
|
|
27777
|
+
async () => {
|
|
27778
|
+
await refreshDependencyStatus();
|
|
27779
|
+
await refreshCodexAuthStatus();
|
|
27780
|
+
}
|
|
26273
27781
|
);
|
|
26274
27782
|
return { instanceId, codexUrl, close };
|
|
26275
27783
|
}
|
|
@@ -26283,28 +27791,34 @@ function commanderOutboxPersistDir() {
|
|
|
26283
27791
|
if (override !== void 0 && override !== "") {
|
|
26284
27792
|
return override;
|
|
26285
27793
|
}
|
|
26286
|
-
return
|
|
27794
|
+
return join22(homedir15(), ".linzumi", "commander-outbox");
|
|
27795
|
+
}
|
|
27796
|
+
function controlCursorsDir() {
|
|
27797
|
+
const override = process.env.LINZUMI_CONTROL_CURSOR_DIR?.trim();
|
|
27798
|
+
if (override !== void 0 && override !== "") {
|
|
27799
|
+
return override;
|
|
27800
|
+
}
|
|
27801
|
+
return join22(homedir15(), ".linzumi", "control-cursors");
|
|
26287
27802
|
}
|
|
26288
27803
|
function controlCursorStorePath(runnerId) {
|
|
26289
27804
|
const sanitized = runnerId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
|
|
26290
|
-
const digest =
|
|
27805
|
+
const digest = createHash7("sha256").update(runnerId).digest("hex").slice(0, 8);
|
|
26291
27806
|
const stem = sanitized === "" ? "runner" : sanitized;
|
|
26292
|
-
return
|
|
26293
|
-
homedir14(),
|
|
26294
|
-
".linzumi",
|
|
26295
|
-
"control-cursors",
|
|
26296
|
-
`${stem}.${digest}.json`
|
|
26297
|
-
);
|
|
27807
|
+
return join22(controlCursorsDir(), `${stem}.${digest}.json`);
|
|
26298
27808
|
}
|
|
26299
27809
|
function appliedStartTurnStorePath(runnerId) {
|
|
26300
27810
|
const sanitized = runnerId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
|
|
26301
|
-
const digest =
|
|
27811
|
+
const digest = createHash7("sha256").update(runnerId).digest("hex").slice(0, 8);
|
|
26302
27812
|
const stem = sanitized === "" ? "runner" : sanitized;
|
|
26303
|
-
return
|
|
26304
|
-
|
|
26305
|
-
|
|
26306
|
-
|
|
26307
|
-
|
|
27813
|
+
return join22(controlCursorsDir(), `${stem}.${digest}.start-turn.json`);
|
|
27814
|
+
}
|
|
27815
|
+
function appliedSourceSeqStorePath(runnerId) {
|
|
27816
|
+
const sanitized = runnerId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
|
|
27817
|
+
const digest = createHash7("sha256").update(runnerId).digest("hex").slice(0, 8);
|
|
27818
|
+
const stem = sanitized === "" ? "runner" : sanitized;
|
|
27819
|
+
return join22(
|
|
27820
|
+
controlCursorsDir(),
|
|
27821
|
+
`${stem}.${digest}.source-seq.json`
|
|
26308
27822
|
);
|
|
26309
27823
|
}
|
|
26310
27824
|
function startTurnWatermarkKey(topic, threadId) {
|
|
@@ -26426,6 +27940,7 @@ function createConnectionStalenessTracker(log2, now = Date.now, thresholdMs = co
|
|
|
26426
27940
|
}
|
|
26427
27941
|
function createControlCursorFileStore(path2, log2, options) {
|
|
26428
27942
|
const discardSeqsOnFirstEpochAdoption = options?.discardSeqsOnFirstEpochAdoption === true;
|
|
27943
|
+
const fsyncOnWrite = options?.fsyncOnWrite === true;
|
|
26429
27944
|
const { cursors, epoch: persistedEpoch } = readControlCursorFile(path2, log2);
|
|
26430
27945
|
let epoch = persistedEpoch;
|
|
26431
27946
|
let dirty = false;
|
|
@@ -26436,18 +27951,32 @@ function createControlCursorFileStore(path2, log2, options) {
|
|
|
26436
27951
|
}
|
|
26437
27952
|
dirty = false;
|
|
26438
27953
|
try {
|
|
26439
|
-
|
|
27954
|
+
mkdirSync14(dirname15(path2), { recursive: true });
|
|
26440
27955
|
const tmpPath = `${path2}.tmp`;
|
|
26441
|
-
|
|
26442
|
-
|
|
26443
|
-
|
|
26444
|
-
|
|
26445
|
-
|
|
26446
|
-
|
|
26447
|
-
|
|
26448
|
-
|
|
26449
|
-
|
|
26450
|
-
|
|
27956
|
+
const contents = `${JSON.stringify({
|
|
27957
|
+
...epoch === void 0 ? {} : { [controlCursorEpochKey]: epoch },
|
|
27958
|
+
...Object.fromEntries(cursors)
|
|
27959
|
+
})}
|
|
27960
|
+
`;
|
|
27961
|
+
if (fsyncOnWrite) {
|
|
27962
|
+
const fileFd = openSync5(tmpPath, "w");
|
|
27963
|
+
try {
|
|
27964
|
+
writeFileSync12(fileFd, contents, "utf8");
|
|
27965
|
+
fsyncSync2(fileFd);
|
|
27966
|
+
} finally {
|
|
27967
|
+
closeSync4(fileFd);
|
|
27968
|
+
}
|
|
27969
|
+
renameSync5(tmpPath, path2);
|
|
27970
|
+
const dirFd = openSync5(dirname15(path2), "r");
|
|
27971
|
+
try {
|
|
27972
|
+
fsyncSync2(dirFd);
|
|
27973
|
+
} finally {
|
|
27974
|
+
closeSync4(dirFd);
|
|
27975
|
+
}
|
|
27976
|
+
} else {
|
|
27977
|
+
writeFileSync12(tmpPath, contents, "utf8");
|
|
27978
|
+
renameSync5(tmpPath, path2);
|
|
27979
|
+
}
|
|
26451
27980
|
} catch (error) {
|
|
26452
27981
|
log2("control_cursor_store.write_failed", {
|
|
26453
27982
|
path: path2,
|
|
@@ -26546,7 +28075,7 @@ function readControlCursorFile(path2, log2) {
|
|
|
26546
28075
|
const cursors = /* @__PURE__ */ new Map();
|
|
26547
28076
|
let raw;
|
|
26548
28077
|
try {
|
|
26549
|
-
raw =
|
|
28078
|
+
raw = readFileSync18(path2, "utf8");
|
|
26550
28079
|
} catch (error) {
|
|
26551
28080
|
if (!isErrnoCode2(error, "ENOENT")) {
|
|
26552
28081
|
log2("control_cursor_store.read_failed", {
|
|
@@ -26891,21 +28420,47 @@ function makeRunnerLogger(options) {
|
|
|
26891
28420
|
consoleReporter
|
|
26892
28421
|
);
|
|
26893
28422
|
}
|
|
26894
|
-
function installCleanupHandlers(close) {
|
|
26895
|
-
const
|
|
26896
|
-
|
|
26897
|
-
|
|
28423
|
+
function installCleanupHandlers(close, drainHooks, log2) {
|
|
28424
|
+
const uninstallDrain = installDrainOnSigterm({
|
|
28425
|
+
steps: {
|
|
28426
|
+
stopAccepting: () => {
|
|
28427
|
+
drainHooks.stopAccepting();
|
|
28428
|
+
},
|
|
28429
|
+
// SCOPE NOTE (BET1, partial): checkpointing an in-flight turn on a LIVE
|
|
28430
|
+
// worker (persisting enough codex turn state to resume it) is NOT
|
|
28431
|
+
// implemented in this branch - that turn still relies on the server's
|
|
28432
|
+
// rejoin replay. What IS durable is every durably-owned message that was
|
|
28433
|
+
// ENQUEUED into the per-thread inbound queue (the respawn-window / spawn-on-
|
|
28434
|
+
// miss path): flushInboundQueues fsyncs those below, and the boot/rejoin
|
|
28435
|
+
// drain replays them. Left as an explicit no-op (not silently absent) so the
|
|
28436
|
+
// gap is visible rather than implied-handled.
|
|
28437
|
+
checkpointCurrentTurn: () => void 0,
|
|
28438
|
+
flushInboundQueues: () => {
|
|
28439
|
+
drainHooks.flushInboundQueues();
|
|
28440
|
+
},
|
|
28441
|
+
// The final flush also performs the connection teardown via close(): the
|
|
28442
|
+
// cleanup stack runs after kandan.close() so no advance races the final
|
|
28443
|
+
// cursor/watermark write.
|
|
28444
|
+
flushOutbox: () => close().catch(() => void 0)
|
|
28445
|
+
},
|
|
28446
|
+
deadlineMs: runnerDrainExitDeadlineMs,
|
|
28447
|
+
log: (event, fields) => log2(event, fields),
|
|
28448
|
+
// SIGHUP is a drain-AND-EXIT signal too: a detached child of the Electron
|
|
28449
|
+
// shell that receives a parent hangup must drain its inbound queue + cursor
|
|
28450
|
+
// stores and then EXIT, never linger as a defunct lock-holder that does no
|
|
28451
|
+
// work (the prior close()-without-exit handler left a zombie that stopped
|
|
28452
|
+
// accepting, tore down its connection, yet never exited - stranding prior
|
|
28453
|
+
// jobs and holding the runner lock). installDrainOnSigterm's handler exits
|
|
28454
|
+
// after the drain (deadline-bounded), so routing SIGHUP through it fixes the
|
|
28455
|
+
// orphan-zombie regression.
|
|
28456
|
+
signals: ["SIGTERM", "SIGINT", "SIGHUP"]
|
|
28457
|
+
});
|
|
26898
28458
|
const closeOnExit = () => {
|
|
26899
28459
|
void close().catch(() => void 0);
|
|
26900
28460
|
};
|
|
26901
|
-
process.once("SIGINT", closeAndExit);
|
|
26902
|
-
process.once("SIGTERM", closeAndExit);
|
|
26903
|
-
process.once("SIGHUP", closeAndExit);
|
|
26904
28461
|
process.once("exit", closeOnExit);
|
|
26905
28462
|
return () => {
|
|
26906
|
-
|
|
26907
|
-
process.off("SIGTERM", closeAndExit);
|
|
26908
|
-
process.off("SIGHUP", closeAndExit);
|
|
28463
|
+
uninstallDrain();
|
|
26909
28464
|
process.off("exit", closeOnExit);
|
|
26910
28465
|
};
|
|
26911
28466
|
}
|
|
@@ -27208,7 +28763,7 @@ async function applyCodexUnavailableControl(kandan, topic, instanceId, control,
|
|
|
27208
28763
|
}
|
|
27209
28764
|
throw new Error(error);
|
|
27210
28765
|
}
|
|
27211
|
-
async function applyControl(codex, kandan, topic, instanceId, options, agentProviders, allowedCwds, remoteCodexSandboxRunner, activeRemoteCodexExecRequests, activeClaudeCodeSessions, pendingClaudeCodeApprovals, ensureClaudeCodeForwardPortSession, disposeClaudeCodeForwardPortSession, control, log2, onStartedThread, onThreadProcessStart) {
|
|
28766
|
+
async function applyControl(codex, kandan, topic, instanceId, options, agentProviders, allowedCwds, remoteCodexSandboxRunner, activeRemoteCodexExecRequests, activeClaudeCodeSessions, pendingClaudeCodeApprovals, ensureClaudeCodeForwardPortSession, disposeClaudeCodeForwardPortSession, control, log2, onStartedThread, onThreadProcessStart, leaseEpochFor, onLiveSourceSeqAccepted, captureLiveFollowUpEvent, onUndeliveredFollowUpsAtClose, liveSourceSeqAlreadyApplied) {
|
|
27212
28767
|
if (codex === void 0) {
|
|
27213
28768
|
switch (control.type) {
|
|
27214
28769
|
case "remote_codex_exec":
|
|
@@ -27401,6 +28956,9 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
|
|
|
27401
28956
|
ensureClaudeCodeForwardPortSession,
|
|
27402
28957
|
disposeClaudeCodeForwardPortSession,
|
|
27403
28958
|
resumeSessionId: normalizedWorkDescription(control.resumeSessionId),
|
|
28959
|
+
leaseEpochFor,
|
|
28960
|
+
onLiveSourceSeqAccepted,
|
|
28961
|
+
onUndeliveredFollowUpsAtClose,
|
|
27404
28962
|
setStartupStage: (stage) => {
|
|
27405
28963
|
startupStage = stage;
|
|
27406
28964
|
}
|
|
@@ -27603,6 +29161,51 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
|
|
|
27603
29161
|
message
|
|
27604
29162
|
};
|
|
27605
29163
|
}
|
|
29164
|
+
const followUpThreadId = optionalThreadControlField(
|
|
29165
|
+
control,
|
|
29166
|
+
"threadId"
|
|
29167
|
+
);
|
|
29168
|
+
const followUpChannel = optionalThreadControlField(
|
|
29169
|
+
control,
|
|
29170
|
+
"channel"
|
|
29171
|
+
);
|
|
29172
|
+
if (sourceSeq !== void 0 && followUpThreadId !== void 0 && followUpChannel !== void 0) {
|
|
29173
|
+
captureLiveFollowUpEvent?.(followUpChannel, followUpThreadId, {
|
|
29174
|
+
seq: sourceSeq,
|
|
29175
|
+
type: "thread.message",
|
|
29176
|
+
actorKind: void 0,
|
|
29177
|
+
actorSlug: void 0,
|
|
29178
|
+
actorUserId: void 0,
|
|
29179
|
+
threadId: followUpThreadId,
|
|
29180
|
+
threadTitle: void 0,
|
|
29181
|
+
replyToSeq: integerValue(control.rootSeq),
|
|
29182
|
+
localRunnerEventType: void 0,
|
|
29183
|
+
body: workDescription,
|
|
29184
|
+
attachments
|
|
29185
|
+
});
|
|
29186
|
+
}
|
|
29187
|
+
if (followUpChannel !== void 0 && followUpThreadId !== void 0 && liveSourceSeqAlreadyApplied?.({
|
|
29188
|
+
channel: followUpChannel,
|
|
29189
|
+
threadId: followUpThreadId,
|
|
29190
|
+
sourceSeq
|
|
29191
|
+
}) === true) {
|
|
29192
|
+
log2("claude_code.message_duplicate_ignored", {
|
|
29193
|
+
linzumi_thread_id: followUpThreadId,
|
|
29194
|
+
claude_session_id: codexThreadId,
|
|
29195
|
+
body_preview: claudeConsoleBodyPreview(workDescription),
|
|
29196
|
+
source_seq: sourceSeq ?? null
|
|
29197
|
+
});
|
|
29198
|
+
return {
|
|
29199
|
+
instanceId,
|
|
29200
|
+
controlType: control.type,
|
|
29201
|
+
agentProvider: "claude-code",
|
|
29202
|
+
cwd: cwd.cwd,
|
|
29203
|
+
matchedRoot: cwd.matchedRoot,
|
|
29204
|
+
codexThreadId,
|
|
29205
|
+
queuedInput: true,
|
|
29206
|
+
duplicate: true
|
|
29207
|
+
};
|
|
29208
|
+
}
|
|
27606
29209
|
try {
|
|
27607
29210
|
const enqueueResult = activeSession.enqueueInput({
|
|
27608
29211
|
content: contentResult.content,
|
|
@@ -27707,6 +29310,9 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
|
|
|
27707
29310
|
pendingClaudeCodeApprovals,
|
|
27708
29311
|
ensureClaudeCodeForwardPortSession,
|
|
27709
29312
|
disposeClaudeCodeForwardPortSession,
|
|
29313
|
+
leaseEpochFor,
|
|
29314
|
+
onLiveSourceSeqAccepted,
|
|
29315
|
+
onUndeliveredFollowUpsAtClose,
|
|
27710
29316
|
setStartupStage: (stage) => {
|
|
27711
29317
|
startupStage = stage;
|
|
27712
29318
|
}
|
|
@@ -27750,6 +29356,30 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
|
|
|
27750
29356
|
if (startedThreadSession === void 0 || sourceSeq === void 0) {
|
|
27751
29357
|
throw new Error("cannot reconnect a Kandan thread without a session");
|
|
27752
29358
|
}
|
|
29359
|
+
const reconnectChannel = optionalThreadControlField(control, "channel");
|
|
29360
|
+
const reconnectThreadId = optionalThreadControlField(
|
|
29361
|
+
control,
|
|
29362
|
+
"threadId"
|
|
29363
|
+
);
|
|
29364
|
+
if (reconnectChannel !== void 0 && reconnectThreadId !== void 0 && liveSourceSeqAlreadyApplied?.({
|
|
29365
|
+
channel: reconnectChannel,
|
|
29366
|
+
threadId: reconnectThreadId,
|
|
29367
|
+
sourceSeq
|
|
29368
|
+
}) === true) {
|
|
29369
|
+
log2("kandan.reconnect_thread_seq_already_applied", {
|
|
29370
|
+
linzumi_thread_id: reconnectThreadId,
|
|
29371
|
+
codex_thread_id: codexThreadId,
|
|
29372
|
+
source_seq: sourceSeq
|
|
29373
|
+
});
|
|
29374
|
+
return {
|
|
29375
|
+
instanceId,
|
|
29376
|
+
controlType: control.type,
|
|
29377
|
+
cwd: cwd.cwd,
|
|
29378
|
+
matchedRoot: cwd.matchedRoot,
|
|
29379
|
+
codexThreadId,
|
|
29380
|
+
duplicate: true
|
|
29381
|
+
};
|
|
29382
|
+
}
|
|
27753
29383
|
await resumeCodexThreadForReconnect(
|
|
27754
29384
|
codex,
|
|
27755
29385
|
codexThreadId,
|
|
@@ -28496,11 +30126,13 @@ function claudeCodeTurnStallThresholdMs(env) {
|
|
|
28496
30126
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultClaudeCodeTurnStallThresholdMs;
|
|
28497
30127
|
}
|
|
28498
30128
|
function createClaudeCodeInputQueue(input) {
|
|
30129
|
+
const onSourceSeqDelivered = input.onSourceSeqDelivered;
|
|
28499
30130
|
const pendingMessages = [
|
|
28500
30131
|
claudeCodeUserInputMessage(input.content)
|
|
28501
30132
|
];
|
|
28502
30133
|
const pendingSourceSeqs = [input.sourceSeq];
|
|
28503
30134
|
const deferredFollowUps = [];
|
|
30135
|
+
const deliveredSourceSeqs = /* @__PURE__ */ new Set();
|
|
28504
30136
|
const waiters = [];
|
|
28505
30137
|
const state = {
|
|
28506
30138
|
closed: false
|
|
@@ -28516,6 +30148,10 @@ function createClaudeCodeInputQueue(input) {
|
|
|
28516
30148
|
};
|
|
28517
30149
|
const deliverNow = (message, sourceSeq) => {
|
|
28518
30150
|
pendingSourceSeqs.push(sourceSeq);
|
|
30151
|
+
if (sourceSeq !== void 0 && onSourceSeqDelivered !== void 0 && !deliveredSourceSeqs.has(sourceSeq)) {
|
|
30152
|
+
deliveredSourceSeqs.add(sourceSeq);
|
|
30153
|
+
onSourceSeqDelivered(sourceSeq);
|
|
30154
|
+
}
|
|
28519
30155
|
const waiter = waiters.shift();
|
|
28520
30156
|
if (waiter === void 0) {
|
|
28521
30157
|
pendingMessages.push(message);
|
|
@@ -28584,6 +30220,17 @@ function createClaudeCodeInputQueue(input) {
|
|
|
28584
30220
|
},
|
|
28585
30221
|
enqueue,
|
|
28586
30222
|
enqueueSteer,
|
|
30223
|
+
// BLOCKER A (don't drop deferred follow-ups on a kill): the parked,
|
|
30224
|
+
// never-delivered follow-ups (a turn was in flight at close, so close()'s
|
|
30225
|
+
// drain is intentionally skipped to avoid folding them into the active turn).
|
|
30226
|
+
// Their watermark was NOT committed (commit fires only at deliverNow), so the
|
|
30227
|
+
// runner must re-enqueue them into the DURABLE inbound queue; a kill-before-
|
|
30228
|
+
// run then replays + runs them exactly once. Drains the parked list so a
|
|
30229
|
+
// double-call cannot double-enqueue. Returns the un-run seqs+messages.
|
|
30230
|
+
drainUndeliveredFollowUps: () => {
|
|
30231
|
+
const drained = deferredFollowUps.splice(0);
|
|
30232
|
+
return drained;
|
|
30233
|
+
},
|
|
28587
30234
|
completeTurn: () => {
|
|
28588
30235
|
const completed = pendingSourceSeqs.shift();
|
|
28589
30236
|
if (!state.closed && pendingSourceSeqs.length === 0) {
|
|
@@ -28810,7 +30457,21 @@ async function startClaudeCodeProviderInstance(args) {
|
|
|
28810
30457
|
}
|
|
28811
30458
|
const inputQueue = createClaudeCodeInputQueue({
|
|
28812
30459
|
content: initialContentResult.content,
|
|
28813
|
-
sourceSeq
|
|
30460
|
+
sourceSeq,
|
|
30461
|
+
// BLOCKER A: commit the persisted dedup watermark at DISPATCH (deliverNow),
|
|
30462
|
+
// not at enqueue-accept. enqueueInput used to commit unconditionally right
|
|
30463
|
+
// after inputQueue.enqueue, but enqueue PARKS a mid-turn follow-up in
|
|
30464
|
+
// deferredFollowUps (a bare return) and a close-in-flight drops it - so a
|
|
30465
|
+
// never-run seq was marked applied and its replay was gated 'ignored' (the
|
|
30466
|
+
// drop). Committing only when the seq is actually delivered onto the live
|
|
30467
|
+
// stream means a parked-but-unrun seq is never marked applied.
|
|
30468
|
+
onSourceSeqDelivered: (deliveredSeq) => {
|
|
30469
|
+
args.onLiveSourceSeqAccepted?.({
|
|
30470
|
+
channel,
|
|
30471
|
+
threadId,
|
|
30472
|
+
sourceSeq: deliveredSeq
|
|
30473
|
+
});
|
|
30474
|
+
}
|
|
28814
30475
|
});
|
|
28815
30476
|
const developerPrompt = normalizedWorkDescription(
|
|
28816
30477
|
args.control.developerPrompt
|
|
@@ -28919,7 +30580,8 @@ async function startClaudeCodeProviderInstance(args) {
|
|
|
28919
30580
|
channelSlug: channel,
|
|
28920
30581
|
kandanThreadId: threadId,
|
|
28921
30582
|
codexThreadId: activeSessionId ?? args.resumeSessionId,
|
|
28922
|
-
rootSeq
|
|
30583
|
+
rootSeq,
|
|
30584
|
+
leaseEpoch: args.leaseEpochFor?.(threadId)
|
|
28923
30585
|
}),
|
|
28924
30586
|
push: async (event, payload, _timeoutMs, onSent) => {
|
|
28925
30587
|
const reply = await args.kandan.push(args.topic, event, payload, onSent);
|
|
@@ -28967,7 +30629,7 @@ async function startClaudeCodeProviderInstance(args) {
|
|
|
28967
30629
|
log: args.log
|
|
28968
30630
|
});
|
|
28969
30631
|
const liveBashCapture = args.options.claudeCodeRunner === void 0 && claudeLiveBashCaptureEnabled(process.env) ? createClaudeCodeLiveBashCapture({
|
|
28970
|
-
captureDir:
|
|
30632
|
+
captureDir: join22(
|
|
28971
30633
|
tmpdir3(),
|
|
28972
30634
|
`linzumi-claude-live-bash-${args.instanceId}`
|
|
28973
30635
|
),
|
|
@@ -29086,6 +30748,7 @@ async function startClaudeCodeProviderInstance(args) {
|
|
|
29086
30748
|
linzumi_thread_id: threadId,
|
|
29087
30749
|
claude_session_id: event.sessionId
|
|
29088
30750
|
});
|
|
30751
|
+
args.onLiveSourceSeqAccepted?.({ channel, threadId, sourceSeq });
|
|
29089
30752
|
args.activeClaudeCodeSessions.set(event.sessionId, {
|
|
29090
30753
|
abortController,
|
|
29091
30754
|
workspace,
|
|
@@ -29294,6 +30957,15 @@ async function startClaudeCodeProviderInstance(args) {
|
|
|
29294
30957
|
onTranscriptEvent
|
|
29295
30958
|
});
|
|
29296
30959
|
} finally {
|
|
30960
|
+
const undeliveredFollowUps = inputQueue.drainUndeliveredFollowUps();
|
|
30961
|
+
const undeliveredSeqs = undeliveredFollowUps.map((followUp) => followUp.sourceSeq).filter((seq) => seq !== void 0);
|
|
30962
|
+
if (undeliveredSeqs.length > 0 && args.onUndeliveredFollowUpsAtClose !== void 0) {
|
|
30963
|
+
args.onUndeliveredFollowUpsAtClose({
|
|
30964
|
+
channel,
|
|
30965
|
+
threadId,
|
|
30966
|
+
sourceSeqs: undeliveredSeqs
|
|
30967
|
+
});
|
|
30968
|
+
}
|
|
29297
30969
|
inputQueue.close();
|
|
29298
30970
|
mcpAuthCleanup?.();
|
|
29299
30971
|
liveBashCapture?.close();
|
|
@@ -29817,15 +31489,15 @@ function rehydrationControlSnapshot(args) {
|
|
|
29817
31489
|
...runtimeSettings.fast === void 0 ? {} : { fast: runtimeSettings.fast }
|
|
29818
31490
|
};
|
|
29819
31491
|
}
|
|
29820
|
-
function
|
|
29821
|
-
|
|
29822
|
-
|
|
29823
|
-
|
|
29824
|
-
|
|
29825
|
-
|
|
29826
|
-
|
|
29827
|
-
|
|
29828
|
-
}
|
|
31492
|
+
function deadLetterFailedReason(reason) {
|
|
31493
|
+
switch (reason) {
|
|
31494
|
+
case "exhausted":
|
|
31495
|
+
return "couldn't resume this job after several attempts; please resend your last message to continue.";
|
|
31496
|
+
case "withheld":
|
|
31497
|
+
return "this message may have already run before an unexpected shutdown; it was not re-run to avoid duplicating work. Please resend if it did not complete.";
|
|
31498
|
+
case "permanent":
|
|
31499
|
+
return "couldn't resume this job's coding session.";
|
|
31500
|
+
}
|
|
29829
31501
|
}
|
|
29830
31502
|
function rehydrationReconnectControl(record) {
|
|
29831
31503
|
const snapshot = record.control;
|
|
@@ -30038,6 +31710,9 @@ function threadRunnerEntryLiveness(entry) {
|
|
|
30038
31710
|
if (ipcClosed) {
|
|
30039
31711
|
return "dead";
|
|
30040
31712
|
}
|
|
31713
|
+
if (handle.sessionClosed?.() === true) {
|
|
31714
|
+
return "dead";
|
|
31715
|
+
}
|
|
30041
31716
|
return processIsAlive2(handle.processPid) ? "live" : "dead";
|
|
30042
31717
|
}
|
|
30043
31718
|
async function closeThreadRunnerEntry(entry) {
|
|
@@ -30095,6 +31770,18 @@ async function spawnLocalThreadRunnerProcess(options) {
|
|
|
30095
31770
|
});
|
|
30096
31771
|
const exitListeners = /* @__PURE__ */ new Set();
|
|
30097
31772
|
const exitState = { exited: false };
|
|
31773
|
+
const sessionState = { closed: false };
|
|
31774
|
+
child.on("message", (message) => {
|
|
31775
|
+
if (isThreadCodexWorkerSessionClosedMessage(message)) {
|
|
31776
|
+
sessionState.closed = true;
|
|
31777
|
+
writeCliAuditEvent("runner.thread_runner_session_detached", {
|
|
31778
|
+
purpose: "linzumi.thread_runner",
|
|
31779
|
+
kandanThreadId: message.kandanThreadId,
|
|
31780
|
+
reason: message.reason,
|
|
31781
|
+
pid: child.pid
|
|
31782
|
+
});
|
|
31783
|
+
}
|
|
31784
|
+
});
|
|
30098
31785
|
child.once("exit", (code, signal) => {
|
|
30099
31786
|
writeCliAuditEvent("process.exit", {
|
|
30100
31787
|
command: process.execPath,
|
|
@@ -30170,6 +31857,7 @@ async function spawnLocalThreadRunnerProcess(options) {
|
|
|
30170
31857
|
codex: connectThreadCodexWorkerIpc(child),
|
|
30171
31858
|
processPid: child.pid,
|
|
30172
31859
|
commanderManagedPorts: ready2.commanderManagedPorts,
|
|
31860
|
+
sessionClosed: () => sessionState.closed,
|
|
30173
31861
|
onExit: (listener) => {
|
|
30174
31862
|
if (exitState.exited) {
|
|
30175
31863
|
queueMicrotask(listener);
|
|
@@ -30857,8 +32545,8 @@ function onboardingConversationOfficeImport(response) {
|
|
|
30857
32545
|
return objectValue(metadata?.office_channel_import);
|
|
30858
32546
|
}
|
|
30859
32547
|
function onboardingConversationImportClientMessageId(importKey, itemKey) {
|
|
30860
|
-
const importHash =
|
|
30861
|
-
const itemHash =
|
|
32548
|
+
const importHash = createHash7("sha256").update(importKey).digest("hex");
|
|
32549
|
+
const itemHash = createHash7("sha256").update(itemKey).digest("hex");
|
|
30862
32550
|
return `onboarding-import:${importHash.slice(0, 32)}:${itemHash.slice(0, 24)}`;
|
|
30863
32551
|
}
|
|
30864
32552
|
async function uploadLocalConversationAttachments(args) {
|
|
@@ -30956,7 +32644,7 @@ async function localConversationAttachmentFile(candidatePath, attachment) {
|
|
|
30956
32644
|
};
|
|
30957
32645
|
}
|
|
30958
32646
|
case "path": {
|
|
30959
|
-
const path2 = isAbsolute5(attachment.path) ? attachment.path : resolve8(
|
|
32647
|
+
const path2 = isAbsolute5(attachment.path) ? attachment.path : resolve8(dirname15(candidatePath), attachment.path);
|
|
30960
32648
|
try {
|
|
30961
32649
|
const bytes = await readFile2(path2);
|
|
30962
32650
|
return {
|
|
@@ -31411,9 +33099,9 @@ function mcpOwnerUsername(options) {
|
|
|
31411
33099
|
return options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
|
|
31412
33100
|
}
|
|
31413
33101
|
function writeEphemeralMcpAuthFile(options) {
|
|
31414
|
-
const directory = mkdtempSync4(
|
|
33102
|
+
const directory = mkdtempSync4(join22(tmpdir3(), "linzumi-mcp-auth-"));
|
|
31415
33103
|
chmodSync2(directory, 448);
|
|
31416
|
-
const path2 =
|
|
33104
|
+
const path2 = join22(directory, "auth.json");
|
|
31417
33105
|
writeCachedLocalRunnerToken({
|
|
31418
33106
|
kandanUrl: options.kandanUrl,
|
|
31419
33107
|
accessToken: options.token,
|
|
@@ -31489,7 +33177,7 @@ function configuredAllowedCwds(values, options = {}) {
|
|
|
31489
33177
|
const absolutePath = resolve8(expandUserPath(value));
|
|
31490
33178
|
try {
|
|
31491
33179
|
if (options.createMissing === true) {
|
|
31492
|
-
|
|
33180
|
+
mkdirSync14(absolutePath, { recursive: true });
|
|
31493
33181
|
}
|
|
31494
33182
|
const realPath = realpathSync6(absolutePath);
|
|
31495
33183
|
allowedCwds.push(
|
|
@@ -31526,7 +33214,7 @@ function allowedCwdProjects(allowedCwds) {
|
|
|
31526
33214
|
});
|
|
31527
33215
|
}
|
|
31528
33216
|
function isGitProjectDirectory(cwd) {
|
|
31529
|
-
const gitPath =
|
|
33217
|
+
const gitPath = join22(cwd, ".git");
|
|
31530
33218
|
try {
|
|
31531
33219
|
const gitPathStats = statSync3(gitPath);
|
|
31532
33220
|
return gitPathStats.isDirectory() || gitPathStats.isFile();
|
|
@@ -31549,9 +33237,9 @@ function browseRunnerDirectory(control, options) {
|
|
|
31549
33237
|
error: "not_directory"
|
|
31550
33238
|
};
|
|
31551
33239
|
}
|
|
31552
|
-
const parent =
|
|
33240
|
+
const parent = dirname15(currentPath);
|
|
31553
33241
|
const entries = readdirSync4(currentPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => visibleRunnerDirectoryEntryName(entry.name)).map((entry) => {
|
|
31554
|
-
const path2 =
|
|
33242
|
+
const path2 = join22(currentPath, entry.name);
|
|
31555
33243
|
return {
|
|
31556
33244
|
name: entry.name,
|
|
31557
33245
|
path: path2,
|
|
@@ -31592,7 +33280,7 @@ function projectDirectoryName(name) {
|
|
|
31592
33280
|
function availableProjectDirectoryName(projectsRoot, baseName, suffix = 0) {
|
|
31593
33281
|
for (let nextSuffix = suffix; ; nextSuffix += 1) {
|
|
31594
33282
|
const candidate = nextSuffix === 0 ? baseName : `${baseName}-${nextSuffix}`;
|
|
31595
|
-
if (!projectPathExists(
|
|
33283
|
+
if (!projectPathExists(join22(projectsRoot, candidate))) {
|
|
31596
33284
|
return candidate;
|
|
31597
33285
|
}
|
|
31598
33286
|
}
|
|
@@ -31620,9 +33308,9 @@ function createRunnerProject(control, options, allowedCwds) {
|
|
|
31620
33308
|
error: "invalid_project_template"
|
|
31621
33309
|
};
|
|
31622
33310
|
}
|
|
31623
|
-
const projectsRoot =
|
|
33311
|
+
const projectsRoot = join22(currentHomeDirectory(), "linzumi");
|
|
31624
33312
|
const resolvedProjectDirName = template === "hello_linzumi_demo" ? availableProjectDirectoryName(projectsRoot, projectDirName) : projectDirName;
|
|
31625
|
-
const projectPath =
|
|
33313
|
+
const projectPath = join22(projectsRoot, resolvedProjectDirName);
|
|
31626
33314
|
let createdProjectPath = false;
|
|
31627
33315
|
try {
|
|
31628
33316
|
if (template !== "hello_linzumi_demo" && projectPathExists(projectPath)) {
|
|
@@ -31634,7 +33322,7 @@ function createRunnerProject(control, options, allowedCwds) {
|
|
|
31634
33322
|
error: "project_directory_exists"
|
|
31635
33323
|
};
|
|
31636
33324
|
}
|
|
31637
|
-
|
|
33325
|
+
mkdirSync14(projectsRoot, { recursive: true });
|
|
31638
33326
|
if (template === "hello_linzumi_demo") {
|
|
31639
33327
|
createdProjectPath = true;
|
|
31640
33328
|
createHelloLinzumiProject({
|
|
@@ -31642,7 +33330,7 @@ function createRunnerProject(control, options, allowedCwds) {
|
|
|
31642
33330
|
name: resolvedProjectDirName
|
|
31643
33331
|
});
|
|
31644
33332
|
} else {
|
|
31645
|
-
|
|
33333
|
+
mkdirSync14(projectPath, { recursive: false });
|
|
31646
33334
|
createdProjectPath = true;
|
|
31647
33335
|
}
|
|
31648
33336
|
const git = spawnSync5("git", ["init"], {
|
|
@@ -31755,7 +33443,7 @@ async function suggestRunnerTasks(control, options, allowedCwds) {
|
|
|
31755
33443
|
};
|
|
31756
33444
|
}
|
|
31757
33445
|
}
|
|
31758
|
-
var THREAD_RUNNER_READY_TIMEOUT_MS, THREAD_RUNNER_REAPER_INTERVAL_MS, onboardingDiscoveryAgentStartKeys, onboardingConversationImportKeys, onboardingConversationDiscoveryReportLimit, staleStartInstanceWindowMs, connectionStaleThresholdMs, connectionUnrecoverableAfterMs, controlCursorWriteDebounceMs, controlCursorEpochKey, codexTurnTerminalNotificationMethods, claudeSessionStoreSequenceHighWater, claudeCodeLlmProxySessionHeader, onboardingConversationTitleFirstMessages, onboardingConversationTitleLastMessages, onboardingConversationTitleBodyMaxLength, onboardingConversationImportMessageMaxLength, onboardingConversationImportCandidateLimit, onboardingConversationImportProgressInterval;
|
|
33446
|
+
var THREAD_RUNNER_READY_TIMEOUT_MS, THREAD_RUNNER_REAPER_INTERVAL_MS, onboardingDiscoveryAgentStartKeys, onboardingConversationImportKeys, onboardingConversationDiscoveryReportLimit, CODEX_AUTH_STATUS_PROBE_TIMEOUT_MS, staleStartInstanceWindowMs, connectionStaleThresholdMs, connectionUnrecoverableAfterMs, controlCursorWriteDebounceMs, controlCursorEpochKey, codexTurnTerminalNotificationMethods, runnerDrainExitDeadlineMs, claudeSessionStoreSequenceHighWater, claudeCodeLlmProxySessionHeader, onboardingConversationTitleFirstMessages, onboardingConversationTitleLastMessages, onboardingConversationTitleBodyMaxLength, onboardingConversationImportMessageMaxLength, onboardingConversationImportCandidateLimit, onboardingConversationImportProgressInterval;
|
|
31759
33447
|
var init_runner = __esm({
|
|
31760
33448
|
"src/runner.ts"() {
|
|
31761
33449
|
"use strict";
|
|
@@ -31763,6 +33451,7 @@ var init_runner = __esm({
|
|
|
31763
33451
|
init_runnerThreadSessionRegistry();
|
|
31764
33452
|
init_outboxJournal();
|
|
31765
33453
|
init_channelSessionSupport();
|
|
33454
|
+
init_channelSessionSupport();
|
|
31766
33455
|
init_commanderAttachments();
|
|
31767
33456
|
init_claudeCodePipeline();
|
|
31768
33457
|
init_claudeCodePlanMirror();
|
|
@@ -31805,11 +33494,18 @@ var init_runner = __esm({
|
|
|
31805
33494
|
init_signupTaskSuggestions();
|
|
31806
33495
|
init_userFacingErrors();
|
|
31807
33496
|
init_remoteCodexSandboxRunner();
|
|
33497
|
+
init_durableInboundQueue();
|
|
33498
|
+
init_appliedSourceSeqWatermark();
|
|
33499
|
+
init_threadReconcilerLock();
|
|
33500
|
+
init_runnerLifecycleGuards();
|
|
33501
|
+
init_leaseEpochGuard();
|
|
33502
|
+
init_resumeKeystone();
|
|
31808
33503
|
THREAD_RUNNER_READY_TIMEOUT_MS = 3e4;
|
|
31809
33504
|
THREAD_RUNNER_REAPER_INTERVAL_MS = 3e4;
|
|
31810
33505
|
onboardingDiscoveryAgentStartKeys = /* @__PURE__ */ new Set();
|
|
31811
33506
|
onboardingConversationImportKeys = /* @__PURE__ */ new Set();
|
|
31812
33507
|
onboardingConversationDiscoveryReportLimit = 100;
|
|
33508
|
+
CODEX_AUTH_STATUS_PROBE_TIMEOUT_MS = 3e3;
|
|
31813
33509
|
staleStartInstanceWindowMs = 6e4;
|
|
31814
33510
|
connectionStaleThresholdMs = 12e4;
|
|
31815
33511
|
connectionUnrecoverableAfterMs = 10 * 60 * 1e3;
|
|
@@ -31822,6 +33518,7 @@ var init_runner = __esm({
|
|
|
31822
33518
|
"turn/canceled",
|
|
31823
33519
|
"turn/cancelled"
|
|
31824
33520
|
]);
|
|
33521
|
+
runnerDrainExitDeadlineMs = 8e3;
|
|
31825
33522
|
claudeSessionStoreSequenceHighWater = /* @__PURE__ */ new Map();
|
|
31826
33523
|
claudeCodeLlmProxySessionHeader = "x-linzumi-llm-proxy-token";
|
|
31827
33524
|
onboardingConversationTitleFirstMessages = 4;
|
|
@@ -31834,7 +33531,7 @@ var init_runner = __esm({
|
|
|
31834
33531
|
});
|
|
31835
33532
|
|
|
31836
33533
|
// src/kandanTls.ts
|
|
31837
|
-
import { existsSync as existsSync14, readFileSync as
|
|
33534
|
+
import { existsSync as existsSync14, readFileSync as readFileSync19 } from "node:fs";
|
|
31838
33535
|
import { Agent } from "undici";
|
|
31839
33536
|
import WsWebSocket from "ws";
|
|
31840
33537
|
function kandanTlsTrustFromEnv() {
|
|
@@ -31848,7 +33545,7 @@ function kandanTlsTrustFromCaFile(caFile) {
|
|
|
31848
33545
|
if (!existsSync14(trimmed)) {
|
|
31849
33546
|
throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
|
|
31850
33547
|
}
|
|
31851
|
-
const ca =
|
|
33548
|
+
const ca = readFileSync19(trimmed, "utf8");
|
|
31852
33549
|
return {
|
|
31853
33550
|
caFile: trimmed,
|
|
31854
33551
|
ca,
|
|
@@ -50632,7 +52329,7 @@ var init_RemoveFileError = __esm({
|
|
|
50632
52329
|
|
|
50633
52330
|
// ../../node_modules/@inquirer/external-editor/dist/esm/index.js
|
|
50634
52331
|
import { spawn as spawn11, spawnSync as spawnSync6 } from "child_process";
|
|
50635
|
-
import { readFileSync as
|
|
52332
|
+
import { readFileSync as readFileSync22, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "fs";
|
|
50636
52333
|
import path from "node:path";
|
|
50637
52334
|
import os from "node:os";
|
|
50638
52335
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
@@ -50756,7 +52453,7 @@ var init_esm5 = __esm({
|
|
|
50756
52453
|
}
|
|
50757
52454
|
readTemporaryFile() {
|
|
50758
52455
|
try {
|
|
50759
|
-
const tempFileBuffer =
|
|
52456
|
+
const tempFileBuffer = readFileSync22(this.tempFile);
|
|
50760
52457
|
if (tempFileBuffer.length === 0) {
|
|
50761
52458
|
this.text = "";
|
|
50762
52459
|
} else {
|
|
@@ -52125,17 +53822,17 @@ import { spawn as spawn12, spawnSync as spawnSync7 } from "node:child_process";
|
|
|
52125
53822
|
import {
|
|
52126
53823
|
existsSync as existsSync17,
|
|
52127
53824
|
constants as fsConstants,
|
|
52128
|
-
mkdirSync as
|
|
53825
|
+
mkdirSync as mkdirSync17,
|
|
52129
53826
|
mkdtempSync as mkdtempSync6,
|
|
52130
|
-
readFileSync as
|
|
53827
|
+
readFileSync as readFileSync23,
|
|
52131
53828
|
readdirSync as readdirSync5,
|
|
52132
53829
|
rmSync as rmSync7,
|
|
52133
53830
|
statSync as statSync4,
|
|
52134
53831
|
writeFileSync as writeFileSync16
|
|
52135
53832
|
} from "node:fs";
|
|
52136
53833
|
import { access } from "node:fs/promises";
|
|
52137
|
-
import { homedir as
|
|
52138
|
-
import { delimiter as delimiter3, dirname as
|
|
53834
|
+
import { homedir as homedir18, tmpdir as tmpdir5 } from "node:os";
|
|
53835
|
+
import { delimiter as delimiter3, dirname as dirname19, join as join26, resolve as resolve10 } from "node:path";
|
|
52139
53836
|
import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
|
|
52140
53837
|
import { emitKeypressEvents } from "node:readline";
|
|
52141
53838
|
function signupHelpText() {
|
|
@@ -52256,7 +53953,7 @@ function defaultSignupDraftStore(serviceUrl) {
|
|
|
52256
53953
|
}
|
|
52257
53954
|
let parsed;
|
|
52258
53955
|
try {
|
|
52259
|
-
parsed = JSON.parse(
|
|
53956
|
+
parsed = JSON.parse(readFileSync23(path2, "utf8"));
|
|
52260
53957
|
} catch (_error) {
|
|
52261
53958
|
return void 0;
|
|
52262
53959
|
}
|
|
@@ -52266,7 +53963,7 @@ function defaultSignupDraftStore(serviceUrl) {
|
|
|
52266
53963
|
return comparableServiceUrl(parsed.serviceUrl) === comparableServiceUrl(serviceUrl) ? parsed : void 0;
|
|
52267
53964
|
},
|
|
52268
53965
|
write: (draft) => {
|
|
52269
|
-
|
|
53966
|
+
mkdirSync17(dirname19(path2), { recursive: true });
|
|
52270
53967
|
writeFileSync16(path2, `${JSON.stringify(draft, null, 2)}
|
|
52271
53968
|
`, {
|
|
52272
53969
|
encoding: "utf8",
|
|
@@ -52279,8 +53976,8 @@ function defaultSignupDraftStore(serviceUrl) {
|
|
|
52279
53976
|
};
|
|
52280
53977
|
}
|
|
52281
53978
|
function defaultSignupDraftPath(serviceUrl) {
|
|
52282
|
-
return
|
|
52283
|
-
|
|
53979
|
+
return join26(
|
|
53980
|
+
dirname19(localConfigPath()),
|
|
52284
53981
|
`signup-draft.${localConfigScopeFileStem(serviceUrl)}.json`
|
|
52285
53982
|
);
|
|
52286
53983
|
}
|
|
@@ -52314,7 +54011,7 @@ function defaultSignupTaskCacheStore(serviceUrl) {
|
|
|
52314
54011
|
}
|
|
52315
54012
|
}
|
|
52316
54013
|
};
|
|
52317
|
-
|
|
54014
|
+
mkdirSync17(dirname19(path2), { recursive: true });
|
|
52318
54015
|
writeFileSync16(path2, `${JSON.stringify(next, null, 2)}
|
|
52319
54016
|
`, {
|
|
52320
54017
|
encoding: "utf8",
|
|
@@ -52324,8 +54021,8 @@ function defaultSignupTaskCacheStore(serviceUrl) {
|
|
|
52324
54021
|
};
|
|
52325
54022
|
}
|
|
52326
54023
|
function defaultSignupTaskCachePath(serviceUrl) {
|
|
52327
|
-
return
|
|
52328
|
-
|
|
54024
|
+
return join26(
|
|
54025
|
+
dirname19(localConfigPath()),
|
|
52329
54026
|
`signup-task-cache.${localConfigScopeFileStem(serviceUrl)}.json`
|
|
52330
54027
|
);
|
|
52331
54028
|
}
|
|
@@ -52335,7 +54032,7 @@ function readSignupTaskCache(path2) {
|
|
|
52335
54032
|
}
|
|
52336
54033
|
let parsed;
|
|
52337
54034
|
try {
|
|
52338
|
-
parsed = JSON.parse(
|
|
54035
|
+
parsed = JSON.parse(readFileSync23(path2, "utf8"));
|
|
52339
54036
|
} catch (_error) {
|
|
52340
54037
|
return { version: 1, entries: {} };
|
|
52341
54038
|
}
|
|
@@ -54220,7 +55917,7 @@ function autocompletePathSuggestions(pathInput) {
|
|
|
54220
55917
|
try {
|
|
54221
55918
|
const showHidden = prefix.startsWith(".");
|
|
54222
55919
|
const currentPath = directoryExists(normalizedInput) ? [normalizedInput.replace(/\/$/, "") || "/"] : [];
|
|
54223
|
-
const childPaths = readdirSync5(directoryPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => showHidden || !entry.name.startsWith(".")).map((entry) =>
|
|
55920
|
+
const childPaths = readdirSync5(directoryPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => showHidden || !entry.name.startsWith(".")).map((entry) => join26(directoryPath, entry.name)).map((path2) => ({
|
|
54224
55921
|
path: path2,
|
|
54225
55922
|
score: fuzzyPathSegmentScore(path2.split("/").at(-1) ?? path2, prefix)
|
|
54226
55923
|
})).filter((candidate) => candidate.score !== void 0).sort((left, right) => {
|
|
@@ -54466,7 +56163,7 @@ async function runSignupPreflights(runtime) {
|
|
|
54466
56163
|
function defaultPreflightRuntime() {
|
|
54467
56164
|
return {
|
|
54468
56165
|
cwd: process.cwd(),
|
|
54469
|
-
homeDir:
|
|
56166
|
+
homeDir: homedir18(),
|
|
54470
56167
|
probeTool,
|
|
54471
56168
|
readGitEmail,
|
|
54472
56169
|
discoverCodeRoots,
|
|
@@ -54611,9 +56308,9 @@ async function suggestTasksWithCodex(projectPath, retryPolicy) {
|
|
|
54611
56308
|
);
|
|
54612
56309
|
}
|
|
54613
56310
|
async function runCodexTaskSuggestion2(projectPath, previousResponse, retryPolicy) {
|
|
54614
|
-
const tempRoot = mkdtempSync6(
|
|
54615
|
-
const schemaPath =
|
|
54616
|
-
const outputPath =
|
|
56311
|
+
const tempRoot = mkdtempSync6(join26(tmpdir5(), "linzumi-signup-codex-tasks-"));
|
|
56312
|
+
const schemaPath = join26(tempRoot, "task-suggestions.schema.json");
|
|
56313
|
+
const outputPath = join26(tempRoot, "task-suggestions.json");
|
|
54617
56314
|
const model = process.env.LINZUMI_SIGNUP_TASK_MODEL ?? "gpt-5.4-mini";
|
|
54618
56315
|
const prompt = taskSuggestionPrompt2(previousResponse);
|
|
54619
56316
|
const codexCommand = await resolveSignupCodexCommand();
|
|
@@ -54638,7 +56335,7 @@ async function runCodexTaskSuggestion2(projectPath, previousResponse, retryPolic
|
|
|
54638
56335
|
),
|
|
54639
56336
|
retryPolicy
|
|
54640
56337
|
);
|
|
54641
|
-
return
|
|
56338
|
+
return readFileSync23(outputPath, "utf8");
|
|
54642
56339
|
} finally {
|
|
54643
56340
|
rmSync7(tempRoot, { recursive: true, force: true });
|
|
54644
56341
|
}
|
|
@@ -54675,7 +56372,7 @@ function codexTaskSuggestionProcess2(args) {
|
|
|
54675
56372
|
function signupCodexTaskSuggestionProcessForTest(args) {
|
|
54676
56373
|
return codexTaskSuggestionProcess2(args);
|
|
54677
56374
|
}
|
|
54678
|
-
async function resolveSignupCodexCommand(env = process.env, homeDir =
|
|
56375
|
+
async function resolveSignupCodexCommand(env = process.env, homeDir = homedir18(), executableExists = fileIsExecutable) {
|
|
54679
56376
|
const override = firstConfiguredValue([
|
|
54680
56377
|
env.LINZUMI_SIGNUP_CODEX_BIN,
|
|
54681
56378
|
env.LINZUMI_CODEX_BIN
|
|
@@ -54730,7 +56427,7 @@ function resolveHomePath(path2, homeDir) {
|
|
|
54730
56427
|
return homeDir;
|
|
54731
56428
|
}
|
|
54732
56429
|
if (path2.startsWith("~/")) {
|
|
54733
|
-
return
|
|
56430
|
+
return join26(homeDir, path2.slice(2));
|
|
54734
56431
|
}
|
|
54735
56432
|
return resolve10(path2);
|
|
54736
56433
|
}
|
|
@@ -54743,9 +56440,9 @@ function commandLooksPathLike(command) {
|
|
|
54743
56440
|
}
|
|
54744
56441
|
function homeManagedCodexCandidates(homeDir) {
|
|
54745
56442
|
return [
|
|
54746
|
-
|
|
54747
|
-
|
|
54748
|
-
|
|
56443
|
+
join26(homeDir, ".volta", "bin", "codex"),
|
|
56444
|
+
join26(homeDir, ".local", "bin", "codex"),
|
|
56445
|
+
join26(homeDir, "bin", "codex")
|
|
54749
56446
|
];
|
|
54750
56447
|
}
|
|
54751
56448
|
async function firstExecutablePath(paths, executableExists) {
|
|
@@ -54760,7 +56457,7 @@ function commandPathCandidates(path2) {
|
|
|
54760
56457
|
if (path2 === void 0 || path2.trim() === "") {
|
|
54761
56458
|
return [];
|
|
54762
56459
|
}
|
|
54763
|
-
return path2.split(delimiter3).filter((entry) => entry.trim() !== "").map((entry) =>
|
|
56460
|
+
return path2.split(delimiter3).filter((entry) => entry.trim() !== "").map((entry) => join26(entry, "codex"));
|
|
54764
56461
|
}
|
|
54765
56462
|
async function fileIsExecutable(path2) {
|
|
54766
56463
|
try {
|
|
@@ -54993,11 +56690,11 @@ function codexPreflightLocation(command, homeDir) {
|
|
|
54993
56690
|
switch (command) {
|
|
54994
56691
|
case "codex":
|
|
54995
56692
|
return "on PATH";
|
|
54996
|
-
case
|
|
56693
|
+
case join26(homeDir, ".volta", "bin", "codex"):
|
|
54997
56694
|
return "via Volta";
|
|
54998
|
-
case
|
|
56695
|
+
case join26(homeDir, ".local", "bin", "codex"):
|
|
54999
56696
|
return "via ~/.local/bin";
|
|
55000
|
-
case
|
|
56697
|
+
case join26(homeDir, "bin", "codex"):
|
|
55001
56698
|
return "via ~/bin";
|
|
55002
56699
|
default:
|
|
55003
56700
|
return "from configured path";
|
|
@@ -55156,7 +56853,7 @@ function probeToolWithArgs(command, args, cwd) {
|
|
|
55156
56853
|
}
|
|
55157
56854
|
async function discoverCodeRoots(homeDir) {
|
|
55158
56855
|
const candidates = ["src", "code", "projects"].map(
|
|
55159
|
-
(name) =>
|
|
56856
|
+
(name) => join26(homeDir, name)
|
|
55160
56857
|
);
|
|
55161
56858
|
return candidates.filter((path2) => existsSync17(path2)).flatMap((path2) => discoveredProjectNames(path2));
|
|
55162
56859
|
}
|
|
@@ -55210,7 +56907,7 @@ function discoverProjectsFromCurrentDirectory(cwd) {
|
|
|
55210
56907
|
}
|
|
55211
56908
|
function discoverProjectsFromGuessedRoots(homeDir) {
|
|
55212
56909
|
const guessedRoots = ["src", "code", "projects"].map(
|
|
55213
|
-
(name) =>
|
|
56910
|
+
(name) => join26(homeDir, name)
|
|
55214
56911
|
);
|
|
55215
56912
|
const projects = guessedRoots.flatMap(
|
|
55216
56913
|
(root) => discoverProjectsUnderRoot(root)
|
|
@@ -55262,25 +56959,25 @@ function looksLikeProject(path2) {
|
|
|
55262
56959
|
"pnpm-lock.yaml",
|
|
55263
56960
|
"yarn.lock",
|
|
55264
56961
|
"package-lock.json"
|
|
55265
|
-
].some((name) => existsSync17(
|
|
56962
|
+
].some((name) => existsSync17(join26(path2, name)));
|
|
55266
56963
|
}
|
|
55267
56964
|
function detectProjectLanguage(path2) {
|
|
55268
|
-
if (existsSync17(
|
|
56965
|
+
if (existsSync17(join26(path2, "pyproject.toml")) || existsSync17(join26(path2, "requirements.txt"))) {
|
|
55269
56966
|
return "Python";
|
|
55270
56967
|
}
|
|
55271
|
-
if (existsSync17(
|
|
56968
|
+
if (existsSync17(join26(path2, "Cargo.toml"))) {
|
|
55272
56969
|
return "Rust";
|
|
55273
56970
|
}
|
|
55274
|
-
if (existsSync17(
|
|
56971
|
+
if (existsSync17(join26(path2, "go.mod"))) {
|
|
55275
56972
|
return "Go";
|
|
55276
56973
|
}
|
|
55277
|
-
if (existsSync17(
|
|
56974
|
+
if (existsSync17(join26(path2, "mix.exs"))) {
|
|
55278
56975
|
return "Elixir";
|
|
55279
56976
|
}
|
|
55280
|
-
if (existsSync17(
|
|
56977
|
+
if (existsSync17(join26(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
|
|
55281
56978
|
return "TypeScript";
|
|
55282
56979
|
}
|
|
55283
|
-
if (existsSync17(
|
|
56980
|
+
if (existsSync17(join26(path2, "package.json"))) {
|
|
55284
56981
|
return "JavaScript";
|
|
55285
56982
|
}
|
|
55286
56983
|
return "Project";
|
|
@@ -55288,7 +56985,7 @@ function detectProjectLanguage(path2) {
|
|
|
55288
56985
|
function packageJsonMentionsTypeScript(path2) {
|
|
55289
56986
|
try {
|
|
55290
56987
|
const packageJson2 = JSON.parse(
|
|
55291
|
-
|
|
56988
|
+
readFileSync23(join26(path2, "package.json"), "utf8")
|
|
55292
56989
|
);
|
|
55293
56990
|
return packageJson2.dependencies?.typescript !== void 0 || packageJson2.devDependencies?.typescript !== void 0;
|
|
55294
56991
|
} catch {
|
|
@@ -55296,11 +56993,11 @@ function packageJsonMentionsTypeScript(path2) {
|
|
|
55296
56993
|
}
|
|
55297
56994
|
}
|
|
55298
56995
|
function hasGitMetadata(path2) {
|
|
55299
|
-
return existsSync17(
|
|
56996
|
+
return existsSync17(join26(path2, ".git"));
|
|
55300
56997
|
}
|
|
55301
56998
|
function childDirectories(root) {
|
|
55302
56999
|
try {
|
|
55303
|
-
return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) =>
|
|
57000
|
+
return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join26(root, entry.name));
|
|
55304
57001
|
} catch {
|
|
55305
57002
|
return [];
|
|
55306
57003
|
}
|
|
@@ -55326,10 +57023,10 @@ function directoryExists(path2) {
|
|
|
55326
57023
|
}
|
|
55327
57024
|
function expandHomePath(path2) {
|
|
55328
57025
|
if (path2 === "~") {
|
|
55329
|
-
return
|
|
57026
|
+
return homedir18();
|
|
55330
57027
|
}
|
|
55331
57028
|
if (path2.startsWith("~/")) {
|
|
55332
|
-
return
|
|
57029
|
+
return join26(homedir18(), path2.slice(2));
|
|
55333
57030
|
}
|
|
55334
57031
|
return resolve10(path2);
|
|
55335
57032
|
}
|
|
@@ -55420,8 +57117,8 @@ init_runner();
|
|
|
55420
57117
|
init_runnerLockTakeover();
|
|
55421
57118
|
init_claudeCodeSession();
|
|
55422
57119
|
init_authCache();
|
|
55423
|
-
import { existsSync as existsSync18, readFileSync as
|
|
55424
|
-
import { homedir as
|
|
57120
|
+
import { existsSync as existsSync18, readFileSync as readFileSync24, realpathSync as realpathSync7 } from "node:fs";
|
|
57121
|
+
import { homedir as homedir19 } from "node:os";
|
|
55425
57122
|
import { resolve as resolve11 } from "node:path";
|
|
55426
57123
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
55427
57124
|
|
|
@@ -55516,9 +57213,9 @@ init_kandanTls();
|
|
|
55516
57213
|
init_protocol();
|
|
55517
57214
|
init_json();
|
|
55518
57215
|
init_defaultUrls();
|
|
55519
|
-
import { existsSync as existsSync15, mkdirSync as
|
|
55520
|
-
import { dirname as
|
|
55521
|
-
import { homedir as
|
|
57216
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync20, writeFileSync as writeFileSync13 } from "node:fs";
|
|
57217
|
+
import { dirname as dirname16, join as join23 } from "node:path";
|
|
57218
|
+
import { homedir as homedir16 } from "node:os";
|
|
55522
57219
|
async function runAgentCliCommand(args, deps = {
|
|
55523
57220
|
fetchImpl: fetch,
|
|
55524
57221
|
stdout: process.stdout,
|
|
@@ -56226,7 +57923,7 @@ function agentTokenFile(flags) {
|
|
|
56226
57923
|
return flags.get("agent-token-file") ?? defaultAgentTokenFilePath();
|
|
56227
57924
|
}
|
|
56228
57925
|
function defaultAgentTokenFilePath() {
|
|
56229
|
-
return
|
|
57926
|
+
return join23(homedir16(), ".linzumi", "agent-token.json");
|
|
56230
57927
|
}
|
|
56231
57928
|
function normalizedApiUrl(apiUrl) {
|
|
56232
57929
|
return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
|
|
@@ -56235,10 +57932,10 @@ function authorizationHeaders(token) {
|
|
|
56235
57932
|
return { authorization: `Bearer ${token}` };
|
|
56236
57933
|
}
|
|
56237
57934
|
function readOptionalTextFile(path2) {
|
|
56238
|
-
return existsSync15(path2) ?
|
|
57935
|
+
return existsSync15(path2) ? readFileSync20(path2, "utf8") : void 0;
|
|
56239
57936
|
}
|
|
56240
57937
|
function writeTextFile(path2, content) {
|
|
56241
|
-
|
|
57938
|
+
mkdirSync15(dirname16(path2), { recursive: true });
|
|
56242
57939
|
writeFileSync13(path2, content);
|
|
56243
57940
|
}
|
|
56244
57941
|
function readStoredAgentTokenFile(path2, readTextFile = readOptionalTextFile) {
|
|
@@ -56327,26 +58024,26 @@ init_helloLinzumiProject();
|
|
|
56327
58024
|
init_runnerLogger();
|
|
56328
58025
|
import {
|
|
56329
58026
|
existsSync as existsSync16,
|
|
56330
|
-
closeSync as
|
|
56331
|
-
mkdirSync as
|
|
56332
|
-
openSync as
|
|
56333
|
-
readFileSync as
|
|
58027
|
+
closeSync as closeSync5,
|
|
58028
|
+
mkdirSync as mkdirSync16,
|
|
58029
|
+
openSync as openSync6,
|
|
58030
|
+
readFileSync as readFileSync21,
|
|
56334
58031
|
watch,
|
|
56335
58032
|
writeFileSync as writeFileSync14
|
|
56336
58033
|
} from "node:fs";
|
|
56337
|
-
import { homedir as
|
|
56338
|
-
import { dirname as
|
|
58034
|
+
import { homedir as homedir17 } from "node:os";
|
|
58035
|
+
import { dirname as dirname17, join as join24, resolve as resolve9 } from "node:path";
|
|
56339
58036
|
import { execFileSync, spawn as spawn10 } from "node:child_process";
|
|
56340
58037
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
56341
58038
|
var connectedMarkers = ["Connected to Linzumi", "Runner connected:"];
|
|
56342
58039
|
function commanderStatusDir() {
|
|
56343
|
-
return
|
|
58040
|
+
return join24(homedir17(), ".linzumi", "commanders");
|
|
56344
58041
|
}
|
|
56345
58042
|
function commanderStatusFile(runnerId, statusDir = commanderStatusDir()) {
|
|
56346
|
-
return
|
|
58043
|
+
return join24(statusDir, `${safeRunnerId(runnerId)}.json`);
|
|
56347
58044
|
}
|
|
56348
58045
|
function defaultCommanderLogFile(runnerId) {
|
|
56349
|
-
return
|
|
58046
|
+
return join24(homedir17(), ".linzumi", "logs", `${safeRunnerId(runnerId)}.log`);
|
|
56350
58047
|
}
|
|
56351
58048
|
function commanderLogIsConnected(log2) {
|
|
56352
58049
|
return connectedMarkers.some((marker) => log2.includes(marker));
|
|
@@ -56367,10 +58064,10 @@ function startCommanderDaemon(options) {
|
|
|
56367
58064
|
"--log-file",
|
|
56368
58065
|
logFile
|
|
56369
58066
|
];
|
|
56370
|
-
|
|
56371
|
-
|
|
56372
|
-
const out =
|
|
56373
|
-
const err =
|
|
58067
|
+
mkdirSync16(statusDir, { recursive: true });
|
|
58068
|
+
mkdirSync16(dirname17(logFile), { recursive: true });
|
|
58069
|
+
const out = openSync6(logFile, "a");
|
|
58070
|
+
const err = openSync6(logFile, "a");
|
|
56374
58071
|
writeCliAuditEvent(
|
|
56375
58072
|
"process.spawn",
|
|
56376
58073
|
{
|
|
@@ -56397,8 +58094,8 @@ function startCommanderDaemon(options) {
|
|
|
56397
58094
|
},
|
|
56398
58095
|
{ sessionId: options.runnerId }
|
|
56399
58096
|
);
|
|
56400
|
-
|
|
56401
|
-
|
|
58097
|
+
closeSync5(out);
|
|
58098
|
+
closeSync5(err);
|
|
56402
58099
|
child.unref();
|
|
56403
58100
|
if (child.pid === void 0) {
|
|
56404
58101
|
throw new Error("commander daemon did not report a pid");
|
|
@@ -56423,12 +58120,12 @@ function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), proce
|
|
|
56423
58120
|
if (!existsSync16(statusFile)) {
|
|
56424
58121
|
return { status: "missing", runnerId, statusFile };
|
|
56425
58122
|
}
|
|
56426
|
-
const record = parseRecord2(
|
|
58123
|
+
const record = parseRecord2(readFileSync21(statusFile, "utf8"));
|
|
56427
58124
|
return processIsRunning(record.pid) && processMatchesRecord(record, processIdentityReader) ? { status: "running", record } : { status: "stopped", record };
|
|
56428
58125
|
}
|
|
56429
58126
|
async function waitForCommanderDaemon(options) {
|
|
56430
58127
|
const now = options.now ?? (() => Date.now());
|
|
56431
|
-
const readTextFile = options.readTextFile ?? ((path2) => existsSync16(path2) ?
|
|
58128
|
+
const readTextFile = options.readTextFile ?? ((path2) => existsSync16(path2) ? readFileSync21(path2, "utf8") : void 0);
|
|
56432
58129
|
const statusImpl = options.statusImpl ?? commanderDaemonStatus;
|
|
56433
58130
|
const deadline = now() + options.timeoutMs;
|
|
56434
58131
|
while (now() <= deadline) {
|
|
@@ -69321,7 +71018,7 @@ init_userFacingErrors();
|
|
|
69321
71018
|
init_version();
|
|
69322
71019
|
import { chmodSync as chmodSync3, mkdtempSync as mkdtempSync5, rmSync as rmSync6 } from "node:fs";
|
|
69323
71020
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
69324
|
-
import { basename as basename9, dirname as
|
|
71021
|
+
import { basename as basename9, dirname as dirname18, join as join25 } from "node:path";
|
|
69325
71022
|
var configEnvKey = "LINZUMI_REMOTE_CODEX_HARNESS_CONFIG_B64";
|
|
69326
71023
|
var remoteHarnessEnvironmentId = "linzumi-remote-codex-harness";
|
|
69327
71024
|
async function runRemoteCodexHarnessWorkerFromEnv(env = process.env) {
|
|
@@ -69554,18 +71251,18 @@ function resolveLinzumiCliEntrypoint(entrypoint) {
|
|
|
69554
71251
|
case "remote-codex-harness-worker.js":
|
|
69555
71252
|
case "remote-codex-harness-worker.mjs":
|
|
69556
71253
|
case "remote-codex-harness-worker.cjs":
|
|
69557
|
-
return
|
|
71254
|
+
return join25(dirname18(entrypoint), "linzumi.js");
|
|
69558
71255
|
case "remoteCodexHarnessWorkerEntrypoint.ts":
|
|
69559
71256
|
case "remoteCodexHarnessWorkerEntrypoint.js":
|
|
69560
|
-
return
|
|
71257
|
+
return join25(dirname18(entrypoint), "index.ts");
|
|
69561
71258
|
default:
|
|
69562
71259
|
return entrypoint;
|
|
69563
71260
|
}
|
|
69564
71261
|
}
|
|
69565
71262
|
function writeEphemeralMcpAuthFile2(options) {
|
|
69566
|
-
const directory = mkdtempSync5(
|
|
71263
|
+
const directory = mkdtempSync5(join25(tmpdir4(), "linzumi-mcp-auth-"));
|
|
69567
71264
|
chmodSync3(directory, 448);
|
|
69568
|
-
const path2 =
|
|
71265
|
+
const path2 = join25(directory, "auth.json");
|
|
69569
71266
|
try {
|
|
69570
71267
|
writeCachedLocalRunnerToken({
|
|
69571
71268
|
kandanUrl: options.kandanUrl,
|
|
@@ -70516,7 +72213,7 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
70516
72213
|
};
|
|
70517
72214
|
}
|
|
70518
72215
|
function readAgentTokenTextFile(path2) {
|
|
70519
|
-
return existsSync18(path2) ?
|
|
72216
|
+
return existsSync18(path2) ? readFileSync24(path2, "utf8") : void 0;
|
|
70520
72217
|
}
|
|
70521
72218
|
function rejectAgentRunnerTargetingFlags(values) {
|
|
70522
72219
|
const unsupportedFlags = [
|
|
@@ -70835,10 +72532,10 @@ function rejectStartTargetingFlags(values) {
|
|
|
70835
72532
|
}
|
|
70836
72533
|
function resolveUserPath(pathValue) {
|
|
70837
72534
|
if (pathValue === "~") {
|
|
70838
|
-
return
|
|
72535
|
+
return homedir19();
|
|
70839
72536
|
}
|
|
70840
72537
|
if (pathValue.startsWith("~/")) {
|
|
70841
|
-
return resolve11(
|
|
72538
|
+
return resolve11(homedir19(), pathValue.slice(2));
|
|
70842
72539
|
}
|
|
70843
72540
|
return resolve11(pathValue);
|
|
70844
72541
|
}
|