@linzumi/cli 0.0.107-beta → 0.0.109-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 +1917 -175
- 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,
|
|
@@ -19998,7 +20078,7 @@ var linzumiCliVersion, linzumiCliVersionText;
|
|
|
19998
20078
|
var init_version = __esm({
|
|
19999
20079
|
"src/version.ts"() {
|
|
20000
20080
|
"use strict";
|
|
20001
|
-
linzumiCliVersion = "0.0.
|
|
20081
|
+
linzumiCliVersion = "0.0.109-beta";
|
|
20002
20082
|
linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
|
|
20003
20083
|
}
|
|
20004
20084
|
});
|
|
@@ -23586,33 +23666,901 @@ var init_remoteCodexSandboxRunner = __esm({
|
|
|
23586
23666
|
}
|
|
23587
23667
|
});
|
|
23588
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
|
+
|
|
23589
24534
|
// src/runner.ts
|
|
23590
24535
|
import { spawn as spawn9, spawnSync as spawnSync5 } from "node:child_process";
|
|
23591
|
-
import { createHash as
|
|
24536
|
+
import { createHash as createHash7, randomUUID as randomUUID4 } from "node:crypto";
|
|
23592
24537
|
import {
|
|
23593
24538
|
chmodSync as chmodSync2,
|
|
24539
|
+
closeSync as closeSync4,
|
|
23594
24540
|
existsSync as existsSync13,
|
|
24541
|
+
fsyncSync as fsyncSync2,
|
|
23595
24542
|
lstatSync,
|
|
23596
|
-
mkdirSync as
|
|
24543
|
+
mkdirSync as mkdirSync14,
|
|
23597
24544
|
mkdtempSync as mkdtempSync4,
|
|
24545
|
+
openSync as openSync5,
|
|
23598
24546
|
readdirSync as readdirSync4,
|
|
23599
|
-
readFileSync as
|
|
24547
|
+
readFileSync as readFileSync18,
|
|
23600
24548
|
realpathSync as realpathSync6,
|
|
23601
|
-
renameSync as
|
|
24549
|
+
renameSync as renameSync5,
|
|
23602
24550
|
rmSync as rmSync5,
|
|
23603
24551
|
statSync as statSync3,
|
|
23604
24552
|
writeFileSync as writeFileSync12
|
|
23605
24553
|
} from "node:fs";
|
|
23606
24554
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
23607
24555
|
import { createServer as createServer3 } from "node:http";
|
|
23608
|
-
import { homedir as
|
|
24556
|
+
import { homedir as homedir15, hostname as hostname2, tmpdir as tmpdir3 } from "node:os";
|
|
23609
24557
|
import { createInterface } from "node:readline";
|
|
23610
24558
|
import {
|
|
23611
24559
|
basename as basename8,
|
|
23612
|
-
dirname as
|
|
24560
|
+
dirname as dirname15,
|
|
23613
24561
|
extname as extname2,
|
|
23614
24562
|
isAbsolute as isAbsolute5,
|
|
23615
|
-
join as
|
|
24563
|
+
join as join22,
|
|
23616
24564
|
resolve as resolve8
|
|
23617
24565
|
} from "node:path";
|
|
23618
24566
|
async function runLocalCodexRunner(options) {
|
|
@@ -23623,7 +24571,11 @@ async function runLocalCodexRunner(options) {
|
|
|
23623
24571
|
removeHandlers: void 0
|
|
23624
24572
|
};
|
|
23625
24573
|
const close = () => closeCleanupStack(cleanup);
|
|
23626
|
-
|
|
24574
|
+
const runnerDrainHooks = {
|
|
24575
|
+
stopAccepting: () => void 0,
|
|
24576
|
+
flushInboundQueues: () => void 0
|
|
24577
|
+
};
|
|
24578
|
+
cleanup.removeHandlers = installCleanupHandlers(close, runnerDrainHooks, log2);
|
|
23627
24579
|
log2("runner.starting", {
|
|
23628
24580
|
runnerId: options.runnerId,
|
|
23629
24581
|
cwd: options.cwd,
|
|
@@ -23667,7 +24619,13 @@ async function runLocalCodexRunner(options) {
|
|
|
23667
24619
|
runnerId: options.runnerId
|
|
23668
24620
|
});
|
|
23669
24621
|
}
|
|
23670
|
-
return await openLocalCodexRunner(
|
|
24622
|
+
return await openLocalCodexRunner(
|
|
24623
|
+
options,
|
|
24624
|
+
log2,
|
|
24625
|
+
cleanup,
|
|
24626
|
+
close,
|
|
24627
|
+
runnerDrainHooks
|
|
24628
|
+
);
|
|
23671
24629
|
} catch (error) {
|
|
23672
24630
|
await close().catch(() => void 0);
|
|
23673
24631
|
throw error;
|
|
@@ -23715,12 +24673,81 @@ async function runThreadCodexWorker(options) {
|
|
|
23715
24673
|
await waitForThreadCodexWorkerStop(started.process);
|
|
23716
24674
|
stop();
|
|
23717
24675
|
}
|
|
23718
|
-
|
|
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) {
|
|
23719
24739
|
const agentProviders = availableRunnerAgentProviders(options);
|
|
23720
24740
|
const localCodexAppServerAvailable = hasLocalCodexAppServerCapability(options);
|
|
23721
24741
|
const dependencyStatusRef = {
|
|
23722
24742
|
value: options.dependencyStatus
|
|
23723
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
|
+
});
|
|
23724
24751
|
const refreshDependencyStatus = async () => {
|
|
23725
24752
|
if (dependencyStatusRef.value === void 0) {
|
|
23726
24753
|
return;
|
|
@@ -23746,6 +24773,27 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
23746
24773
|
});
|
|
23747
24774
|
}
|
|
23748
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
|
+
};
|
|
23749
24797
|
const allowedForwardPorts = options.allowedForwardPorts ?? [];
|
|
23750
24798
|
const liveForwardPorts = new Set(allowedForwardPorts);
|
|
23751
24799
|
const managedForwardPorts = /* @__PURE__ */ new Set();
|
|
@@ -23899,6 +24947,16 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
23899
24947
|
// would silently drop it (and run against the user's personal account).
|
|
23900
24948
|
// Old servers ignore the extra key.
|
|
23901
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,
|
|
23902
24960
|
defaultAgentProvider: "codex",
|
|
23903
24961
|
startInstance: allowedCwds.value.length > 0,
|
|
23904
24962
|
durableClaudeSessionStore: true,
|
|
@@ -23947,6 +25005,124 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
23947
25005
|
runnerThreadSessionRegistryPath(options.runnerId),
|
|
23948
25006
|
log2
|
|
23949
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 resolveRunnerBoundChannel = () => options.channelSession?.channelSlug ?? threadSessionRegistry.list()[0]?.channel;
|
|
25068
|
+
const durableInboundQueueFor = (channel, kandanThreadId) => {
|
|
25069
|
+
const workspace = inboundQueueWorkspace();
|
|
25070
|
+
const cacheKey = `${workspace}:${channel}:${kandanThreadId}`;
|
|
25071
|
+
const existing = durableInboundQueues.get(cacheKey);
|
|
25072
|
+
if (existing !== void 0) {
|
|
25073
|
+
return existing;
|
|
25074
|
+
}
|
|
25075
|
+
const queue = createDurableInboundThreadQueue(
|
|
25076
|
+
durableInboundQueuePath(workspace, channel, kandanThreadId),
|
|
25077
|
+
log2
|
|
25078
|
+
);
|
|
25079
|
+
durableInboundQueues.set(cacheKey, queue);
|
|
25080
|
+
return queue;
|
|
25081
|
+
};
|
|
25082
|
+
const capturedLiveFollowUpEvents = /* @__PURE__ */ new Map();
|
|
25083
|
+
const capturedFollowUpKey = (channel, threadId, seq2) => `${inboundQueueWorkspace()}:${channel}:${threadId}:${seq2}`;
|
|
25084
|
+
const captureLiveFollowUpEvent = (channel, threadId, event) => {
|
|
25085
|
+
capturedLiveFollowUpEvents.set(
|
|
25086
|
+
capturedFollowUpKey(channel, threadId, event.seq),
|
|
25087
|
+
event
|
|
25088
|
+
);
|
|
25089
|
+
};
|
|
25090
|
+
const reEnqueueUndeliveredFollowUps = (args) => {
|
|
25091
|
+
const queue = durableInboundQueueFor(args.channel, args.threadId);
|
|
25092
|
+
for (const seq2 of args.sourceSeqs) {
|
|
25093
|
+
const key = capturedFollowUpKey(args.channel, args.threadId, seq2);
|
|
25094
|
+
const event = capturedLiveFollowUpEvents.get(key);
|
|
25095
|
+
if (event === void 0) {
|
|
25096
|
+
log2("runner.live_follow_up_recapture_missing", {
|
|
25097
|
+
kandanThreadId: args.threadId,
|
|
25098
|
+
channel: args.channel,
|
|
25099
|
+
sourceSeq: seq2
|
|
25100
|
+
});
|
|
25101
|
+
continue;
|
|
25102
|
+
}
|
|
25103
|
+
const enqueued = queue.enqueue(
|
|
25104
|
+
seq2,
|
|
25105
|
+
"replay",
|
|
25106
|
+
serializeKandanChatEventForQueue(event),
|
|
25107
|
+
Date.now()
|
|
25108
|
+
);
|
|
25109
|
+
log2("runner.live_follow_up_re_enqueued_on_close", {
|
|
25110
|
+
kandanThreadId: args.threadId,
|
|
25111
|
+
channel: args.channel,
|
|
25112
|
+
sourceSeq: seq2,
|
|
25113
|
+
enqueued: enqueued.accepted
|
|
25114
|
+
});
|
|
25115
|
+
capturedLiveFollowUpEvents.delete(key);
|
|
25116
|
+
}
|
|
25117
|
+
};
|
|
25118
|
+
runnerDrainHooks.stopAccepting = () => {
|
|
25119
|
+
acceptingInbound = false;
|
|
25120
|
+
};
|
|
25121
|
+
runnerDrainHooks.flushInboundQueues = () => {
|
|
25122
|
+
for (const queue of durableInboundQueues.values()) {
|
|
25123
|
+
queue.compact();
|
|
25124
|
+
}
|
|
25125
|
+
};
|
|
23950
25126
|
const controlEpochStore = createRunnerControlEpochStore(
|
|
23951
25127
|
controlCursorStore,
|
|
23952
25128
|
appliedStartTurnStore,
|
|
@@ -24788,6 +25964,8 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
24788
25964
|
codex,
|
|
24789
25965
|
topic,
|
|
24790
25966
|
instanceId,
|
|
25967
|
+
// SEAM 2: source the held lease epoch for the stream-write fence echo.
|
|
25968
|
+
leaseEpochFor: (threadId) => leaseEpochRegistry.current(threadId),
|
|
24791
25969
|
options: {
|
|
24792
25970
|
kandanUrl: options.kandanUrl,
|
|
24793
25971
|
token: options.token,
|
|
@@ -24824,6 +26002,10 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
24824
26002
|
// session is attached (the durable thread's worker fully died). Late-
|
|
24825
26003
|
// bound to dodge the const's temporal dead zone (defined below).
|
|
24826
26004
|
onUnroutedThreadMessage: (event) => handleUnroutedThreadMessage(event),
|
|
26005
|
+
// BLOCKER B: codex live dispatch-confirm watermark commit on the main
|
|
26006
|
+
// channel session too, off the same persisted authority the keystone
|
|
26007
|
+
// gates on.
|
|
26008
|
+
onLiveSourceSeqAccepted: commitLiveWorkerAcceptedSourceSeq,
|
|
24827
26009
|
channelSession: options.channelSession
|
|
24828
26010
|
},
|
|
24829
26011
|
log: log2
|
|
@@ -25174,6 +26356,8 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25174
26356
|
codex: sessionCodex,
|
|
25175
26357
|
topic,
|
|
25176
26358
|
instanceId,
|
|
26359
|
+
// SEAM 2: dynamic per-thread session also echoes the held lease epoch.
|
|
26360
|
+
leaseEpochFor: (threadId) => leaseEpochRegistry.current(threadId),
|
|
25177
26361
|
options: {
|
|
25178
26362
|
kandanUrl: options.kandanUrl,
|
|
25179
26363
|
token: options.token,
|
|
@@ -25188,6 +26372,11 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25188
26372
|
// it out of the const's temporal dead zone while still resolving the
|
|
25189
26373
|
// live handler when an unrouted chat event actually arrives.
|
|
25190
26374
|
onUnroutedThreadMessage: (event) => handleUnroutedThreadMessage(event),
|
|
26375
|
+
// BLOCKER B: codex live dispatch-confirm watermark commit (the
|
|
26376
|
+
// production-default double-exec fix). Fires from drainKandanMessageQueue
|
|
26377
|
+
// when turn/start replies, off the SAME persisted authority the keystone
|
|
26378
|
+
// gates on, so a codex worker-death + replay is gated 'ignored'.
|
|
26379
|
+
onLiveSourceSeqAccepted: commitLiveWorkerAcceptedSourceSeq,
|
|
25191
26380
|
pipelinePersistDir: commanderOutboxPersistDir(),
|
|
25192
26381
|
enablePortForwardWatch: true,
|
|
25193
26382
|
initialForwardPorts: allowedForwardPorts,
|
|
@@ -25566,7 +26755,12 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25566
26755
|
control,
|
|
25567
26756
|
log2,
|
|
25568
26757
|
onStartedThread,
|
|
25569
|
-
void 0
|
|
26758
|
+
void 0,
|
|
26759
|
+
(id) => leaseEpochRegistry.current(id),
|
|
26760
|
+
commitLiveWorkerAcceptedSourceSeq,
|
|
26761
|
+
captureLiveFollowUpEvent,
|
|
26762
|
+
reEnqueueUndeliveredFollowUps,
|
|
26763
|
+
liveSourceSeqAlreadyApplied
|
|
25570
26764
|
)
|
|
25571
26765
|
);
|
|
25572
26766
|
}
|
|
@@ -25609,6 +26803,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25609
26803
|
attachments: event.attachments,
|
|
25610
26804
|
boundStartTimeout: true
|
|
25611
26805
|
});
|
|
26806
|
+
return "dispatched";
|
|
25612
26807
|
} catch (error) {
|
|
25613
26808
|
log2("runner.spawn_on_miss_failed", {
|
|
25614
26809
|
kandanThreadId: event.threadId ?? null,
|
|
@@ -25622,6 +26817,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25622
26817
|
`could not resume the thread's coding session: ${error instanceof Error ? error.message : String(error)}`,
|
|
25623
26818
|
record.codexThreadId
|
|
25624
26819
|
);
|
|
26820
|
+
return "transient_failed";
|
|
25625
26821
|
}
|
|
25626
26822
|
};
|
|
25627
26823
|
const handleUnroutedThreadMessage = async (event) => {
|
|
@@ -25650,8 +26846,304 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25650
26846
|
}
|
|
25651
26847
|
const record = threadSessionRegistry.list().find((entry) => entry.kandanThreadId === kandanThreadId);
|
|
25652
26848
|
if (record === void 0) {
|
|
25653
|
-
|
|
26849
|
+
const boundChannel = resolveRunnerBoundChannel();
|
|
26850
|
+
if (boundChannel === void 0 || !acceptingInbound) {
|
|
26851
|
+
return "ignored";
|
|
26852
|
+
}
|
|
26853
|
+
const queue2 = durableInboundQueueFor(boundChannel, kandanThreadId);
|
|
26854
|
+
const enqueued = queue2.enqueue(
|
|
26855
|
+
event.seq,
|
|
26856
|
+
event.localRunnerEventType === void 0 ? "live" : "replay",
|
|
26857
|
+
serializeKandanChatEventForQueue(event),
|
|
26858
|
+
Date.now()
|
|
26859
|
+
);
|
|
26860
|
+
log2("runner.spawn_on_miss_enqueued_without_local_record", {
|
|
26861
|
+
kandanThreadId,
|
|
26862
|
+
channel: boundChannel,
|
|
26863
|
+
seq: event.seq,
|
|
26864
|
+
enqueued: enqueued.accepted
|
|
26865
|
+
});
|
|
26866
|
+
await publishUnroutedDurableEnqueueState(boundChannel, event);
|
|
26867
|
+
return "spawned";
|
|
26868
|
+
}
|
|
26869
|
+
const queueChannel = record.channel;
|
|
26870
|
+
const watermarkKey = threadSourceSeqWatermarkKey(
|
|
26871
|
+
inboundQueueWorkspace(),
|
|
26872
|
+
queueChannel,
|
|
26873
|
+
kandanThreadId
|
|
26874
|
+
);
|
|
26875
|
+
const queue = durableInboundQueueFor(queueChannel, kandanThreadId);
|
|
26876
|
+
if (!acceptingInbound) {
|
|
26877
|
+
const enqueued = queue.enqueue(
|
|
26878
|
+
event.seq,
|
|
26879
|
+
event.localRunnerEventType === void 0 ? "live" : "replay",
|
|
26880
|
+
serializeKandanChatEventForQueue(event),
|
|
26881
|
+
Date.now()
|
|
26882
|
+
);
|
|
26883
|
+
log2("runner.spawn_on_miss_drain_in_progress", {
|
|
26884
|
+
kandanThreadId,
|
|
26885
|
+
seq: event.seq,
|
|
26886
|
+
enqueued: enqueued.accepted
|
|
26887
|
+
});
|
|
26888
|
+
return "spawned";
|
|
26889
|
+
}
|
|
26890
|
+
const outcome = await receiveResumeMessage({
|
|
26891
|
+
queue,
|
|
26892
|
+
store: appliedSourceSeqStore,
|
|
26893
|
+
lock: threadReconcilerLock,
|
|
26894
|
+
leases: leaseEpochRegistry,
|
|
26895
|
+
threadId: kandanThreadId,
|
|
26896
|
+
watermarkKey,
|
|
26897
|
+
sourceSeq: event.seq,
|
|
26898
|
+
kind: event.localRunnerEventType === void 0 ? "live" : "replay",
|
|
26899
|
+
payload: serializeKandanChatEventForQueue(event),
|
|
26900
|
+
leaseEpoch: readLeaseEpoch(event),
|
|
26901
|
+
// Blocker #2: the keystone drains the queue HEAD in source_seq order, so
|
|
26902
|
+
// the dispatch is keyed by (sourceSeq, payload) - it may run a LOWER
|
|
26903
|
+
// pending seq before this arrived one. Reconstruct the event from the
|
|
26904
|
+
// head's payload, exactly as the boot/rejoin drain does.
|
|
26905
|
+
dispatch: async (sourceSeq, payload) => {
|
|
26906
|
+
const drained = deserializeKandanChatEventFromQueue(payload);
|
|
26907
|
+
if (drained === void 0) {
|
|
26908
|
+
log2("runner.spawn_on_miss_bad_payload", {
|
|
26909
|
+
kandanThreadId,
|
|
26910
|
+
seq: sourceSeq
|
|
26911
|
+
});
|
|
26912
|
+
return "permanent_failed";
|
|
26913
|
+
}
|
|
26914
|
+
return await dispatchResumeForOwnedThread(drained, record);
|
|
26915
|
+
},
|
|
26916
|
+
// BLOCKER #4: pass the server's per-thread processed floor (the most-recent
|
|
26917
|
+
// resume_reconcile rows) so the live-miss path reconciles
|
|
26918
|
+
// highestProcessedSourceSeq into the watermark BEFORE dispatching - a
|
|
26919
|
+
// completed-but-not-locally-acked turn that replays is gated 'ignored', not
|
|
26920
|
+
// re-run. Empty until the server SEAM 1 half ships; then the floor is
|
|
26921
|
+
// authoritative. The live-miss path leaves onlyDispatchUnattempted at its
|
|
26922
|
+
// default (false): the arrived message is attempts===0 and the blind
|
|
26923
|
+
// post-completion fence is owned by the boot/rejoin recovery drain, not this
|
|
26924
|
+
// on-receipt path (changing it here would alter the existing live behavior).
|
|
26925
|
+
serverRows: latestResumeReconcileByThread.get(kandanThreadId) ?? [],
|
|
26926
|
+
// Blocker #3: surface a LOUD terminal `failed` state on any dead-letter.
|
|
26927
|
+
onDeadLetter: deadLetterPublisherFor(record),
|
|
26928
|
+
log: log2
|
|
26929
|
+
});
|
|
26930
|
+
return outcome === "ignored" ? "ignored" : "spawned";
|
|
26931
|
+
};
|
|
26932
|
+
const drainOwnedThreadQueue = async (record, serverRows = []) => {
|
|
26933
|
+
if (!acceptingInbound) {
|
|
26934
|
+
return;
|
|
26935
|
+
}
|
|
26936
|
+
const kandanThreadId = record.kandanThreadId;
|
|
26937
|
+
const queueChannel = record.channel;
|
|
26938
|
+
const watermarkKey = threadSourceSeqWatermarkKey(
|
|
26939
|
+
inboundQueueWorkspace(),
|
|
26940
|
+
queueChannel,
|
|
26941
|
+
kandanThreadId
|
|
26942
|
+
);
|
|
26943
|
+
const queue = durableInboundQueueFor(queueChannel, kandanThreadId);
|
|
26944
|
+
await drainResumeQueue({
|
|
26945
|
+
queue,
|
|
26946
|
+
store: appliedSourceSeqStore,
|
|
26947
|
+
lock: threadReconcilerLock,
|
|
26948
|
+
threadId: kandanThreadId,
|
|
26949
|
+
watermarkKey,
|
|
26950
|
+
serverRows,
|
|
26951
|
+
log: log2,
|
|
26952
|
+
// Until the SERVER message_state reconcile half (SEAM 1 server) ships, the
|
|
26953
|
+
// drain runs with serverRows=[] in production. In that mode a post-
|
|
26954
|
+
// completion-kill (turn confirmed-dispatched, then SIGKILL before the
|
|
26955
|
+
// watermark flush + ack) leaves an attempted-but-unacked entry. Blindly
|
|
26956
|
+
// re-dispatching it would double-execute the turn (real edits/commits), so
|
|
26957
|
+
// gate the drain to UNATTEMPTED entries only: an attempted entry is dead-
|
|
26958
|
+
// lettered with a loud `failed` state rather than re-run. Once serverRows
|
|
26959
|
+
// is populated the reconcile fences already-run seqs and this gate is moot.
|
|
26960
|
+
onlyDispatchUnattempted: serverRows.length === 0,
|
|
26961
|
+
// Blocker #3: surface a LOUD terminal `failed` state on any dead-letter
|
|
26962
|
+
// (exhausted retries, a withheld post-completion-kill entry, or a permanent
|
|
26963
|
+
// failure) so a persistently-unbindable thread is never silently dropped.
|
|
26964
|
+
onDeadLetter: deadLetterPublisherFor(record),
|
|
26965
|
+
// A torn tail with no server reconcile cannot be re-derived on this branch
|
|
26966
|
+
// (no server outbox/replay). Publish a loud "please resend" terminal state
|
|
26967
|
+
// for the thread instead of silently dropping the lost tail.
|
|
26968
|
+
onTornTailUnrecoverable: async (threadId) => {
|
|
26969
|
+
const control = rehydrationReconnectControl(record);
|
|
26970
|
+
await publishSpawnOnMissMessageState(
|
|
26971
|
+
control,
|
|
26972
|
+
"failed",
|
|
26973
|
+
"a message may have been lost during an unexpected shutdown (power loss). Please resend your last message to continue this job.",
|
|
26974
|
+
record.codexThreadId
|
|
26975
|
+
);
|
|
26976
|
+
log2("runner.inbound_torn_tail_resend_published", {
|
|
26977
|
+
kandanThreadId: threadId
|
|
26978
|
+
});
|
|
26979
|
+
},
|
|
26980
|
+
dispatch: async (sourceSeq, payload) => {
|
|
26981
|
+
const event = deserializeKandanChatEventFromQueue(payload);
|
|
26982
|
+
if (event === void 0) {
|
|
26983
|
+
log2("runner.inbound_drain_bad_payload", {
|
|
26984
|
+
kandanThreadId,
|
|
26985
|
+
seq: sourceSeq
|
|
26986
|
+
});
|
|
26987
|
+
return "permanent_failed";
|
|
26988
|
+
}
|
|
26989
|
+
return await dispatchResumeForOwnedThread(event, record);
|
|
26990
|
+
}
|
|
26991
|
+
});
|
|
26992
|
+
};
|
|
26993
|
+
const drainThreadQueueAfterSpawn = async (kandanThreadId, spawnResponse) => {
|
|
26994
|
+
if (spawnResponse !== void 0 && spawnResponse.ok === false) {
|
|
26995
|
+
return;
|
|
26996
|
+
}
|
|
26997
|
+
if (!acceptingInbound) {
|
|
26998
|
+
return;
|
|
26999
|
+
}
|
|
27000
|
+
const record = threadSessionRegistry.list().find((entry) => entry.kandanThreadId === kandanThreadId);
|
|
27001
|
+
if (record === void 0) {
|
|
27002
|
+
return;
|
|
27003
|
+
}
|
|
27004
|
+
const queue = durableInboundQueueFor(record.channel, kandanThreadId);
|
|
27005
|
+
if (queue.entries().length === 0) {
|
|
27006
|
+
return;
|
|
27007
|
+
}
|
|
27008
|
+
log2("runner.inbound_drain_after_spawn", {
|
|
27009
|
+
kandanThreadId,
|
|
27010
|
+
channel: record.channel,
|
|
27011
|
+
pending: queue.entries().length
|
|
27012
|
+
});
|
|
27013
|
+
await drainOwnedThreadQueue(
|
|
27014
|
+
record,
|
|
27015
|
+
latestResumeReconcileByThread.get(kandanThreadId) ?? []
|
|
27016
|
+
).catch((error) => {
|
|
27017
|
+
log2("runner.inbound_drain_after_spawn_failed", {
|
|
27018
|
+
kandanThreadId,
|
|
27019
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27020
|
+
});
|
|
27021
|
+
});
|
|
27022
|
+
};
|
|
27023
|
+
const startThreadRunnerProcessFromServerControl = async (control, cwd) => {
|
|
27024
|
+
const response = await startThreadRunnerProcess(control, cwd);
|
|
27025
|
+
const kandanThreadId = optionalThreadControlField(control, "threadId");
|
|
27026
|
+
if (kandanThreadId !== void 0) {
|
|
27027
|
+
await drainThreadQueueAfterSpawn(kandanThreadId, response);
|
|
27028
|
+
}
|
|
27029
|
+
return response;
|
|
27030
|
+
};
|
|
27031
|
+
const consumeResumeReconcile = (reconcileValue) => {
|
|
27032
|
+
const byThread = /* @__PURE__ */ new Map();
|
|
27033
|
+
const entries = parseResumeReconcileEntries(reconcileValue);
|
|
27034
|
+
for (const entry of entries) {
|
|
27035
|
+
if (entry.leaseEpoch !== void 0) {
|
|
27036
|
+
const adopted = leaseEpochRegistry.adopt(entry.threadId, entry.leaseEpoch);
|
|
27037
|
+
if (adopted) {
|
|
27038
|
+
log2("runner.resume_reconcile_lease_adopted", {
|
|
27039
|
+
kandanThreadId: entry.threadId,
|
|
27040
|
+
leaseEpoch: entry.leaseEpoch
|
|
27041
|
+
});
|
|
27042
|
+
}
|
|
27043
|
+
}
|
|
27044
|
+
const rows = resumeReconcileServerRows(entry);
|
|
27045
|
+
byThread.set(entry.threadId, rows);
|
|
27046
|
+
latestResumeReconcileByThread.set(entry.threadId, rows);
|
|
27047
|
+
}
|
|
27048
|
+
if (entries.length > 0) {
|
|
27049
|
+
log2("runner.resume_reconcile_consumed", { threadCount: entries.length });
|
|
27050
|
+
}
|
|
27051
|
+
return byThread;
|
|
27052
|
+
};
|
|
27053
|
+
const drainOrphanedThreadQueues = async () => {
|
|
27054
|
+
const dir = durableInboundQueueDir();
|
|
27055
|
+
let files;
|
|
27056
|
+
try {
|
|
27057
|
+
files = readdirSync4(dir).filter((name) => name.endsWith(".log"));
|
|
27058
|
+
} catch (error) {
|
|
27059
|
+
if (error?.code === "ENOENT") {
|
|
27060
|
+
return;
|
|
27061
|
+
}
|
|
27062
|
+
log2("runner.inbound_orphan_scan_failed", {
|
|
27063
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27064
|
+
});
|
|
27065
|
+
return;
|
|
27066
|
+
}
|
|
27067
|
+
const workspace = inboundQueueWorkspace();
|
|
27068
|
+
const owned = new Set(
|
|
27069
|
+
threadSessionRegistry.list().map(
|
|
27070
|
+
(record) => durableInboundQueuePath(
|
|
27071
|
+
workspace,
|
|
27072
|
+
record.channel,
|
|
27073
|
+
record.kandanThreadId
|
|
27074
|
+
)
|
|
27075
|
+
)
|
|
27076
|
+
);
|
|
27077
|
+
for (const name of files) {
|
|
27078
|
+
const path2 = join22(dir, name);
|
|
27079
|
+
if (owned.has(path2)) {
|
|
27080
|
+
continue;
|
|
27081
|
+
}
|
|
27082
|
+
try {
|
|
27083
|
+
const queue = createDurableInboundThreadQueue(path2, log2);
|
|
27084
|
+
const stranded = queue.entries();
|
|
27085
|
+
if (stranded.length === 0) {
|
|
27086
|
+
continue;
|
|
27087
|
+
}
|
|
27088
|
+
for (const entry of stranded) {
|
|
27089
|
+
queue.deadLetter(entry.sourceSeq);
|
|
27090
|
+
}
|
|
27091
|
+
queue.compact();
|
|
27092
|
+
log2("runner.inbound_orphan_dead_lettered", {
|
|
27093
|
+
path: path2,
|
|
27094
|
+
strandedCount: stranded.length,
|
|
27095
|
+
strandedSeqs: stranded.map((e) => e.sourceSeq)
|
|
27096
|
+
});
|
|
27097
|
+
} catch (error) {
|
|
27098
|
+
log2("runner.inbound_orphan_drain_failed", {
|
|
27099
|
+
path: path2,
|
|
27100
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27101
|
+
});
|
|
27102
|
+
}
|
|
25654
27103
|
}
|
|
27104
|
+
};
|
|
27105
|
+
const drainAllOwnedThreadQueues = async (reconcileByThread) => {
|
|
27106
|
+
if (!acceptingInbound) {
|
|
27107
|
+
return;
|
|
27108
|
+
}
|
|
27109
|
+
const records = threadSessionRegistry.list();
|
|
27110
|
+
await Promise.all(
|
|
27111
|
+
records.map(
|
|
27112
|
+
(record) => drainOwnedThreadQueue(
|
|
27113
|
+
record,
|
|
27114
|
+
reconcileByThread?.get(record.kandanThreadId) ?? []
|
|
27115
|
+
).catch((error) => {
|
|
27116
|
+
log2("runner.inbound_drain_failed", {
|
|
27117
|
+
kandanThreadId: record.kandanThreadId,
|
|
27118
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27119
|
+
});
|
|
27120
|
+
})
|
|
27121
|
+
)
|
|
27122
|
+
);
|
|
27123
|
+
await drainOrphanedThreadQueues();
|
|
27124
|
+
};
|
|
27125
|
+
const bootReconcile = consumeResumeReconcile(
|
|
27126
|
+
objectValue(joinResponse)?.resume_reconcile
|
|
27127
|
+
);
|
|
27128
|
+
const bootDrain = threadSessionRehydration.then(() => drainAllOwnedThreadQueues(bootReconcile)).catch((error) => {
|
|
27129
|
+
log2("runner.inbound_boot_drain_failed", {
|
|
27130
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27131
|
+
});
|
|
27132
|
+
});
|
|
27133
|
+
cleanup.actions.push(() => bootDrain);
|
|
27134
|
+
kandan.onReconnect((rejoinReplies) => {
|
|
27135
|
+
const rejoinReply = rejoinReplies.get(topic);
|
|
27136
|
+
const reconcile = consumeResumeReconcile(
|
|
27137
|
+
rejoinReply === void 0 ? void 0 : rejoinReply.resume_reconcile
|
|
27138
|
+
);
|
|
27139
|
+
void drainAllOwnedThreadQueues(reconcile).catch((error) => {
|
|
27140
|
+
log2("runner.inbound_rejoin_drain_failed", {
|
|
27141
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27142
|
+
});
|
|
27143
|
+
});
|
|
27144
|
+
});
|
|
27145
|
+
const dispatchResumeForOwnedThread = async (event, record) => {
|
|
27146
|
+
const kandanThreadId = record.kandanThreadId;
|
|
25655
27147
|
const baseControl = rehydrationReconnectControl(record);
|
|
25656
27148
|
const providerBinding = threadModelProviderBindings.get(kandanThreadId);
|
|
25657
27149
|
const reconnectControl = {
|
|
@@ -25681,7 +27173,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25681
27173
|
`could not resume the thread's coding session: working directory ${record.cwd} is not an allowed root (${cwd.reason})`,
|
|
25682
27174
|
record.codexThreadId
|
|
25683
27175
|
);
|
|
25684
|
-
return "
|
|
27176
|
+
return "permanent_failed";
|
|
25685
27177
|
}
|
|
25686
27178
|
const resolvedCwd = cwd.cwd;
|
|
25687
27179
|
const inFlightSpawn = spawnOnMissInFlight.get(kandanThreadId);
|
|
@@ -25701,14 +27193,13 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25701
27193
|
`could not resume the thread's coding session: no live session after spawn`,
|
|
25702
27194
|
record.codexThreadId
|
|
25703
27195
|
);
|
|
25704
|
-
return "
|
|
27196
|
+
return "transient_failed";
|
|
25705
27197
|
}
|
|
25706
|
-
await replaySpawnOnMissMessageOnLiveSession(
|
|
27198
|
+
return await replaySpawnOnMissMessageOnLiveSession(
|
|
25707
27199
|
event,
|
|
25708
27200
|
record,
|
|
25709
27201
|
reconnectControl
|
|
25710
27202
|
);
|
|
25711
|
-
return "spawned";
|
|
25712
27203
|
}
|
|
25713
27204
|
log2("runner.spawn_on_miss_resuming", {
|
|
25714
27205
|
kandanThreadId,
|
|
@@ -25729,12 +27220,11 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25729
27220
|
);
|
|
25730
27221
|
if (threadStartDelegatedToSibling(startResult)) {
|
|
25731
27222
|
spawnSettle("spawned");
|
|
25732
|
-
await replaySpawnOnMissMessageOnLiveSession(
|
|
27223
|
+
return await replaySpawnOnMissMessageOnLiveSession(
|
|
25733
27224
|
event,
|
|
25734
27225
|
record,
|
|
25735
27226
|
reconnectControl
|
|
25736
27227
|
);
|
|
25737
|
-
return "spawned";
|
|
25738
27228
|
}
|
|
25739
27229
|
if (startResult === void 0 || startResult.ok === false) {
|
|
25740
27230
|
spawnSettle("no_session");
|
|
@@ -25751,10 +27241,10 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25751
27241
|
`could not resume the thread's coding session: ${startError}`,
|
|
25752
27242
|
record.codexThreadId
|
|
25753
27243
|
);
|
|
25754
|
-
return "
|
|
27244
|
+
return "transient_failed";
|
|
25755
27245
|
}
|
|
25756
27246
|
spawnSettle("spawned");
|
|
25757
|
-
return "
|
|
27247
|
+
return "dispatched";
|
|
25758
27248
|
} catch (error) {
|
|
25759
27249
|
spawnSettle("no_session");
|
|
25760
27250
|
log2("runner.spawn_on_miss_failed", {
|
|
@@ -25769,7 +27259,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25769
27259
|
`could not resume the thread's coding session: ${error instanceof Error ? error.message : String(error)}`,
|
|
25770
27260
|
record.codexThreadId
|
|
25771
27261
|
);
|
|
25772
|
-
return "
|
|
27262
|
+
return "transient_failed";
|
|
25773
27263
|
} finally {
|
|
25774
27264
|
spawnSettle("spawned");
|
|
25775
27265
|
if (spawnOnMissInFlight.get(kandanThreadId) === spawnPromise) {
|
|
@@ -25795,6 +27285,49 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
25795
27285
|
});
|
|
25796
27286
|
}
|
|
25797
27287
|
};
|
|
27288
|
+
const publishUnroutedDurableEnqueueState = async (channel, event) => {
|
|
27289
|
+
if (event.threadId === void 0) {
|
|
27290
|
+
return;
|
|
27291
|
+
}
|
|
27292
|
+
const payload = {
|
|
27293
|
+
workspace: inboundQueueWorkspace(),
|
|
27294
|
+
channel,
|
|
27295
|
+
thread_id: event.threadId,
|
|
27296
|
+
instance_id: instanceId,
|
|
27297
|
+
seq: event.seq,
|
|
27298
|
+
status: "queued",
|
|
27299
|
+
reason: "queued for durable resume"
|
|
27300
|
+
};
|
|
27301
|
+
try {
|
|
27302
|
+
await kandan.push(topic, "message_state", payload);
|
|
27303
|
+
} catch (error) {
|
|
27304
|
+
log2("runner.spawn_on_miss_state_push_failed", {
|
|
27305
|
+
thread_id: event.threadId,
|
|
27306
|
+
status: "queued",
|
|
27307
|
+
message: error instanceof Error ? error.message : String(error)
|
|
27308
|
+
});
|
|
27309
|
+
}
|
|
27310
|
+
};
|
|
27311
|
+
const deadLetterPublisherFor = (record) => {
|
|
27312
|
+
return async ({ sourceSeq, reason, attempts, alreadyPublished }) => {
|
|
27313
|
+
log2("runner.inbound_dead_lettered", {
|
|
27314
|
+
kandanThreadId: record.kandanThreadId,
|
|
27315
|
+
seq: sourceSeq,
|
|
27316
|
+
reason,
|
|
27317
|
+
attempts,
|
|
27318
|
+
alreadyPublished
|
|
27319
|
+
});
|
|
27320
|
+
if (alreadyPublished) {
|
|
27321
|
+
return;
|
|
27322
|
+
}
|
|
27323
|
+
await publishSpawnOnMissMessageState(
|
|
27324
|
+
rehydrationReconnectControl(record),
|
|
27325
|
+
"failed",
|
|
27326
|
+
deadLetterFailedReason(reason),
|
|
27327
|
+
record.codexThreadId
|
|
27328
|
+
);
|
|
27329
|
+
};
|
|
27330
|
+
};
|
|
25798
27331
|
const heartbeatPayload = () => ({
|
|
25799
27332
|
instanceId,
|
|
25800
27333
|
clientId,
|
|
@@ -26285,7 +27818,12 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
26285
27818
|
control,
|
|
26286
27819
|
log2,
|
|
26287
27820
|
attachThreadSession,
|
|
26288
|
-
shouldUseThreadProcesses(options) ?
|
|
27821
|
+
shouldUseThreadProcesses(options) ? startThreadRunnerProcessFromServerControl : void 0,
|
|
27822
|
+
(id) => leaseEpochRegistry.current(id),
|
|
27823
|
+
commitLiveWorkerAcceptedSourceSeq,
|
|
27824
|
+
captureLiveFollowUpEvent,
|
|
27825
|
+
reEnqueueUndeliveredFollowUps,
|
|
27826
|
+
liveSourceSeqAlreadyApplied
|
|
26289
27827
|
);
|
|
26290
27828
|
}),
|
|
26291
27829
|
commitStartTurnWatermark
|
|
@@ -26316,7 +27854,10 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
|
|
|
26316
27854
|
pendingControls,
|
|
26317
27855
|
controlDispatcher,
|
|
26318
27856
|
dispatchControl,
|
|
26319
|
-
|
|
27857
|
+
async () => {
|
|
27858
|
+
await refreshDependencyStatus();
|
|
27859
|
+
await refreshCodexAuthStatus();
|
|
27860
|
+
}
|
|
26320
27861
|
);
|
|
26321
27862
|
return { instanceId, codexUrl, close };
|
|
26322
27863
|
}
|
|
@@ -26330,28 +27871,34 @@ function commanderOutboxPersistDir() {
|
|
|
26330
27871
|
if (override !== void 0 && override !== "") {
|
|
26331
27872
|
return override;
|
|
26332
27873
|
}
|
|
26333
|
-
return
|
|
27874
|
+
return join22(homedir15(), ".linzumi", "commander-outbox");
|
|
27875
|
+
}
|
|
27876
|
+
function controlCursorsDir() {
|
|
27877
|
+
const override = process.env.LINZUMI_CONTROL_CURSOR_DIR?.trim();
|
|
27878
|
+
if (override !== void 0 && override !== "") {
|
|
27879
|
+
return override;
|
|
27880
|
+
}
|
|
27881
|
+
return join22(homedir15(), ".linzumi", "control-cursors");
|
|
26334
27882
|
}
|
|
26335
27883
|
function controlCursorStorePath(runnerId) {
|
|
26336
27884
|
const sanitized = runnerId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
|
|
26337
|
-
const digest =
|
|
27885
|
+
const digest = createHash7("sha256").update(runnerId).digest("hex").slice(0, 8);
|
|
26338
27886
|
const stem = sanitized === "" ? "runner" : sanitized;
|
|
26339
|
-
return
|
|
26340
|
-
homedir14(),
|
|
26341
|
-
".linzumi",
|
|
26342
|
-
"control-cursors",
|
|
26343
|
-
`${stem}.${digest}.json`
|
|
26344
|
-
);
|
|
27887
|
+
return join22(controlCursorsDir(), `${stem}.${digest}.json`);
|
|
26345
27888
|
}
|
|
26346
27889
|
function appliedStartTurnStorePath(runnerId) {
|
|
26347
27890
|
const sanitized = runnerId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
|
|
26348
|
-
const digest =
|
|
27891
|
+
const digest = createHash7("sha256").update(runnerId).digest("hex").slice(0, 8);
|
|
27892
|
+
const stem = sanitized === "" ? "runner" : sanitized;
|
|
27893
|
+
return join22(controlCursorsDir(), `${stem}.${digest}.start-turn.json`);
|
|
27894
|
+
}
|
|
27895
|
+
function appliedSourceSeqStorePath(runnerId) {
|
|
27896
|
+
const sanitized = runnerId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
|
|
27897
|
+
const digest = createHash7("sha256").update(runnerId).digest("hex").slice(0, 8);
|
|
26349
27898
|
const stem = sanitized === "" ? "runner" : sanitized;
|
|
26350
|
-
return
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
"control-cursors",
|
|
26354
|
-
`${stem}.${digest}.start-turn.json`
|
|
27899
|
+
return join22(
|
|
27900
|
+
controlCursorsDir(),
|
|
27901
|
+
`${stem}.${digest}.source-seq.json`
|
|
26355
27902
|
);
|
|
26356
27903
|
}
|
|
26357
27904
|
function startTurnWatermarkKey(topic, threadId) {
|
|
@@ -26473,6 +28020,7 @@ function createConnectionStalenessTracker(log2, now = Date.now, thresholdMs = co
|
|
|
26473
28020
|
}
|
|
26474
28021
|
function createControlCursorFileStore(path2, log2, options) {
|
|
26475
28022
|
const discardSeqsOnFirstEpochAdoption = options?.discardSeqsOnFirstEpochAdoption === true;
|
|
28023
|
+
const fsyncOnWrite = options?.fsyncOnWrite === true;
|
|
26476
28024
|
const { cursors, epoch: persistedEpoch } = readControlCursorFile(path2, log2);
|
|
26477
28025
|
let epoch = persistedEpoch;
|
|
26478
28026
|
let dirty = false;
|
|
@@ -26483,18 +28031,32 @@ function createControlCursorFileStore(path2, log2, options) {
|
|
|
26483
28031
|
}
|
|
26484
28032
|
dirty = false;
|
|
26485
28033
|
try {
|
|
26486
|
-
|
|
28034
|
+
mkdirSync14(dirname15(path2), { recursive: true });
|
|
26487
28035
|
const tmpPath = `${path2}.tmp`;
|
|
26488
|
-
|
|
26489
|
-
|
|
26490
|
-
|
|
26491
|
-
|
|
26492
|
-
|
|
26493
|
-
|
|
26494
|
-
|
|
26495
|
-
|
|
26496
|
-
|
|
26497
|
-
|
|
28036
|
+
const contents = `${JSON.stringify({
|
|
28037
|
+
...epoch === void 0 ? {} : { [controlCursorEpochKey]: epoch },
|
|
28038
|
+
...Object.fromEntries(cursors)
|
|
28039
|
+
})}
|
|
28040
|
+
`;
|
|
28041
|
+
if (fsyncOnWrite) {
|
|
28042
|
+
const fileFd = openSync5(tmpPath, "w");
|
|
28043
|
+
try {
|
|
28044
|
+
writeFileSync12(fileFd, contents, "utf8");
|
|
28045
|
+
fsyncSync2(fileFd);
|
|
28046
|
+
} finally {
|
|
28047
|
+
closeSync4(fileFd);
|
|
28048
|
+
}
|
|
28049
|
+
renameSync5(tmpPath, path2);
|
|
28050
|
+
const dirFd = openSync5(dirname15(path2), "r");
|
|
28051
|
+
try {
|
|
28052
|
+
fsyncSync2(dirFd);
|
|
28053
|
+
} finally {
|
|
28054
|
+
closeSync4(dirFd);
|
|
28055
|
+
}
|
|
28056
|
+
} else {
|
|
28057
|
+
writeFileSync12(tmpPath, contents, "utf8");
|
|
28058
|
+
renameSync5(tmpPath, path2);
|
|
28059
|
+
}
|
|
26498
28060
|
} catch (error) {
|
|
26499
28061
|
log2("control_cursor_store.write_failed", {
|
|
26500
28062
|
path: path2,
|
|
@@ -26593,7 +28155,7 @@ function readControlCursorFile(path2, log2) {
|
|
|
26593
28155
|
const cursors = /* @__PURE__ */ new Map();
|
|
26594
28156
|
let raw;
|
|
26595
28157
|
try {
|
|
26596
|
-
raw =
|
|
28158
|
+
raw = readFileSync18(path2, "utf8");
|
|
26597
28159
|
} catch (error) {
|
|
26598
28160
|
if (!isErrnoCode2(error, "ENOENT")) {
|
|
26599
28161
|
log2("control_cursor_store.read_failed", {
|
|
@@ -26938,21 +28500,47 @@ function makeRunnerLogger(options) {
|
|
|
26938
28500
|
consoleReporter
|
|
26939
28501
|
);
|
|
26940
28502
|
}
|
|
26941
|
-
function installCleanupHandlers(close) {
|
|
26942
|
-
const
|
|
26943
|
-
|
|
26944
|
-
|
|
28503
|
+
function installCleanupHandlers(close, drainHooks, log2) {
|
|
28504
|
+
const uninstallDrain = installDrainOnSigterm({
|
|
28505
|
+
steps: {
|
|
28506
|
+
stopAccepting: () => {
|
|
28507
|
+
drainHooks.stopAccepting();
|
|
28508
|
+
},
|
|
28509
|
+
// SCOPE NOTE (BET1, partial): checkpointing an in-flight turn on a LIVE
|
|
28510
|
+
// worker (persisting enough codex turn state to resume it) is NOT
|
|
28511
|
+
// implemented in this branch - that turn still relies on the server's
|
|
28512
|
+
// rejoin replay. What IS durable is every durably-owned message that was
|
|
28513
|
+
// ENQUEUED into the per-thread inbound queue (the respawn-window / spawn-on-
|
|
28514
|
+
// miss path): flushInboundQueues fsyncs those below, and the boot/rejoin
|
|
28515
|
+
// drain replays them. Left as an explicit no-op (not silently absent) so the
|
|
28516
|
+
// gap is visible rather than implied-handled.
|
|
28517
|
+
checkpointCurrentTurn: () => void 0,
|
|
28518
|
+
flushInboundQueues: () => {
|
|
28519
|
+
drainHooks.flushInboundQueues();
|
|
28520
|
+
},
|
|
28521
|
+
// The final flush also performs the connection teardown via close(): the
|
|
28522
|
+
// cleanup stack runs after kandan.close() so no advance races the final
|
|
28523
|
+
// cursor/watermark write.
|
|
28524
|
+
flushOutbox: () => close().catch(() => void 0)
|
|
28525
|
+
},
|
|
28526
|
+
deadlineMs: runnerDrainExitDeadlineMs,
|
|
28527
|
+
log: (event, fields) => log2(event, fields),
|
|
28528
|
+
// SIGHUP is a drain-AND-EXIT signal too: a detached child of the Electron
|
|
28529
|
+
// shell that receives a parent hangup must drain its inbound queue + cursor
|
|
28530
|
+
// stores and then EXIT, never linger as a defunct lock-holder that does no
|
|
28531
|
+
// work (the prior close()-without-exit handler left a zombie that stopped
|
|
28532
|
+
// accepting, tore down its connection, yet never exited - stranding prior
|
|
28533
|
+
// jobs and holding the runner lock). installDrainOnSigterm's handler exits
|
|
28534
|
+
// after the drain (deadline-bounded), so routing SIGHUP through it fixes the
|
|
28535
|
+
// orphan-zombie regression.
|
|
28536
|
+
signals: ["SIGTERM", "SIGINT", "SIGHUP"]
|
|
28537
|
+
});
|
|
26945
28538
|
const closeOnExit = () => {
|
|
26946
28539
|
void close().catch(() => void 0);
|
|
26947
28540
|
};
|
|
26948
|
-
process.once("SIGINT", closeAndExit);
|
|
26949
|
-
process.once("SIGTERM", closeAndExit);
|
|
26950
|
-
process.once("SIGHUP", closeAndExit);
|
|
26951
28541
|
process.once("exit", closeOnExit);
|
|
26952
28542
|
return () => {
|
|
26953
|
-
|
|
26954
|
-
process.off("SIGTERM", closeAndExit);
|
|
26955
|
-
process.off("SIGHUP", closeAndExit);
|
|
28543
|
+
uninstallDrain();
|
|
26956
28544
|
process.off("exit", closeOnExit);
|
|
26957
28545
|
};
|
|
26958
28546
|
}
|
|
@@ -27255,7 +28843,7 @@ async function applyCodexUnavailableControl(kandan, topic, instanceId, control,
|
|
|
27255
28843
|
}
|
|
27256
28844
|
throw new Error(error);
|
|
27257
28845
|
}
|
|
27258
|
-
async function applyControl(codex, kandan, topic, instanceId, options, agentProviders, allowedCwds, remoteCodexSandboxRunner, activeRemoteCodexExecRequests, activeClaudeCodeSessions, pendingClaudeCodeApprovals, ensureClaudeCodeForwardPortSession, disposeClaudeCodeForwardPortSession, control, log2, onStartedThread, onThreadProcessStart) {
|
|
28846
|
+
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) {
|
|
27259
28847
|
if (codex === void 0) {
|
|
27260
28848
|
switch (control.type) {
|
|
27261
28849
|
case "remote_codex_exec":
|
|
@@ -27448,6 +29036,9 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
|
|
|
27448
29036
|
ensureClaudeCodeForwardPortSession,
|
|
27449
29037
|
disposeClaudeCodeForwardPortSession,
|
|
27450
29038
|
resumeSessionId: normalizedWorkDescription(control.resumeSessionId),
|
|
29039
|
+
leaseEpochFor,
|
|
29040
|
+
onLiveSourceSeqAccepted,
|
|
29041
|
+
onUndeliveredFollowUpsAtClose,
|
|
27451
29042
|
setStartupStage: (stage) => {
|
|
27452
29043
|
startupStage = stage;
|
|
27453
29044
|
}
|
|
@@ -27650,6 +29241,51 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
|
|
|
27650
29241
|
message
|
|
27651
29242
|
};
|
|
27652
29243
|
}
|
|
29244
|
+
const followUpThreadId = optionalThreadControlField(
|
|
29245
|
+
control,
|
|
29246
|
+
"threadId"
|
|
29247
|
+
);
|
|
29248
|
+
const followUpChannel = optionalThreadControlField(
|
|
29249
|
+
control,
|
|
29250
|
+
"channel"
|
|
29251
|
+
);
|
|
29252
|
+
if (sourceSeq !== void 0 && followUpThreadId !== void 0 && followUpChannel !== void 0) {
|
|
29253
|
+
captureLiveFollowUpEvent?.(followUpChannel, followUpThreadId, {
|
|
29254
|
+
seq: sourceSeq,
|
|
29255
|
+
type: "thread.message",
|
|
29256
|
+
actorKind: void 0,
|
|
29257
|
+
actorSlug: void 0,
|
|
29258
|
+
actorUserId: void 0,
|
|
29259
|
+
threadId: followUpThreadId,
|
|
29260
|
+
threadTitle: void 0,
|
|
29261
|
+
replyToSeq: integerValue(control.rootSeq),
|
|
29262
|
+
localRunnerEventType: void 0,
|
|
29263
|
+
body: workDescription,
|
|
29264
|
+
attachments
|
|
29265
|
+
});
|
|
29266
|
+
}
|
|
29267
|
+
if (followUpChannel !== void 0 && followUpThreadId !== void 0 && liveSourceSeqAlreadyApplied?.({
|
|
29268
|
+
channel: followUpChannel,
|
|
29269
|
+
threadId: followUpThreadId,
|
|
29270
|
+
sourceSeq
|
|
29271
|
+
}) === true) {
|
|
29272
|
+
log2("claude_code.message_duplicate_ignored", {
|
|
29273
|
+
linzumi_thread_id: followUpThreadId,
|
|
29274
|
+
claude_session_id: codexThreadId,
|
|
29275
|
+
body_preview: claudeConsoleBodyPreview(workDescription),
|
|
29276
|
+
source_seq: sourceSeq ?? null
|
|
29277
|
+
});
|
|
29278
|
+
return {
|
|
29279
|
+
instanceId,
|
|
29280
|
+
controlType: control.type,
|
|
29281
|
+
agentProvider: "claude-code",
|
|
29282
|
+
cwd: cwd.cwd,
|
|
29283
|
+
matchedRoot: cwd.matchedRoot,
|
|
29284
|
+
codexThreadId,
|
|
29285
|
+
queuedInput: true,
|
|
29286
|
+
duplicate: true
|
|
29287
|
+
};
|
|
29288
|
+
}
|
|
27653
29289
|
try {
|
|
27654
29290
|
const enqueueResult = activeSession.enqueueInput({
|
|
27655
29291
|
content: contentResult.content,
|
|
@@ -27754,6 +29390,9 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
|
|
|
27754
29390
|
pendingClaudeCodeApprovals,
|
|
27755
29391
|
ensureClaudeCodeForwardPortSession,
|
|
27756
29392
|
disposeClaudeCodeForwardPortSession,
|
|
29393
|
+
leaseEpochFor,
|
|
29394
|
+
onLiveSourceSeqAccepted,
|
|
29395
|
+
onUndeliveredFollowUpsAtClose,
|
|
27757
29396
|
setStartupStage: (stage) => {
|
|
27758
29397
|
startupStage = stage;
|
|
27759
29398
|
}
|
|
@@ -27797,6 +29436,30 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
|
|
|
27797
29436
|
if (startedThreadSession === void 0 || sourceSeq === void 0) {
|
|
27798
29437
|
throw new Error("cannot reconnect a Kandan thread without a session");
|
|
27799
29438
|
}
|
|
29439
|
+
const reconnectChannel = optionalThreadControlField(control, "channel");
|
|
29440
|
+
const reconnectThreadId = optionalThreadControlField(
|
|
29441
|
+
control,
|
|
29442
|
+
"threadId"
|
|
29443
|
+
);
|
|
29444
|
+
if (reconnectChannel !== void 0 && reconnectThreadId !== void 0 && liveSourceSeqAlreadyApplied?.({
|
|
29445
|
+
channel: reconnectChannel,
|
|
29446
|
+
threadId: reconnectThreadId,
|
|
29447
|
+
sourceSeq
|
|
29448
|
+
}) === true) {
|
|
29449
|
+
log2("kandan.reconnect_thread_seq_already_applied", {
|
|
29450
|
+
linzumi_thread_id: reconnectThreadId,
|
|
29451
|
+
codex_thread_id: codexThreadId,
|
|
29452
|
+
source_seq: sourceSeq
|
|
29453
|
+
});
|
|
29454
|
+
return {
|
|
29455
|
+
instanceId,
|
|
29456
|
+
controlType: control.type,
|
|
29457
|
+
cwd: cwd.cwd,
|
|
29458
|
+
matchedRoot: cwd.matchedRoot,
|
|
29459
|
+
codexThreadId,
|
|
29460
|
+
duplicate: true
|
|
29461
|
+
};
|
|
29462
|
+
}
|
|
27800
29463
|
await resumeCodexThreadForReconnect(
|
|
27801
29464
|
codex,
|
|
27802
29465
|
codexThreadId,
|
|
@@ -28543,11 +30206,13 @@ function claudeCodeTurnStallThresholdMs(env) {
|
|
|
28543
30206
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultClaudeCodeTurnStallThresholdMs;
|
|
28544
30207
|
}
|
|
28545
30208
|
function createClaudeCodeInputQueue(input) {
|
|
30209
|
+
const onSourceSeqDelivered = input.onSourceSeqDelivered;
|
|
28546
30210
|
const pendingMessages = [
|
|
28547
30211
|
claudeCodeUserInputMessage(input.content)
|
|
28548
30212
|
];
|
|
28549
30213
|
const pendingSourceSeqs = [input.sourceSeq];
|
|
28550
30214
|
const deferredFollowUps = [];
|
|
30215
|
+
const deliveredSourceSeqs = /* @__PURE__ */ new Set();
|
|
28551
30216
|
const waiters = [];
|
|
28552
30217
|
const state = {
|
|
28553
30218
|
closed: false
|
|
@@ -28563,6 +30228,10 @@ function createClaudeCodeInputQueue(input) {
|
|
|
28563
30228
|
};
|
|
28564
30229
|
const deliverNow = (message, sourceSeq) => {
|
|
28565
30230
|
pendingSourceSeqs.push(sourceSeq);
|
|
30231
|
+
if (sourceSeq !== void 0 && onSourceSeqDelivered !== void 0 && !deliveredSourceSeqs.has(sourceSeq)) {
|
|
30232
|
+
deliveredSourceSeqs.add(sourceSeq);
|
|
30233
|
+
onSourceSeqDelivered(sourceSeq);
|
|
30234
|
+
}
|
|
28566
30235
|
const waiter = waiters.shift();
|
|
28567
30236
|
if (waiter === void 0) {
|
|
28568
30237
|
pendingMessages.push(message);
|
|
@@ -28631,6 +30300,17 @@ function createClaudeCodeInputQueue(input) {
|
|
|
28631
30300
|
},
|
|
28632
30301
|
enqueue,
|
|
28633
30302
|
enqueueSteer,
|
|
30303
|
+
// BLOCKER A (don't drop deferred follow-ups on a kill): the parked,
|
|
30304
|
+
// never-delivered follow-ups (a turn was in flight at close, so close()'s
|
|
30305
|
+
// drain is intentionally skipped to avoid folding them into the active turn).
|
|
30306
|
+
// Their watermark was NOT committed (commit fires only at deliverNow), so the
|
|
30307
|
+
// runner must re-enqueue them into the DURABLE inbound queue; a kill-before-
|
|
30308
|
+
// run then replays + runs them exactly once. Drains the parked list so a
|
|
30309
|
+
// double-call cannot double-enqueue. Returns the un-run seqs+messages.
|
|
30310
|
+
drainUndeliveredFollowUps: () => {
|
|
30311
|
+
const drained = deferredFollowUps.splice(0);
|
|
30312
|
+
return drained;
|
|
30313
|
+
},
|
|
28634
30314
|
completeTurn: () => {
|
|
28635
30315
|
const completed = pendingSourceSeqs.shift();
|
|
28636
30316
|
if (!state.closed && pendingSourceSeqs.length === 0) {
|
|
@@ -28857,7 +30537,21 @@ async function startClaudeCodeProviderInstance(args) {
|
|
|
28857
30537
|
}
|
|
28858
30538
|
const inputQueue = createClaudeCodeInputQueue({
|
|
28859
30539
|
content: initialContentResult.content,
|
|
28860
|
-
sourceSeq
|
|
30540
|
+
sourceSeq,
|
|
30541
|
+
// BLOCKER A: commit the persisted dedup watermark at DISPATCH (deliverNow),
|
|
30542
|
+
// not at enqueue-accept. enqueueInput used to commit unconditionally right
|
|
30543
|
+
// after inputQueue.enqueue, but enqueue PARKS a mid-turn follow-up in
|
|
30544
|
+
// deferredFollowUps (a bare return) and a close-in-flight drops it - so a
|
|
30545
|
+
// never-run seq was marked applied and its replay was gated 'ignored' (the
|
|
30546
|
+
// drop). Committing only when the seq is actually delivered onto the live
|
|
30547
|
+
// stream means a parked-but-unrun seq is never marked applied.
|
|
30548
|
+
onSourceSeqDelivered: (deliveredSeq) => {
|
|
30549
|
+
args.onLiveSourceSeqAccepted?.({
|
|
30550
|
+
channel,
|
|
30551
|
+
threadId,
|
|
30552
|
+
sourceSeq: deliveredSeq
|
|
30553
|
+
});
|
|
30554
|
+
}
|
|
28861
30555
|
});
|
|
28862
30556
|
const developerPrompt = normalizedWorkDescription(
|
|
28863
30557
|
args.control.developerPrompt
|
|
@@ -28966,7 +30660,8 @@ async function startClaudeCodeProviderInstance(args) {
|
|
|
28966
30660
|
channelSlug: channel,
|
|
28967
30661
|
kandanThreadId: threadId,
|
|
28968
30662
|
codexThreadId: activeSessionId ?? args.resumeSessionId,
|
|
28969
|
-
rootSeq
|
|
30663
|
+
rootSeq,
|
|
30664
|
+
leaseEpoch: args.leaseEpochFor?.(threadId)
|
|
28970
30665
|
}),
|
|
28971
30666
|
push: async (event, payload, _timeoutMs, onSent) => {
|
|
28972
30667
|
const reply = await args.kandan.push(args.topic, event, payload, onSent);
|
|
@@ -29014,7 +30709,7 @@ async function startClaudeCodeProviderInstance(args) {
|
|
|
29014
30709
|
log: args.log
|
|
29015
30710
|
});
|
|
29016
30711
|
const liveBashCapture = args.options.claudeCodeRunner === void 0 && claudeLiveBashCaptureEnabled(process.env) ? createClaudeCodeLiveBashCapture({
|
|
29017
|
-
captureDir:
|
|
30712
|
+
captureDir: join22(
|
|
29018
30713
|
tmpdir3(),
|
|
29019
30714
|
`linzumi-claude-live-bash-${args.instanceId}`
|
|
29020
30715
|
),
|
|
@@ -29133,6 +30828,7 @@ async function startClaudeCodeProviderInstance(args) {
|
|
|
29133
30828
|
linzumi_thread_id: threadId,
|
|
29134
30829
|
claude_session_id: event.sessionId
|
|
29135
30830
|
});
|
|
30831
|
+
args.onLiveSourceSeqAccepted?.({ channel, threadId, sourceSeq });
|
|
29136
30832
|
args.activeClaudeCodeSessions.set(event.sessionId, {
|
|
29137
30833
|
abortController,
|
|
29138
30834
|
workspace,
|
|
@@ -29341,6 +31037,15 @@ async function startClaudeCodeProviderInstance(args) {
|
|
|
29341
31037
|
onTranscriptEvent
|
|
29342
31038
|
});
|
|
29343
31039
|
} finally {
|
|
31040
|
+
const undeliveredFollowUps = inputQueue.drainUndeliveredFollowUps();
|
|
31041
|
+
const undeliveredSeqs = undeliveredFollowUps.map((followUp) => followUp.sourceSeq).filter((seq) => seq !== void 0);
|
|
31042
|
+
if (undeliveredSeqs.length > 0 && args.onUndeliveredFollowUpsAtClose !== void 0) {
|
|
31043
|
+
args.onUndeliveredFollowUpsAtClose({
|
|
31044
|
+
channel,
|
|
31045
|
+
threadId,
|
|
31046
|
+
sourceSeqs: undeliveredSeqs
|
|
31047
|
+
});
|
|
31048
|
+
}
|
|
29344
31049
|
inputQueue.close();
|
|
29345
31050
|
mcpAuthCleanup?.();
|
|
29346
31051
|
liveBashCapture?.close();
|
|
@@ -29701,7 +31406,8 @@ ${developerPrompt}`,
|
|
|
29701
31406
|
metadata: {
|
|
29702
31407
|
local_codex_runner: {
|
|
29703
31408
|
event_type: "codex_start_instructions",
|
|
29704
|
-
codex_thread_id: codexThreadId
|
|
31409
|
+
codex_thread_id: codexThreadId,
|
|
31410
|
+
instructions_body: developerPrompt
|
|
29705
31411
|
}
|
|
29706
31412
|
}
|
|
29707
31413
|
},
|
|
@@ -29864,15 +31570,15 @@ function rehydrationControlSnapshot(args) {
|
|
|
29864
31570
|
...runtimeSettings.fast === void 0 ? {} : { fast: runtimeSettings.fast }
|
|
29865
31571
|
};
|
|
29866
31572
|
}
|
|
29867
|
-
function
|
|
29868
|
-
|
|
29869
|
-
|
|
29870
|
-
|
|
29871
|
-
|
|
29872
|
-
|
|
29873
|
-
|
|
29874
|
-
|
|
29875
|
-
}
|
|
31573
|
+
function deadLetterFailedReason(reason) {
|
|
31574
|
+
switch (reason) {
|
|
31575
|
+
case "exhausted":
|
|
31576
|
+
return "couldn't resume this job after several attempts; please resend your last message to continue.";
|
|
31577
|
+
case "withheld":
|
|
31578
|
+
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.";
|
|
31579
|
+
case "permanent":
|
|
31580
|
+
return "couldn't resume this job's coding session.";
|
|
31581
|
+
}
|
|
29876
31582
|
}
|
|
29877
31583
|
function rehydrationReconnectControl(record) {
|
|
29878
31584
|
const snapshot = record.control;
|
|
@@ -30920,8 +32626,8 @@ function onboardingConversationOfficeImport(response) {
|
|
|
30920
32626
|
return objectValue(metadata?.office_channel_import);
|
|
30921
32627
|
}
|
|
30922
32628
|
function onboardingConversationImportClientMessageId(importKey, itemKey) {
|
|
30923
|
-
const importHash =
|
|
30924
|
-
const itemHash =
|
|
32629
|
+
const importHash = createHash7("sha256").update(importKey).digest("hex");
|
|
32630
|
+
const itemHash = createHash7("sha256").update(itemKey).digest("hex");
|
|
30925
32631
|
return `onboarding-import:${importHash.slice(0, 32)}:${itemHash.slice(0, 24)}`;
|
|
30926
32632
|
}
|
|
30927
32633
|
async function uploadLocalConversationAttachments(args) {
|
|
@@ -31019,7 +32725,7 @@ async function localConversationAttachmentFile(candidatePath, attachment) {
|
|
|
31019
32725
|
};
|
|
31020
32726
|
}
|
|
31021
32727
|
case "path": {
|
|
31022
|
-
const path2 = isAbsolute5(attachment.path) ? attachment.path : resolve8(
|
|
32728
|
+
const path2 = isAbsolute5(attachment.path) ? attachment.path : resolve8(dirname15(candidatePath), attachment.path);
|
|
31023
32729
|
try {
|
|
31024
32730
|
const bytes = await readFile2(path2);
|
|
31025
32731
|
return {
|
|
@@ -31474,9 +33180,9 @@ function mcpOwnerUsername(options) {
|
|
|
31474
33180
|
return options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
|
|
31475
33181
|
}
|
|
31476
33182
|
function writeEphemeralMcpAuthFile(options) {
|
|
31477
|
-
const directory = mkdtempSync4(
|
|
33183
|
+
const directory = mkdtempSync4(join22(tmpdir3(), "linzumi-mcp-auth-"));
|
|
31478
33184
|
chmodSync2(directory, 448);
|
|
31479
|
-
const path2 =
|
|
33185
|
+
const path2 = join22(directory, "auth.json");
|
|
31480
33186
|
writeCachedLocalRunnerToken({
|
|
31481
33187
|
kandanUrl: options.kandanUrl,
|
|
31482
33188
|
accessToken: options.token,
|
|
@@ -31552,7 +33258,7 @@ function configuredAllowedCwds(values, options = {}) {
|
|
|
31552
33258
|
const absolutePath = resolve8(expandUserPath(value));
|
|
31553
33259
|
try {
|
|
31554
33260
|
if (options.createMissing === true) {
|
|
31555
|
-
|
|
33261
|
+
mkdirSync14(absolutePath, { recursive: true });
|
|
31556
33262
|
}
|
|
31557
33263
|
const realPath = realpathSync6(absolutePath);
|
|
31558
33264
|
allowedCwds.push(
|
|
@@ -31589,7 +33295,7 @@ function allowedCwdProjects(allowedCwds) {
|
|
|
31589
33295
|
});
|
|
31590
33296
|
}
|
|
31591
33297
|
function isGitProjectDirectory(cwd) {
|
|
31592
|
-
const gitPath =
|
|
33298
|
+
const gitPath = join22(cwd, ".git");
|
|
31593
33299
|
try {
|
|
31594
33300
|
const gitPathStats = statSync3(gitPath);
|
|
31595
33301
|
return gitPathStats.isDirectory() || gitPathStats.isFile();
|
|
@@ -31612,9 +33318,9 @@ function browseRunnerDirectory(control, options) {
|
|
|
31612
33318
|
error: "not_directory"
|
|
31613
33319
|
};
|
|
31614
33320
|
}
|
|
31615
|
-
const parent =
|
|
33321
|
+
const parent = dirname15(currentPath);
|
|
31616
33322
|
const entries = readdirSync4(currentPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => visibleRunnerDirectoryEntryName(entry.name)).map((entry) => {
|
|
31617
|
-
const path2 =
|
|
33323
|
+
const path2 = join22(currentPath, entry.name);
|
|
31618
33324
|
return {
|
|
31619
33325
|
name: entry.name,
|
|
31620
33326
|
path: path2,
|
|
@@ -31655,7 +33361,7 @@ function projectDirectoryName(name) {
|
|
|
31655
33361
|
function availableProjectDirectoryName(projectsRoot, baseName, suffix = 0) {
|
|
31656
33362
|
for (let nextSuffix = suffix; ; nextSuffix += 1) {
|
|
31657
33363
|
const candidate = nextSuffix === 0 ? baseName : `${baseName}-${nextSuffix}`;
|
|
31658
|
-
if (!projectPathExists(
|
|
33364
|
+
if (!projectPathExists(join22(projectsRoot, candidate))) {
|
|
31659
33365
|
return candidate;
|
|
31660
33366
|
}
|
|
31661
33367
|
}
|
|
@@ -31683,9 +33389,9 @@ function createRunnerProject(control, options, allowedCwds) {
|
|
|
31683
33389
|
error: "invalid_project_template"
|
|
31684
33390
|
};
|
|
31685
33391
|
}
|
|
31686
|
-
const projectsRoot =
|
|
33392
|
+
const projectsRoot = join22(currentHomeDirectory(), "linzumi");
|
|
31687
33393
|
const resolvedProjectDirName = template === "hello_linzumi_demo" ? availableProjectDirectoryName(projectsRoot, projectDirName) : projectDirName;
|
|
31688
|
-
const projectPath =
|
|
33394
|
+
const projectPath = join22(projectsRoot, resolvedProjectDirName);
|
|
31689
33395
|
let createdProjectPath = false;
|
|
31690
33396
|
try {
|
|
31691
33397
|
if (template !== "hello_linzumi_demo" && projectPathExists(projectPath)) {
|
|
@@ -31697,7 +33403,7 @@ function createRunnerProject(control, options, allowedCwds) {
|
|
|
31697
33403
|
error: "project_directory_exists"
|
|
31698
33404
|
};
|
|
31699
33405
|
}
|
|
31700
|
-
|
|
33406
|
+
mkdirSync14(projectsRoot, { recursive: true });
|
|
31701
33407
|
if (template === "hello_linzumi_demo") {
|
|
31702
33408
|
createdProjectPath = true;
|
|
31703
33409
|
createHelloLinzumiProject({
|
|
@@ -31705,7 +33411,7 @@ function createRunnerProject(control, options, allowedCwds) {
|
|
|
31705
33411
|
name: resolvedProjectDirName
|
|
31706
33412
|
});
|
|
31707
33413
|
} else {
|
|
31708
|
-
|
|
33414
|
+
mkdirSync14(projectPath, { recursive: false });
|
|
31709
33415
|
createdProjectPath = true;
|
|
31710
33416
|
}
|
|
31711
33417
|
const git = spawnSync5("git", ["init"], {
|
|
@@ -31818,7 +33524,7 @@ async function suggestRunnerTasks(control, options, allowedCwds) {
|
|
|
31818
33524
|
};
|
|
31819
33525
|
}
|
|
31820
33526
|
}
|
|
31821
|
-
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;
|
|
33527
|
+
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;
|
|
31822
33528
|
var init_runner = __esm({
|
|
31823
33529
|
"src/runner.ts"() {
|
|
31824
33530
|
"use strict";
|
|
@@ -31826,6 +33532,7 @@ var init_runner = __esm({
|
|
|
31826
33532
|
init_runnerThreadSessionRegistry();
|
|
31827
33533
|
init_outboxJournal();
|
|
31828
33534
|
init_channelSessionSupport();
|
|
33535
|
+
init_channelSessionSupport();
|
|
31829
33536
|
init_commanderAttachments();
|
|
31830
33537
|
init_claudeCodePipeline();
|
|
31831
33538
|
init_claudeCodePlanMirror();
|
|
@@ -31868,11 +33575,18 @@ var init_runner = __esm({
|
|
|
31868
33575
|
init_signupTaskSuggestions();
|
|
31869
33576
|
init_userFacingErrors();
|
|
31870
33577
|
init_remoteCodexSandboxRunner();
|
|
33578
|
+
init_durableInboundQueue();
|
|
33579
|
+
init_appliedSourceSeqWatermark();
|
|
33580
|
+
init_threadReconcilerLock();
|
|
33581
|
+
init_runnerLifecycleGuards();
|
|
33582
|
+
init_leaseEpochGuard();
|
|
33583
|
+
init_resumeKeystone();
|
|
31871
33584
|
THREAD_RUNNER_READY_TIMEOUT_MS = 3e4;
|
|
31872
33585
|
THREAD_RUNNER_REAPER_INTERVAL_MS = 3e4;
|
|
31873
33586
|
onboardingDiscoveryAgentStartKeys = /* @__PURE__ */ new Set();
|
|
31874
33587
|
onboardingConversationImportKeys = /* @__PURE__ */ new Set();
|
|
31875
33588
|
onboardingConversationDiscoveryReportLimit = 100;
|
|
33589
|
+
CODEX_AUTH_STATUS_PROBE_TIMEOUT_MS = 3e3;
|
|
31876
33590
|
staleStartInstanceWindowMs = 6e4;
|
|
31877
33591
|
connectionStaleThresholdMs = 12e4;
|
|
31878
33592
|
connectionUnrecoverableAfterMs = 10 * 60 * 1e3;
|
|
@@ -31885,6 +33599,7 @@ var init_runner = __esm({
|
|
|
31885
33599
|
"turn/canceled",
|
|
31886
33600
|
"turn/cancelled"
|
|
31887
33601
|
]);
|
|
33602
|
+
runnerDrainExitDeadlineMs = 8e3;
|
|
31888
33603
|
claudeSessionStoreSequenceHighWater = /* @__PURE__ */ new Map();
|
|
31889
33604
|
claudeCodeLlmProxySessionHeader = "x-linzumi-llm-proxy-token";
|
|
31890
33605
|
onboardingConversationTitleFirstMessages = 4;
|
|
@@ -31897,7 +33612,7 @@ var init_runner = __esm({
|
|
|
31897
33612
|
});
|
|
31898
33613
|
|
|
31899
33614
|
// src/kandanTls.ts
|
|
31900
|
-
import { existsSync as existsSync14, readFileSync as
|
|
33615
|
+
import { existsSync as existsSync14, readFileSync as readFileSync19 } from "node:fs";
|
|
31901
33616
|
import { Agent } from "undici";
|
|
31902
33617
|
import WsWebSocket from "ws";
|
|
31903
33618
|
function kandanTlsTrustFromEnv() {
|
|
@@ -31911,7 +33626,7 @@ function kandanTlsTrustFromCaFile(caFile) {
|
|
|
31911
33626
|
if (!existsSync14(trimmed)) {
|
|
31912
33627
|
throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
|
|
31913
33628
|
}
|
|
31914
|
-
const ca =
|
|
33629
|
+
const ca = readFileSync19(trimmed, "utf8");
|
|
31915
33630
|
return {
|
|
31916
33631
|
caFile: trimmed,
|
|
31917
33632
|
ca,
|
|
@@ -50695,7 +52410,7 @@ var init_RemoveFileError = __esm({
|
|
|
50695
52410
|
|
|
50696
52411
|
// ../../node_modules/@inquirer/external-editor/dist/esm/index.js
|
|
50697
52412
|
import { spawn as spawn11, spawnSync as spawnSync6 } from "child_process";
|
|
50698
|
-
import { readFileSync as
|
|
52413
|
+
import { readFileSync as readFileSync22, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "fs";
|
|
50699
52414
|
import path from "node:path";
|
|
50700
52415
|
import os from "node:os";
|
|
50701
52416
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
@@ -50819,7 +52534,7 @@ var init_esm5 = __esm({
|
|
|
50819
52534
|
}
|
|
50820
52535
|
readTemporaryFile() {
|
|
50821
52536
|
try {
|
|
50822
|
-
const tempFileBuffer =
|
|
52537
|
+
const tempFileBuffer = readFileSync22(this.tempFile);
|
|
50823
52538
|
if (tempFileBuffer.length === 0) {
|
|
50824
52539
|
this.text = "";
|
|
50825
52540
|
} else {
|
|
@@ -52188,17 +53903,17 @@ import { spawn as spawn12, spawnSync as spawnSync7 } from "node:child_process";
|
|
|
52188
53903
|
import {
|
|
52189
53904
|
existsSync as existsSync17,
|
|
52190
53905
|
constants as fsConstants,
|
|
52191
|
-
mkdirSync as
|
|
53906
|
+
mkdirSync as mkdirSync17,
|
|
52192
53907
|
mkdtempSync as mkdtempSync6,
|
|
52193
|
-
readFileSync as
|
|
53908
|
+
readFileSync as readFileSync23,
|
|
52194
53909
|
readdirSync as readdirSync5,
|
|
52195
53910
|
rmSync as rmSync7,
|
|
52196
53911
|
statSync as statSync4,
|
|
52197
53912
|
writeFileSync as writeFileSync16
|
|
52198
53913
|
} from "node:fs";
|
|
52199
53914
|
import { access } from "node:fs/promises";
|
|
52200
|
-
import { homedir as
|
|
52201
|
-
import { delimiter as delimiter3, dirname as
|
|
53915
|
+
import { homedir as homedir18, tmpdir as tmpdir5 } from "node:os";
|
|
53916
|
+
import { delimiter as delimiter3, dirname as dirname19, join as join26, resolve as resolve10 } from "node:path";
|
|
52202
53917
|
import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
|
|
52203
53918
|
import { emitKeypressEvents } from "node:readline";
|
|
52204
53919
|
function signupHelpText() {
|
|
@@ -52319,7 +54034,7 @@ function defaultSignupDraftStore(serviceUrl) {
|
|
|
52319
54034
|
}
|
|
52320
54035
|
let parsed;
|
|
52321
54036
|
try {
|
|
52322
|
-
parsed = JSON.parse(
|
|
54037
|
+
parsed = JSON.parse(readFileSync23(path2, "utf8"));
|
|
52323
54038
|
} catch (_error) {
|
|
52324
54039
|
return void 0;
|
|
52325
54040
|
}
|
|
@@ -52329,7 +54044,7 @@ function defaultSignupDraftStore(serviceUrl) {
|
|
|
52329
54044
|
return comparableServiceUrl(parsed.serviceUrl) === comparableServiceUrl(serviceUrl) ? parsed : void 0;
|
|
52330
54045
|
},
|
|
52331
54046
|
write: (draft) => {
|
|
52332
|
-
|
|
54047
|
+
mkdirSync17(dirname19(path2), { recursive: true });
|
|
52333
54048
|
writeFileSync16(path2, `${JSON.stringify(draft, null, 2)}
|
|
52334
54049
|
`, {
|
|
52335
54050
|
encoding: "utf8",
|
|
@@ -52342,8 +54057,8 @@ function defaultSignupDraftStore(serviceUrl) {
|
|
|
52342
54057
|
};
|
|
52343
54058
|
}
|
|
52344
54059
|
function defaultSignupDraftPath(serviceUrl) {
|
|
52345
|
-
return
|
|
52346
|
-
|
|
54060
|
+
return join26(
|
|
54061
|
+
dirname19(localConfigPath()),
|
|
52347
54062
|
`signup-draft.${localConfigScopeFileStem(serviceUrl)}.json`
|
|
52348
54063
|
);
|
|
52349
54064
|
}
|
|
@@ -52377,7 +54092,7 @@ function defaultSignupTaskCacheStore(serviceUrl) {
|
|
|
52377
54092
|
}
|
|
52378
54093
|
}
|
|
52379
54094
|
};
|
|
52380
|
-
|
|
54095
|
+
mkdirSync17(dirname19(path2), { recursive: true });
|
|
52381
54096
|
writeFileSync16(path2, `${JSON.stringify(next, null, 2)}
|
|
52382
54097
|
`, {
|
|
52383
54098
|
encoding: "utf8",
|
|
@@ -52387,8 +54102,8 @@ function defaultSignupTaskCacheStore(serviceUrl) {
|
|
|
52387
54102
|
};
|
|
52388
54103
|
}
|
|
52389
54104
|
function defaultSignupTaskCachePath(serviceUrl) {
|
|
52390
|
-
return
|
|
52391
|
-
|
|
54105
|
+
return join26(
|
|
54106
|
+
dirname19(localConfigPath()),
|
|
52392
54107
|
`signup-task-cache.${localConfigScopeFileStem(serviceUrl)}.json`
|
|
52393
54108
|
);
|
|
52394
54109
|
}
|
|
@@ -52398,7 +54113,7 @@ function readSignupTaskCache(path2) {
|
|
|
52398
54113
|
}
|
|
52399
54114
|
let parsed;
|
|
52400
54115
|
try {
|
|
52401
|
-
parsed = JSON.parse(
|
|
54116
|
+
parsed = JSON.parse(readFileSync23(path2, "utf8"));
|
|
52402
54117
|
} catch (_error) {
|
|
52403
54118
|
return { version: 1, entries: {} };
|
|
52404
54119
|
}
|
|
@@ -54283,7 +55998,7 @@ function autocompletePathSuggestions(pathInput) {
|
|
|
54283
55998
|
try {
|
|
54284
55999
|
const showHidden = prefix.startsWith(".");
|
|
54285
56000
|
const currentPath = directoryExists(normalizedInput) ? [normalizedInput.replace(/\/$/, "") || "/"] : [];
|
|
54286
|
-
const childPaths = readdirSync5(directoryPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => showHidden || !entry.name.startsWith(".")).map((entry) =>
|
|
56001
|
+
const childPaths = readdirSync5(directoryPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => showHidden || !entry.name.startsWith(".")).map((entry) => join26(directoryPath, entry.name)).map((path2) => ({
|
|
54287
56002
|
path: path2,
|
|
54288
56003
|
score: fuzzyPathSegmentScore(path2.split("/").at(-1) ?? path2, prefix)
|
|
54289
56004
|
})).filter((candidate) => candidate.score !== void 0).sort((left, right) => {
|
|
@@ -54529,7 +56244,7 @@ async function runSignupPreflights(runtime) {
|
|
|
54529
56244
|
function defaultPreflightRuntime() {
|
|
54530
56245
|
return {
|
|
54531
56246
|
cwd: process.cwd(),
|
|
54532
|
-
homeDir:
|
|
56247
|
+
homeDir: homedir18(),
|
|
54533
56248
|
probeTool,
|
|
54534
56249
|
readGitEmail,
|
|
54535
56250
|
discoverCodeRoots,
|
|
@@ -54674,9 +56389,9 @@ async function suggestTasksWithCodex(projectPath, retryPolicy) {
|
|
|
54674
56389
|
);
|
|
54675
56390
|
}
|
|
54676
56391
|
async function runCodexTaskSuggestion2(projectPath, previousResponse, retryPolicy) {
|
|
54677
|
-
const tempRoot = mkdtempSync6(
|
|
54678
|
-
const schemaPath =
|
|
54679
|
-
const outputPath =
|
|
56392
|
+
const tempRoot = mkdtempSync6(join26(tmpdir5(), "linzumi-signup-codex-tasks-"));
|
|
56393
|
+
const schemaPath = join26(tempRoot, "task-suggestions.schema.json");
|
|
56394
|
+
const outputPath = join26(tempRoot, "task-suggestions.json");
|
|
54680
56395
|
const model = process.env.LINZUMI_SIGNUP_TASK_MODEL ?? "gpt-5.4-mini";
|
|
54681
56396
|
const prompt = taskSuggestionPrompt2(previousResponse);
|
|
54682
56397
|
const codexCommand = await resolveSignupCodexCommand();
|
|
@@ -54701,7 +56416,7 @@ async function runCodexTaskSuggestion2(projectPath, previousResponse, retryPolic
|
|
|
54701
56416
|
),
|
|
54702
56417
|
retryPolicy
|
|
54703
56418
|
);
|
|
54704
|
-
return
|
|
56419
|
+
return readFileSync23(outputPath, "utf8");
|
|
54705
56420
|
} finally {
|
|
54706
56421
|
rmSync7(tempRoot, { recursive: true, force: true });
|
|
54707
56422
|
}
|
|
@@ -54738,7 +56453,7 @@ function codexTaskSuggestionProcess2(args) {
|
|
|
54738
56453
|
function signupCodexTaskSuggestionProcessForTest(args) {
|
|
54739
56454
|
return codexTaskSuggestionProcess2(args);
|
|
54740
56455
|
}
|
|
54741
|
-
async function resolveSignupCodexCommand(env = process.env, homeDir =
|
|
56456
|
+
async function resolveSignupCodexCommand(env = process.env, homeDir = homedir18(), executableExists = fileIsExecutable) {
|
|
54742
56457
|
const override = firstConfiguredValue([
|
|
54743
56458
|
env.LINZUMI_SIGNUP_CODEX_BIN,
|
|
54744
56459
|
env.LINZUMI_CODEX_BIN
|
|
@@ -54793,7 +56508,7 @@ function resolveHomePath(path2, homeDir) {
|
|
|
54793
56508
|
return homeDir;
|
|
54794
56509
|
}
|
|
54795
56510
|
if (path2.startsWith("~/")) {
|
|
54796
|
-
return
|
|
56511
|
+
return join26(homeDir, path2.slice(2));
|
|
54797
56512
|
}
|
|
54798
56513
|
return resolve10(path2);
|
|
54799
56514
|
}
|
|
@@ -54806,9 +56521,9 @@ function commandLooksPathLike(command) {
|
|
|
54806
56521
|
}
|
|
54807
56522
|
function homeManagedCodexCandidates(homeDir) {
|
|
54808
56523
|
return [
|
|
54809
|
-
|
|
54810
|
-
|
|
54811
|
-
|
|
56524
|
+
join26(homeDir, ".volta", "bin", "codex"),
|
|
56525
|
+
join26(homeDir, ".local", "bin", "codex"),
|
|
56526
|
+
join26(homeDir, "bin", "codex")
|
|
54812
56527
|
];
|
|
54813
56528
|
}
|
|
54814
56529
|
async function firstExecutablePath(paths, executableExists) {
|
|
@@ -54823,7 +56538,7 @@ function commandPathCandidates(path2) {
|
|
|
54823
56538
|
if (path2 === void 0 || path2.trim() === "") {
|
|
54824
56539
|
return [];
|
|
54825
56540
|
}
|
|
54826
|
-
return path2.split(delimiter3).filter((entry) => entry.trim() !== "").map((entry) =>
|
|
56541
|
+
return path2.split(delimiter3).filter((entry) => entry.trim() !== "").map((entry) => join26(entry, "codex"));
|
|
54827
56542
|
}
|
|
54828
56543
|
async function fileIsExecutable(path2) {
|
|
54829
56544
|
try {
|
|
@@ -55056,11 +56771,11 @@ function codexPreflightLocation(command, homeDir) {
|
|
|
55056
56771
|
switch (command) {
|
|
55057
56772
|
case "codex":
|
|
55058
56773
|
return "on PATH";
|
|
55059
|
-
case
|
|
56774
|
+
case join26(homeDir, ".volta", "bin", "codex"):
|
|
55060
56775
|
return "via Volta";
|
|
55061
|
-
case
|
|
56776
|
+
case join26(homeDir, ".local", "bin", "codex"):
|
|
55062
56777
|
return "via ~/.local/bin";
|
|
55063
|
-
case
|
|
56778
|
+
case join26(homeDir, "bin", "codex"):
|
|
55064
56779
|
return "via ~/bin";
|
|
55065
56780
|
default:
|
|
55066
56781
|
return "from configured path";
|
|
@@ -55219,7 +56934,7 @@ function probeToolWithArgs(command, args, cwd) {
|
|
|
55219
56934
|
}
|
|
55220
56935
|
async function discoverCodeRoots(homeDir) {
|
|
55221
56936
|
const candidates = ["src", "code", "projects"].map(
|
|
55222
|
-
(name) =>
|
|
56937
|
+
(name) => join26(homeDir, name)
|
|
55223
56938
|
);
|
|
55224
56939
|
return candidates.filter((path2) => existsSync17(path2)).flatMap((path2) => discoveredProjectNames(path2));
|
|
55225
56940
|
}
|
|
@@ -55273,7 +56988,7 @@ function discoverProjectsFromCurrentDirectory(cwd) {
|
|
|
55273
56988
|
}
|
|
55274
56989
|
function discoverProjectsFromGuessedRoots(homeDir) {
|
|
55275
56990
|
const guessedRoots = ["src", "code", "projects"].map(
|
|
55276
|
-
(name) =>
|
|
56991
|
+
(name) => join26(homeDir, name)
|
|
55277
56992
|
);
|
|
55278
56993
|
const projects = guessedRoots.flatMap(
|
|
55279
56994
|
(root) => discoverProjectsUnderRoot(root)
|
|
@@ -55325,25 +57040,25 @@ function looksLikeProject(path2) {
|
|
|
55325
57040
|
"pnpm-lock.yaml",
|
|
55326
57041
|
"yarn.lock",
|
|
55327
57042
|
"package-lock.json"
|
|
55328
|
-
].some((name) => existsSync17(
|
|
57043
|
+
].some((name) => existsSync17(join26(path2, name)));
|
|
55329
57044
|
}
|
|
55330
57045
|
function detectProjectLanguage(path2) {
|
|
55331
|
-
if (existsSync17(
|
|
57046
|
+
if (existsSync17(join26(path2, "pyproject.toml")) || existsSync17(join26(path2, "requirements.txt"))) {
|
|
55332
57047
|
return "Python";
|
|
55333
57048
|
}
|
|
55334
|
-
if (existsSync17(
|
|
57049
|
+
if (existsSync17(join26(path2, "Cargo.toml"))) {
|
|
55335
57050
|
return "Rust";
|
|
55336
57051
|
}
|
|
55337
|
-
if (existsSync17(
|
|
57052
|
+
if (existsSync17(join26(path2, "go.mod"))) {
|
|
55338
57053
|
return "Go";
|
|
55339
57054
|
}
|
|
55340
|
-
if (existsSync17(
|
|
57055
|
+
if (existsSync17(join26(path2, "mix.exs"))) {
|
|
55341
57056
|
return "Elixir";
|
|
55342
57057
|
}
|
|
55343
|
-
if (existsSync17(
|
|
57058
|
+
if (existsSync17(join26(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
|
|
55344
57059
|
return "TypeScript";
|
|
55345
57060
|
}
|
|
55346
|
-
if (existsSync17(
|
|
57061
|
+
if (existsSync17(join26(path2, "package.json"))) {
|
|
55347
57062
|
return "JavaScript";
|
|
55348
57063
|
}
|
|
55349
57064
|
return "Project";
|
|
@@ -55351,7 +57066,7 @@ function detectProjectLanguage(path2) {
|
|
|
55351
57066
|
function packageJsonMentionsTypeScript(path2) {
|
|
55352
57067
|
try {
|
|
55353
57068
|
const packageJson2 = JSON.parse(
|
|
55354
|
-
|
|
57069
|
+
readFileSync23(join26(path2, "package.json"), "utf8")
|
|
55355
57070
|
);
|
|
55356
57071
|
return packageJson2.dependencies?.typescript !== void 0 || packageJson2.devDependencies?.typescript !== void 0;
|
|
55357
57072
|
} catch {
|
|
@@ -55359,11 +57074,11 @@ function packageJsonMentionsTypeScript(path2) {
|
|
|
55359
57074
|
}
|
|
55360
57075
|
}
|
|
55361
57076
|
function hasGitMetadata(path2) {
|
|
55362
|
-
return existsSync17(
|
|
57077
|
+
return existsSync17(join26(path2, ".git"));
|
|
55363
57078
|
}
|
|
55364
57079
|
function childDirectories(root) {
|
|
55365
57080
|
try {
|
|
55366
|
-
return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) =>
|
|
57081
|
+
return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join26(root, entry.name));
|
|
55367
57082
|
} catch {
|
|
55368
57083
|
return [];
|
|
55369
57084
|
}
|
|
@@ -55389,10 +57104,10 @@ function directoryExists(path2) {
|
|
|
55389
57104
|
}
|
|
55390
57105
|
function expandHomePath(path2) {
|
|
55391
57106
|
if (path2 === "~") {
|
|
55392
|
-
return
|
|
57107
|
+
return homedir18();
|
|
55393
57108
|
}
|
|
55394
57109
|
if (path2.startsWith("~/")) {
|
|
55395
|
-
return
|
|
57110
|
+
return join26(homedir18(), path2.slice(2));
|
|
55396
57111
|
}
|
|
55397
57112
|
return resolve10(path2);
|
|
55398
57113
|
}
|
|
@@ -55483,8 +57198,8 @@ init_runner();
|
|
|
55483
57198
|
init_runnerLockTakeover();
|
|
55484
57199
|
init_claudeCodeSession();
|
|
55485
57200
|
init_authCache();
|
|
55486
|
-
import { existsSync as existsSync18, readFileSync as
|
|
55487
|
-
import { homedir as
|
|
57201
|
+
import { existsSync as existsSync18, readFileSync as readFileSync24, realpathSync as realpathSync7 } from "node:fs";
|
|
57202
|
+
import { homedir as homedir19 } from "node:os";
|
|
55488
57203
|
import { resolve as resolve11 } from "node:path";
|
|
55489
57204
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
55490
57205
|
|
|
@@ -55579,9 +57294,9 @@ init_kandanTls();
|
|
|
55579
57294
|
init_protocol();
|
|
55580
57295
|
init_json();
|
|
55581
57296
|
init_defaultUrls();
|
|
55582
|
-
import { existsSync as existsSync15, mkdirSync as
|
|
55583
|
-
import { dirname as
|
|
55584
|
-
import { homedir as
|
|
57297
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync20, writeFileSync as writeFileSync13 } from "node:fs";
|
|
57298
|
+
import { dirname as dirname16, join as join23 } from "node:path";
|
|
57299
|
+
import { homedir as homedir16 } from "node:os";
|
|
55585
57300
|
async function runAgentCliCommand(args, deps = {
|
|
55586
57301
|
fetchImpl: fetch,
|
|
55587
57302
|
stdout: process.stdout,
|
|
@@ -56289,7 +58004,7 @@ function agentTokenFile(flags) {
|
|
|
56289
58004
|
return flags.get("agent-token-file") ?? defaultAgentTokenFilePath();
|
|
56290
58005
|
}
|
|
56291
58006
|
function defaultAgentTokenFilePath() {
|
|
56292
|
-
return
|
|
58007
|
+
return join23(homedir16(), ".linzumi", "agent-token.json");
|
|
56293
58008
|
}
|
|
56294
58009
|
function normalizedApiUrl(apiUrl) {
|
|
56295
58010
|
return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
|
|
@@ -56298,10 +58013,10 @@ function authorizationHeaders(token) {
|
|
|
56298
58013
|
return { authorization: `Bearer ${token}` };
|
|
56299
58014
|
}
|
|
56300
58015
|
function readOptionalTextFile(path2) {
|
|
56301
|
-
return existsSync15(path2) ?
|
|
58016
|
+
return existsSync15(path2) ? readFileSync20(path2, "utf8") : void 0;
|
|
56302
58017
|
}
|
|
56303
58018
|
function writeTextFile(path2, content) {
|
|
56304
|
-
|
|
58019
|
+
mkdirSync15(dirname16(path2), { recursive: true });
|
|
56305
58020
|
writeFileSync13(path2, content);
|
|
56306
58021
|
}
|
|
56307
58022
|
function readStoredAgentTokenFile(path2, readTextFile = readOptionalTextFile) {
|
|
@@ -56390,26 +58105,26 @@ init_helloLinzumiProject();
|
|
|
56390
58105
|
init_runnerLogger();
|
|
56391
58106
|
import {
|
|
56392
58107
|
existsSync as existsSync16,
|
|
56393
|
-
closeSync as
|
|
56394
|
-
mkdirSync as
|
|
56395
|
-
openSync as
|
|
56396
|
-
readFileSync as
|
|
58108
|
+
closeSync as closeSync5,
|
|
58109
|
+
mkdirSync as mkdirSync16,
|
|
58110
|
+
openSync as openSync6,
|
|
58111
|
+
readFileSync as readFileSync21,
|
|
56397
58112
|
watch,
|
|
56398
58113
|
writeFileSync as writeFileSync14
|
|
56399
58114
|
} from "node:fs";
|
|
56400
|
-
import { homedir as
|
|
56401
|
-
import { dirname as
|
|
58115
|
+
import { homedir as homedir17 } from "node:os";
|
|
58116
|
+
import { dirname as dirname17, join as join24, resolve as resolve9 } from "node:path";
|
|
56402
58117
|
import { execFileSync, spawn as spawn10 } from "node:child_process";
|
|
56403
58118
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
56404
58119
|
var connectedMarkers = ["Connected to Linzumi", "Runner connected:"];
|
|
56405
58120
|
function commanderStatusDir() {
|
|
56406
|
-
return
|
|
58121
|
+
return join24(homedir17(), ".linzumi", "commanders");
|
|
56407
58122
|
}
|
|
56408
58123
|
function commanderStatusFile(runnerId, statusDir = commanderStatusDir()) {
|
|
56409
|
-
return
|
|
58124
|
+
return join24(statusDir, `${safeRunnerId(runnerId)}.json`);
|
|
56410
58125
|
}
|
|
56411
58126
|
function defaultCommanderLogFile(runnerId) {
|
|
56412
|
-
return
|
|
58127
|
+
return join24(homedir17(), ".linzumi", "logs", `${safeRunnerId(runnerId)}.log`);
|
|
56413
58128
|
}
|
|
56414
58129
|
function commanderLogIsConnected(log2) {
|
|
56415
58130
|
return connectedMarkers.some((marker) => log2.includes(marker));
|
|
@@ -56430,10 +58145,10 @@ function startCommanderDaemon(options) {
|
|
|
56430
58145
|
"--log-file",
|
|
56431
58146
|
logFile
|
|
56432
58147
|
];
|
|
56433
|
-
|
|
56434
|
-
|
|
56435
|
-
const out =
|
|
56436
|
-
const err =
|
|
58148
|
+
mkdirSync16(statusDir, { recursive: true });
|
|
58149
|
+
mkdirSync16(dirname17(logFile), { recursive: true });
|
|
58150
|
+
const out = openSync6(logFile, "a");
|
|
58151
|
+
const err = openSync6(logFile, "a");
|
|
56437
58152
|
writeCliAuditEvent(
|
|
56438
58153
|
"process.spawn",
|
|
56439
58154
|
{
|
|
@@ -56460,8 +58175,8 @@ function startCommanderDaemon(options) {
|
|
|
56460
58175
|
},
|
|
56461
58176
|
{ sessionId: options.runnerId }
|
|
56462
58177
|
);
|
|
56463
|
-
|
|
56464
|
-
|
|
58178
|
+
closeSync5(out);
|
|
58179
|
+
closeSync5(err);
|
|
56465
58180
|
child.unref();
|
|
56466
58181
|
if (child.pid === void 0) {
|
|
56467
58182
|
throw new Error("commander daemon did not report a pid");
|
|
@@ -56486,12 +58201,12 @@ function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), proce
|
|
|
56486
58201
|
if (!existsSync16(statusFile)) {
|
|
56487
58202
|
return { status: "missing", runnerId, statusFile };
|
|
56488
58203
|
}
|
|
56489
|
-
const record = parseRecord2(
|
|
58204
|
+
const record = parseRecord2(readFileSync21(statusFile, "utf8"));
|
|
56490
58205
|
return processIsRunning(record.pid) && processMatchesRecord(record, processIdentityReader) ? { status: "running", record } : { status: "stopped", record };
|
|
56491
58206
|
}
|
|
56492
58207
|
async function waitForCommanderDaemon(options) {
|
|
56493
58208
|
const now = options.now ?? (() => Date.now());
|
|
56494
|
-
const readTextFile = options.readTextFile ?? ((path2) => existsSync16(path2) ?
|
|
58209
|
+
const readTextFile = options.readTextFile ?? ((path2) => existsSync16(path2) ? readFileSync21(path2, "utf8") : void 0);
|
|
56495
58210
|
const statusImpl = options.statusImpl ?? commanderDaemonStatus;
|
|
56496
58211
|
const deadline = now() + options.timeoutMs;
|
|
56497
58212
|
while (now() <= deadline) {
|
|
@@ -69287,22 +71002,49 @@ async function resolveMcpAuth(args) {
|
|
|
69287
71002
|
mode: "personal-agent-delegation"
|
|
69288
71003
|
};
|
|
69289
71004
|
}
|
|
69290
|
-
const
|
|
71005
|
+
const accessToken = await resolveMcpLocalRunnerToken({
|
|
71006
|
+
kandanUrl: args.kandanUrl,
|
|
71007
|
+
explicitToken: args.explicitToken,
|
|
71008
|
+
authFilePath: args.authFilePath,
|
|
71009
|
+
workspaceSlug: args.workspaceSlug,
|
|
71010
|
+
channelSlug: args.channelSlug
|
|
71011
|
+
});
|
|
71012
|
+
return { accessToken, mode: "local-runner" };
|
|
71013
|
+
}
|
|
71014
|
+
async function resolveMcpLocalRunnerToken(args, deps = {
|
|
71015
|
+
readCachedToken: readCachedLocalRunnerToken,
|
|
71016
|
+
validateTokenOutcome: validateLocalRunnerTokenOutcome,
|
|
71017
|
+
resolveToken: resolveLocalRunnerToken
|
|
71018
|
+
}) {
|
|
71019
|
+
const token = args.explicitToken ?? deps.readCachedToken(args.kandanUrl, args.authFilePath)?.accessToken;
|
|
69291
71020
|
if (token === void 0) {
|
|
69292
71021
|
throw new Error(
|
|
69293
71022
|
"missing Linzumi MCP token; run linzumi auth or pass --token"
|
|
69294
71023
|
);
|
|
69295
71024
|
}
|
|
69296
|
-
const
|
|
71025
|
+
const outcome = await deps.validateTokenOutcome({
|
|
69297
71026
|
kandanUrl: args.kandanUrl,
|
|
69298
71027
|
accessToken: token,
|
|
69299
71028
|
workspaceSlug: args.workspaceSlug,
|
|
69300
71029
|
channelSlug: args.channelSlug
|
|
69301
71030
|
});
|
|
69302
|
-
if (
|
|
69303
|
-
|
|
71031
|
+
if (outcome === "usable") {
|
|
71032
|
+
return token;
|
|
71033
|
+
}
|
|
71034
|
+
const _recoverable = outcome;
|
|
71035
|
+
void _recoverable;
|
|
71036
|
+
try {
|
|
71037
|
+
return await deps.resolveToken({
|
|
71038
|
+
kandanUrl: args.kandanUrl,
|
|
71039
|
+
workspaceSlug: args.workspaceSlug,
|
|
71040
|
+
channelSlug: args.channelSlug,
|
|
71041
|
+
authFilePath: args.authFilePath
|
|
71042
|
+
});
|
|
71043
|
+
} catch (error) {
|
|
71044
|
+
throw new Error(
|
|
71045
|
+
`Linzumi MCP token was rejected by the server and re-mint failed: ${error instanceof Error ? error.message : String(error)}`
|
|
71046
|
+
);
|
|
69304
71047
|
}
|
|
69305
|
-
return { accessToken: token, mode: "local-runner" };
|
|
69306
71048
|
}
|
|
69307
71049
|
function mcpJsonResult(value) {
|
|
69308
71050
|
return {
|
|
@@ -69384,7 +71126,7 @@ init_userFacingErrors();
|
|
|
69384
71126
|
init_version();
|
|
69385
71127
|
import { chmodSync as chmodSync3, mkdtempSync as mkdtempSync5, rmSync as rmSync6 } from "node:fs";
|
|
69386
71128
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
69387
|
-
import { basename as basename9, dirname as
|
|
71129
|
+
import { basename as basename9, dirname as dirname18, join as join25 } from "node:path";
|
|
69388
71130
|
var configEnvKey = "LINZUMI_REMOTE_CODEX_HARNESS_CONFIG_B64";
|
|
69389
71131
|
var remoteHarnessEnvironmentId = "linzumi-remote-codex-harness";
|
|
69390
71132
|
async function runRemoteCodexHarnessWorkerFromEnv(env = process.env) {
|
|
@@ -69617,18 +71359,18 @@ function resolveLinzumiCliEntrypoint(entrypoint) {
|
|
|
69617
71359
|
case "remote-codex-harness-worker.js":
|
|
69618
71360
|
case "remote-codex-harness-worker.mjs":
|
|
69619
71361
|
case "remote-codex-harness-worker.cjs":
|
|
69620
|
-
return
|
|
71362
|
+
return join25(dirname18(entrypoint), "linzumi.js");
|
|
69621
71363
|
case "remoteCodexHarnessWorkerEntrypoint.ts":
|
|
69622
71364
|
case "remoteCodexHarnessWorkerEntrypoint.js":
|
|
69623
|
-
return
|
|
71365
|
+
return join25(dirname18(entrypoint), "index.ts");
|
|
69624
71366
|
default:
|
|
69625
71367
|
return entrypoint;
|
|
69626
71368
|
}
|
|
69627
71369
|
}
|
|
69628
71370
|
function writeEphemeralMcpAuthFile2(options) {
|
|
69629
|
-
const directory = mkdtempSync5(
|
|
71371
|
+
const directory = mkdtempSync5(join25(tmpdir4(), "linzumi-mcp-auth-"));
|
|
69630
71372
|
chmodSync3(directory, 448);
|
|
69631
|
-
const path2 =
|
|
71373
|
+
const path2 = join25(directory, "auth.json");
|
|
69632
71374
|
try {
|
|
69633
71375
|
writeCachedLocalRunnerToken({
|
|
69634
71376
|
kandanUrl: options.kandanUrl,
|
|
@@ -70579,7 +72321,7 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
70579
72321
|
};
|
|
70580
72322
|
}
|
|
70581
72323
|
function readAgentTokenTextFile(path2) {
|
|
70582
|
-
return existsSync18(path2) ?
|
|
72324
|
+
return existsSync18(path2) ? readFileSync24(path2, "utf8") : void 0;
|
|
70583
72325
|
}
|
|
70584
72326
|
function rejectAgentRunnerTargetingFlags(values) {
|
|
70585
72327
|
const unsupportedFlags = [
|
|
@@ -70898,10 +72640,10 @@ function rejectStartTargetingFlags(values) {
|
|
|
70898
72640
|
}
|
|
70899
72641
|
function resolveUserPath(pathValue) {
|
|
70900
72642
|
if (pathValue === "~") {
|
|
70901
|
-
return
|
|
72643
|
+
return homedir19();
|
|
70902
72644
|
}
|
|
70903
72645
|
if (pathValue.startsWith("~/")) {
|
|
70904
|
-
return resolve11(
|
|
72646
|
+
return resolve11(homedir19(), pathValue.slice(2));
|
|
70905
72647
|
}
|
|
70906
72648
|
return resolve11(pathValue);
|
|
70907
72649
|
}
|