@linzumi/cli 0.0.107-beta → 0.0.108-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1802 -168
  3. 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.107-beta";
20081
+ linzumiCliVersion = "0.0.108-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 createHash6, randomUUID as randomUUID4 } from "node:crypto";
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 mkdirSync13,
24543
+ mkdirSync as mkdirSync14,
23597
24544
  mkdtempSync as mkdtempSync4,
24545
+ openSync as openSync5,
23598
24546
  readdirSync as readdirSync4,
23599
- readFileSync as readFileSync17,
24547
+ readFileSync as readFileSync18,
23600
24548
  realpathSync as realpathSync6,
23601
- renameSync as renameSync4,
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 homedir14, hostname as hostname2, tmpdir as tmpdir3 } from "node:os";
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 dirname14,
24560
+ dirname as dirname15,
23613
24561
  extname as extname2,
23614
24562
  isAbsolute as isAbsolute5,
23615
- join as join21,
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
- cleanup.removeHandlers = installCleanupHandlers(close);
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(options, log2, cleanup, close);
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
- async function openLocalCodexRunner(options, log2, cleanup, close) {
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,123 @@ 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 durableInboundQueueFor = (channel, kandanThreadId) => {
25068
+ const workspace = inboundQueueWorkspace();
25069
+ const cacheKey = `${workspace}:${channel}:${kandanThreadId}`;
25070
+ const existing = durableInboundQueues.get(cacheKey);
25071
+ if (existing !== void 0) {
25072
+ return existing;
25073
+ }
25074
+ const queue = createDurableInboundThreadQueue(
25075
+ durableInboundQueuePath(workspace, channel, kandanThreadId),
25076
+ log2
25077
+ );
25078
+ durableInboundQueues.set(cacheKey, queue);
25079
+ return queue;
25080
+ };
25081
+ const capturedLiveFollowUpEvents = /* @__PURE__ */ new Map();
25082
+ const capturedFollowUpKey = (channel, threadId, seq2) => `${inboundQueueWorkspace()}:${channel}:${threadId}:${seq2}`;
25083
+ const captureLiveFollowUpEvent = (channel, threadId, event) => {
25084
+ capturedLiveFollowUpEvents.set(
25085
+ capturedFollowUpKey(channel, threadId, event.seq),
25086
+ event
25087
+ );
25088
+ };
25089
+ const reEnqueueUndeliveredFollowUps = (args) => {
25090
+ const queue = durableInboundQueueFor(args.channel, args.threadId);
25091
+ for (const seq2 of args.sourceSeqs) {
25092
+ const key = capturedFollowUpKey(args.channel, args.threadId, seq2);
25093
+ const event = capturedLiveFollowUpEvents.get(key);
25094
+ if (event === void 0) {
25095
+ log2("runner.live_follow_up_recapture_missing", {
25096
+ kandanThreadId: args.threadId,
25097
+ channel: args.channel,
25098
+ sourceSeq: seq2
25099
+ });
25100
+ continue;
25101
+ }
25102
+ const enqueued = queue.enqueue(
25103
+ seq2,
25104
+ "replay",
25105
+ serializeKandanChatEventForQueue(event),
25106
+ Date.now()
25107
+ );
25108
+ log2("runner.live_follow_up_re_enqueued_on_close", {
25109
+ kandanThreadId: args.threadId,
25110
+ channel: args.channel,
25111
+ sourceSeq: seq2,
25112
+ enqueued: enqueued.accepted
25113
+ });
25114
+ capturedLiveFollowUpEvents.delete(key);
25115
+ }
25116
+ };
25117
+ runnerDrainHooks.stopAccepting = () => {
25118
+ acceptingInbound = false;
25119
+ };
25120
+ runnerDrainHooks.flushInboundQueues = () => {
25121
+ for (const queue of durableInboundQueues.values()) {
25122
+ queue.compact();
25123
+ }
25124
+ };
23950
25125
  const controlEpochStore = createRunnerControlEpochStore(
23951
25126
  controlCursorStore,
23952
25127
  appliedStartTurnStore,
@@ -24788,6 +25963,8 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
24788
25963
  codex,
24789
25964
  topic,
24790
25965
  instanceId,
25966
+ // SEAM 2: source the held lease epoch for the stream-write fence echo.
25967
+ leaseEpochFor: (threadId) => leaseEpochRegistry.current(threadId),
24791
25968
  options: {
24792
25969
  kandanUrl: options.kandanUrl,
24793
25970
  token: options.token,
@@ -24824,6 +26001,10 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
24824
26001
  // session is attached (the durable thread's worker fully died). Late-
24825
26002
  // bound to dodge the const's temporal dead zone (defined below).
24826
26003
  onUnroutedThreadMessage: (event) => handleUnroutedThreadMessage(event),
26004
+ // BLOCKER B: codex live dispatch-confirm watermark commit on the main
26005
+ // channel session too, off the same persisted authority the keystone
26006
+ // gates on.
26007
+ onLiveSourceSeqAccepted: commitLiveWorkerAcceptedSourceSeq,
24827
26008
  channelSession: options.channelSession
24828
26009
  },
24829
26010
  log: log2
@@ -25174,6 +26355,8 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25174
26355
  codex: sessionCodex,
25175
26356
  topic,
25176
26357
  instanceId,
26358
+ // SEAM 2: dynamic per-thread session also echoes the held lease epoch.
26359
+ leaseEpochFor: (threadId) => leaseEpochRegistry.current(threadId),
25177
26360
  options: {
25178
26361
  kandanUrl: options.kandanUrl,
25179
26362
  token: options.token,
@@ -25188,6 +26371,11 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25188
26371
  // it out of the const's temporal dead zone while still resolving the
25189
26372
  // live handler when an unrouted chat event actually arrives.
25190
26373
  onUnroutedThreadMessage: (event) => handleUnroutedThreadMessage(event),
26374
+ // BLOCKER B: codex live dispatch-confirm watermark commit (the
26375
+ // production-default double-exec fix). Fires from drainKandanMessageQueue
26376
+ // when turn/start replies, off the SAME persisted authority the keystone
26377
+ // gates on, so a codex worker-death + replay is gated 'ignored'.
26378
+ onLiveSourceSeqAccepted: commitLiveWorkerAcceptedSourceSeq,
25191
26379
  pipelinePersistDir: commanderOutboxPersistDir(),
25192
26380
  enablePortForwardWatch: true,
25193
26381
  initialForwardPorts: allowedForwardPorts,
@@ -25566,7 +26754,12 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25566
26754
  control,
25567
26755
  log2,
25568
26756
  onStartedThread,
25569
- void 0
26757
+ void 0,
26758
+ (id) => leaseEpochRegistry.current(id),
26759
+ commitLiveWorkerAcceptedSourceSeq,
26760
+ captureLiveFollowUpEvent,
26761
+ reEnqueueUndeliveredFollowUps,
26762
+ liveSourceSeqAlreadyApplied
25570
26763
  )
25571
26764
  );
25572
26765
  }
@@ -25609,6 +26802,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25609
26802
  attachments: event.attachments,
25610
26803
  boundStartTimeout: true
25611
26804
  });
26805
+ return "dispatched";
25612
26806
  } catch (error) {
25613
26807
  log2("runner.spawn_on_miss_failed", {
25614
26808
  kandanThreadId: event.threadId ?? null,
@@ -25622,6 +26816,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25622
26816
  `could not resume the thread's coding session: ${error instanceof Error ? error.message : String(error)}`,
25623
26817
  record.codexThreadId
25624
26818
  );
26819
+ return "transient_failed";
25625
26820
  }
25626
26821
  };
25627
26822
  const handleUnroutedThreadMessage = async (event) => {
@@ -25652,6 +26847,246 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25652
26847
  if (record === void 0) {
25653
26848
  return "ignored";
25654
26849
  }
26850
+ const queueChannel = record.channel;
26851
+ const watermarkKey = threadSourceSeqWatermarkKey(
26852
+ inboundQueueWorkspace(),
26853
+ queueChannel,
26854
+ kandanThreadId
26855
+ );
26856
+ const queue = durableInboundQueueFor(queueChannel, kandanThreadId);
26857
+ if (!acceptingInbound) {
26858
+ const enqueued = queue.enqueue(
26859
+ event.seq,
26860
+ event.localRunnerEventType === void 0 ? "live" : "replay",
26861
+ serializeKandanChatEventForQueue(event),
26862
+ Date.now()
26863
+ );
26864
+ log2("runner.spawn_on_miss_drain_in_progress", {
26865
+ kandanThreadId,
26866
+ seq: event.seq,
26867
+ enqueued: enqueued.accepted
26868
+ });
26869
+ return "spawned";
26870
+ }
26871
+ const outcome = await receiveResumeMessage({
26872
+ queue,
26873
+ store: appliedSourceSeqStore,
26874
+ lock: threadReconcilerLock,
26875
+ leases: leaseEpochRegistry,
26876
+ threadId: kandanThreadId,
26877
+ watermarkKey,
26878
+ sourceSeq: event.seq,
26879
+ kind: event.localRunnerEventType === void 0 ? "live" : "replay",
26880
+ payload: serializeKandanChatEventForQueue(event),
26881
+ leaseEpoch: readLeaseEpoch(event),
26882
+ // Blocker #2: the keystone drains the queue HEAD in source_seq order, so
26883
+ // the dispatch is keyed by (sourceSeq, payload) - it may run a LOWER
26884
+ // pending seq before this arrived one. Reconstruct the event from the
26885
+ // head's payload, exactly as the boot/rejoin drain does.
26886
+ dispatch: async (sourceSeq, payload) => {
26887
+ const drained = deserializeKandanChatEventFromQueue(payload);
26888
+ if (drained === void 0) {
26889
+ log2("runner.spawn_on_miss_bad_payload", {
26890
+ kandanThreadId,
26891
+ seq: sourceSeq
26892
+ });
26893
+ return "permanent_failed";
26894
+ }
26895
+ return await dispatchResumeForOwnedThread(drained, record);
26896
+ },
26897
+ // BLOCKER #4: pass the server's per-thread processed floor (the most-recent
26898
+ // resume_reconcile rows) so the live-miss path reconciles
26899
+ // highestProcessedSourceSeq into the watermark BEFORE dispatching - a
26900
+ // completed-but-not-locally-acked turn that replays is gated 'ignored', not
26901
+ // re-run. Empty until the server SEAM 1 half ships; then the floor is
26902
+ // authoritative. The live-miss path leaves onlyDispatchUnattempted at its
26903
+ // default (false): the arrived message is attempts===0 and the blind
26904
+ // post-completion fence is owned by the boot/rejoin recovery drain, not this
26905
+ // on-receipt path (changing it here would alter the existing live behavior).
26906
+ serverRows: latestResumeReconcileByThread.get(kandanThreadId) ?? [],
26907
+ // Blocker #3: surface a LOUD terminal `failed` state on any dead-letter.
26908
+ onDeadLetter: deadLetterPublisherFor(record),
26909
+ log: log2
26910
+ });
26911
+ return outcome === "ignored" ? "ignored" : "spawned";
26912
+ };
26913
+ const drainOwnedThreadQueue = async (record, serverRows = []) => {
26914
+ if (!acceptingInbound) {
26915
+ return;
26916
+ }
26917
+ const kandanThreadId = record.kandanThreadId;
26918
+ const queueChannel = record.channel;
26919
+ const watermarkKey = threadSourceSeqWatermarkKey(
26920
+ inboundQueueWorkspace(),
26921
+ queueChannel,
26922
+ kandanThreadId
26923
+ );
26924
+ const queue = durableInboundQueueFor(queueChannel, kandanThreadId);
26925
+ await drainResumeQueue({
26926
+ queue,
26927
+ store: appliedSourceSeqStore,
26928
+ lock: threadReconcilerLock,
26929
+ threadId: kandanThreadId,
26930
+ watermarkKey,
26931
+ serverRows,
26932
+ log: log2,
26933
+ // Until the SERVER message_state reconcile half (SEAM 1 server) ships, the
26934
+ // drain runs with serverRows=[] in production. In that mode a post-
26935
+ // completion-kill (turn confirmed-dispatched, then SIGKILL before the
26936
+ // watermark flush + ack) leaves an attempted-but-unacked entry. Blindly
26937
+ // re-dispatching it would double-execute the turn (real edits/commits), so
26938
+ // gate the drain to UNATTEMPTED entries only: an attempted entry is dead-
26939
+ // lettered with a loud `failed` state rather than re-run. Once serverRows
26940
+ // is populated the reconcile fences already-run seqs and this gate is moot.
26941
+ onlyDispatchUnattempted: serverRows.length === 0,
26942
+ // Blocker #3: surface a LOUD terminal `failed` state on any dead-letter
26943
+ // (exhausted retries, a withheld post-completion-kill entry, or a permanent
26944
+ // failure) so a persistently-unbindable thread is never silently dropped.
26945
+ onDeadLetter: deadLetterPublisherFor(record),
26946
+ // A torn tail with no server reconcile cannot be re-derived on this branch
26947
+ // (no server outbox/replay). Publish a loud "please resend" terminal state
26948
+ // for the thread instead of silently dropping the lost tail.
26949
+ onTornTailUnrecoverable: async (threadId) => {
26950
+ const control = rehydrationReconnectControl(record);
26951
+ await publishSpawnOnMissMessageState(
26952
+ control,
26953
+ "failed",
26954
+ "a message may have been lost during an unexpected shutdown (power loss). Please resend your last message to continue this job.",
26955
+ record.codexThreadId
26956
+ );
26957
+ log2("runner.inbound_torn_tail_resend_published", {
26958
+ kandanThreadId: threadId
26959
+ });
26960
+ },
26961
+ dispatch: async (sourceSeq, payload) => {
26962
+ const event = deserializeKandanChatEventFromQueue(payload);
26963
+ if (event === void 0) {
26964
+ log2("runner.inbound_drain_bad_payload", {
26965
+ kandanThreadId,
26966
+ seq: sourceSeq
26967
+ });
26968
+ return "permanent_failed";
26969
+ }
26970
+ return await dispatchResumeForOwnedThread(event, record);
26971
+ }
26972
+ });
26973
+ };
26974
+ const consumeResumeReconcile = (reconcileValue) => {
26975
+ const byThread = /* @__PURE__ */ new Map();
26976
+ const entries = parseResumeReconcileEntries(reconcileValue);
26977
+ for (const entry of entries) {
26978
+ if (entry.leaseEpoch !== void 0) {
26979
+ const adopted = leaseEpochRegistry.adopt(entry.threadId, entry.leaseEpoch);
26980
+ if (adopted) {
26981
+ log2("runner.resume_reconcile_lease_adopted", {
26982
+ kandanThreadId: entry.threadId,
26983
+ leaseEpoch: entry.leaseEpoch
26984
+ });
26985
+ }
26986
+ }
26987
+ const rows = resumeReconcileServerRows(entry);
26988
+ byThread.set(entry.threadId, rows);
26989
+ latestResumeReconcileByThread.set(entry.threadId, rows);
26990
+ }
26991
+ if (entries.length > 0) {
26992
+ log2("runner.resume_reconcile_consumed", { threadCount: entries.length });
26993
+ }
26994
+ return byThread;
26995
+ };
26996
+ const drainOrphanedThreadQueues = async () => {
26997
+ const dir = durableInboundQueueDir();
26998
+ let files;
26999
+ try {
27000
+ files = readdirSync4(dir).filter((name) => name.endsWith(".log"));
27001
+ } catch (error) {
27002
+ if (error?.code === "ENOENT") {
27003
+ return;
27004
+ }
27005
+ log2("runner.inbound_orphan_scan_failed", {
27006
+ message: error instanceof Error ? error.message : String(error)
27007
+ });
27008
+ return;
27009
+ }
27010
+ const workspace = inboundQueueWorkspace();
27011
+ const owned = new Set(
27012
+ threadSessionRegistry.list().map(
27013
+ (record) => durableInboundQueuePath(
27014
+ workspace,
27015
+ record.channel,
27016
+ record.kandanThreadId
27017
+ )
27018
+ )
27019
+ );
27020
+ for (const name of files) {
27021
+ const path2 = join22(dir, name);
27022
+ if (owned.has(path2)) {
27023
+ continue;
27024
+ }
27025
+ try {
27026
+ const queue = createDurableInboundThreadQueue(path2, log2);
27027
+ const stranded = queue.entries();
27028
+ if (stranded.length === 0) {
27029
+ continue;
27030
+ }
27031
+ for (const entry of stranded) {
27032
+ queue.deadLetter(entry.sourceSeq);
27033
+ }
27034
+ queue.compact();
27035
+ log2("runner.inbound_orphan_dead_lettered", {
27036
+ path: path2,
27037
+ strandedCount: stranded.length,
27038
+ strandedSeqs: stranded.map((e) => e.sourceSeq)
27039
+ });
27040
+ } catch (error) {
27041
+ log2("runner.inbound_orphan_drain_failed", {
27042
+ path: path2,
27043
+ message: error instanceof Error ? error.message : String(error)
27044
+ });
27045
+ }
27046
+ }
27047
+ };
27048
+ const drainAllOwnedThreadQueues = async (reconcileByThread) => {
27049
+ if (!acceptingInbound) {
27050
+ return;
27051
+ }
27052
+ const records = threadSessionRegistry.list();
27053
+ await Promise.all(
27054
+ records.map(
27055
+ (record) => drainOwnedThreadQueue(
27056
+ record,
27057
+ reconcileByThread?.get(record.kandanThreadId) ?? []
27058
+ ).catch((error) => {
27059
+ log2("runner.inbound_drain_failed", {
27060
+ kandanThreadId: record.kandanThreadId,
27061
+ message: error instanceof Error ? error.message : String(error)
27062
+ });
27063
+ })
27064
+ )
27065
+ );
27066
+ await drainOrphanedThreadQueues();
27067
+ };
27068
+ const bootReconcile = consumeResumeReconcile(
27069
+ objectValue(joinResponse)?.resume_reconcile
27070
+ );
27071
+ const bootDrain = threadSessionRehydration.then(() => drainAllOwnedThreadQueues(bootReconcile)).catch((error) => {
27072
+ log2("runner.inbound_boot_drain_failed", {
27073
+ message: error instanceof Error ? error.message : String(error)
27074
+ });
27075
+ });
27076
+ cleanup.actions.push(() => bootDrain);
27077
+ kandan.onReconnect((rejoinReplies) => {
27078
+ const rejoinReply = rejoinReplies.get(topic);
27079
+ const reconcile = consumeResumeReconcile(
27080
+ rejoinReply === void 0 ? void 0 : rejoinReply.resume_reconcile
27081
+ );
27082
+ void drainAllOwnedThreadQueues(reconcile).catch((error) => {
27083
+ log2("runner.inbound_rejoin_drain_failed", {
27084
+ message: error instanceof Error ? error.message : String(error)
27085
+ });
27086
+ });
27087
+ });
27088
+ const dispatchResumeForOwnedThread = async (event, record) => {
27089
+ const kandanThreadId = record.kandanThreadId;
25655
27090
  const baseControl = rehydrationReconnectControl(record);
25656
27091
  const providerBinding = threadModelProviderBindings.get(kandanThreadId);
25657
27092
  const reconnectControl = {
@@ -25681,7 +27116,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25681
27116
  `could not resume the thread's coding session: working directory ${record.cwd} is not an allowed root (${cwd.reason})`,
25682
27117
  record.codexThreadId
25683
27118
  );
25684
- return "spawned";
27119
+ return "permanent_failed";
25685
27120
  }
25686
27121
  const resolvedCwd = cwd.cwd;
25687
27122
  const inFlightSpawn = spawnOnMissInFlight.get(kandanThreadId);
@@ -25701,14 +27136,13 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25701
27136
  `could not resume the thread's coding session: no live session after spawn`,
25702
27137
  record.codexThreadId
25703
27138
  );
25704
- return "spawned";
27139
+ return "transient_failed";
25705
27140
  }
25706
- await replaySpawnOnMissMessageOnLiveSession(
27141
+ return await replaySpawnOnMissMessageOnLiveSession(
25707
27142
  event,
25708
27143
  record,
25709
27144
  reconnectControl
25710
27145
  );
25711
- return "spawned";
25712
27146
  }
25713
27147
  log2("runner.spawn_on_miss_resuming", {
25714
27148
  kandanThreadId,
@@ -25729,12 +27163,11 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25729
27163
  );
25730
27164
  if (threadStartDelegatedToSibling(startResult)) {
25731
27165
  spawnSettle("spawned");
25732
- await replaySpawnOnMissMessageOnLiveSession(
27166
+ return await replaySpawnOnMissMessageOnLiveSession(
25733
27167
  event,
25734
27168
  record,
25735
27169
  reconnectControl
25736
27170
  );
25737
- return "spawned";
25738
27171
  }
25739
27172
  if (startResult === void 0 || startResult.ok === false) {
25740
27173
  spawnSettle("no_session");
@@ -25751,10 +27184,10 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25751
27184
  `could not resume the thread's coding session: ${startError}`,
25752
27185
  record.codexThreadId
25753
27186
  );
25754
- return "spawned";
27187
+ return "transient_failed";
25755
27188
  }
25756
27189
  spawnSettle("spawned");
25757
- return "spawned";
27190
+ return "dispatched";
25758
27191
  } catch (error) {
25759
27192
  spawnSettle("no_session");
25760
27193
  log2("runner.spawn_on_miss_failed", {
@@ -25769,7 +27202,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25769
27202
  `could not resume the thread's coding session: ${error instanceof Error ? error.message : String(error)}`,
25770
27203
  record.codexThreadId
25771
27204
  );
25772
- return "spawned";
27205
+ return "transient_failed";
25773
27206
  } finally {
25774
27207
  spawnSettle("spawned");
25775
27208
  if (spawnOnMissInFlight.get(kandanThreadId) === spawnPromise) {
@@ -25795,6 +27228,26 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25795
27228
  });
25796
27229
  }
25797
27230
  };
27231
+ const deadLetterPublisherFor = (record) => {
27232
+ return async ({ sourceSeq, reason, attempts, alreadyPublished }) => {
27233
+ log2("runner.inbound_dead_lettered", {
27234
+ kandanThreadId: record.kandanThreadId,
27235
+ seq: sourceSeq,
27236
+ reason,
27237
+ attempts,
27238
+ alreadyPublished
27239
+ });
27240
+ if (alreadyPublished) {
27241
+ return;
27242
+ }
27243
+ await publishSpawnOnMissMessageState(
27244
+ rehydrationReconnectControl(record),
27245
+ "failed",
27246
+ deadLetterFailedReason(reason),
27247
+ record.codexThreadId
27248
+ );
27249
+ };
27250
+ };
25798
27251
  const heartbeatPayload = () => ({
25799
27252
  instanceId,
25800
27253
  clientId,
@@ -26285,7 +27738,12 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
26285
27738
  control,
26286
27739
  log2,
26287
27740
  attachThreadSession,
26288
- shouldUseThreadProcesses(options) ? startThreadRunnerProcess : void 0
27741
+ shouldUseThreadProcesses(options) ? startThreadRunnerProcess : void 0,
27742
+ (id) => leaseEpochRegistry.current(id),
27743
+ commitLiveWorkerAcceptedSourceSeq,
27744
+ captureLiveFollowUpEvent,
27745
+ reEnqueueUndeliveredFollowUps,
27746
+ liveSourceSeqAlreadyApplied
26289
27747
  );
26290
27748
  }),
26291
27749
  commitStartTurnWatermark
@@ -26316,7 +27774,10 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
26316
27774
  pendingControls,
26317
27775
  controlDispatcher,
26318
27776
  dispatchControl,
26319
- refreshDependencyStatus
27777
+ async () => {
27778
+ await refreshDependencyStatus();
27779
+ await refreshCodexAuthStatus();
27780
+ }
26320
27781
  );
26321
27782
  return { instanceId, codexUrl, close };
26322
27783
  }
@@ -26330,28 +27791,34 @@ function commanderOutboxPersistDir() {
26330
27791
  if (override !== void 0 && override !== "") {
26331
27792
  return override;
26332
27793
  }
26333
- return join21(homedir14(), ".linzumi", "commander-outbox");
27794
+ return join22(homedir15(), ".linzumi", "commander-outbox");
27795
+ }
27796
+ function controlCursorsDir() {
27797
+ const override = process.env.LINZUMI_CONTROL_CURSOR_DIR?.trim();
27798
+ if (override !== void 0 && override !== "") {
27799
+ return override;
27800
+ }
27801
+ return join22(homedir15(), ".linzumi", "control-cursors");
26334
27802
  }
26335
27803
  function controlCursorStorePath(runnerId) {
26336
27804
  const sanitized = runnerId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
26337
- const digest = createHash6("sha256").update(runnerId).digest("hex").slice(0, 8);
27805
+ const digest = createHash7("sha256").update(runnerId).digest("hex").slice(0, 8);
26338
27806
  const stem = sanitized === "" ? "runner" : sanitized;
26339
- return join21(
26340
- homedir14(),
26341
- ".linzumi",
26342
- "control-cursors",
26343
- `${stem}.${digest}.json`
26344
- );
27807
+ return join22(controlCursorsDir(), `${stem}.${digest}.json`);
26345
27808
  }
26346
27809
  function appliedStartTurnStorePath(runnerId) {
26347
27810
  const sanitized = runnerId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
26348
- const digest = createHash6("sha256").update(runnerId).digest("hex").slice(0, 8);
27811
+ const digest = createHash7("sha256").update(runnerId).digest("hex").slice(0, 8);
27812
+ const stem = sanitized === "" ? "runner" : sanitized;
27813
+ return join22(controlCursorsDir(), `${stem}.${digest}.start-turn.json`);
27814
+ }
27815
+ function appliedSourceSeqStorePath(runnerId) {
27816
+ const sanitized = runnerId.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
27817
+ const digest = createHash7("sha256").update(runnerId).digest("hex").slice(0, 8);
26349
27818
  const stem = sanitized === "" ? "runner" : sanitized;
26350
- return join21(
26351
- homedir14(),
26352
- ".linzumi",
26353
- "control-cursors",
26354
- `${stem}.${digest}.start-turn.json`
27819
+ return join22(
27820
+ controlCursorsDir(),
27821
+ `${stem}.${digest}.source-seq.json`
26355
27822
  );
26356
27823
  }
26357
27824
  function startTurnWatermarkKey(topic, threadId) {
@@ -26473,6 +27940,7 @@ function createConnectionStalenessTracker(log2, now = Date.now, thresholdMs = co
26473
27940
  }
26474
27941
  function createControlCursorFileStore(path2, log2, options) {
26475
27942
  const discardSeqsOnFirstEpochAdoption = options?.discardSeqsOnFirstEpochAdoption === true;
27943
+ const fsyncOnWrite = options?.fsyncOnWrite === true;
26476
27944
  const { cursors, epoch: persistedEpoch } = readControlCursorFile(path2, log2);
26477
27945
  let epoch = persistedEpoch;
26478
27946
  let dirty = false;
@@ -26483,18 +27951,32 @@ function createControlCursorFileStore(path2, log2, options) {
26483
27951
  }
26484
27952
  dirty = false;
26485
27953
  try {
26486
- mkdirSync13(dirname14(path2), { recursive: true });
27954
+ mkdirSync14(dirname15(path2), { recursive: true });
26487
27955
  const tmpPath = `${path2}.tmp`;
26488
- writeFileSync12(
26489
- tmpPath,
26490
- `${JSON.stringify({
26491
- ...epoch === void 0 ? {} : { [controlCursorEpochKey]: epoch },
26492
- ...Object.fromEntries(cursors)
26493
- })}
26494
- `,
26495
- "utf8"
26496
- );
26497
- renameSync4(tmpPath, path2);
27956
+ const contents = `${JSON.stringify({
27957
+ ...epoch === void 0 ? {} : { [controlCursorEpochKey]: epoch },
27958
+ ...Object.fromEntries(cursors)
27959
+ })}
27960
+ `;
27961
+ if (fsyncOnWrite) {
27962
+ const fileFd = openSync5(tmpPath, "w");
27963
+ try {
27964
+ writeFileSync12(fileFd, contents, "utf8");
27965
+ fsyncSync2(fileFd);
27966
+ } finally {
27967
+ closeSync4(fileFd);
27968
+ }
27969
+ renameSync5(tmpPath, path2);
27970
+ const dirFd = openSync5(dirname15(path2), "r");
27971
+ try {
27972
+ fsyncSync2(dirFd);
27973
+ } finally {
27974
+ closeSync4(dirFd);
27975
+ }
27976
+ } else {
27977
+ writeFileSync12(tmpPath, contents, "utf8");
27978
+ renameSync5(tmpPath, path2);
27979
+ }
26498
27980
  } catch (error) {
26499
27981
  log2("control_cursor_store.write_failed", {
26500
27982
  path: path2,
@@ -26593,7 +28075,7 @@ function readControlCursorFile(path2, log2) {
26593
28075
  const cursors = /* @__PURE__ */ new Map();
26594
28076
  let raw;
26595
28077
  try {
26596
- raw = readFileSync17(path2, "utf8");
28078
+ raw = readFileSync18(path2, "utf8");
26597
28079
  } catch (error) {
26598
28080
  if (!isErrnoCode2(error, "ENOENT")) {
26599
28081
  log2("control_cursor_store.read_failed", {
@@ -26938,21 +28420,47 @@ function makeRunnerLogger(options) {
26938
28420
  consoleReporter
26939
28421
  );
26940
28422
  }
26941
- function installCleanupHandlers(close) {
26942
- const closeAndExit = () => {
26943
- void close().catch(() => void 0).finally(() => process.exit(0));
26944
- };
28423
+ function installCleanupHandlers(close, drainHooks, log2) {
28424
+ const uninstallDrain = installDrainOnSigterm({
28425
+ steps: {
28426
+ stopAccepting: () => {
28427
+ drainHooks.stopAccepting();
28428
+ },
28429
+ // SCOPE NOTE (BET1, partial): checkpointing an in-flight turn on a LIVE
28430
+ // worker (persisting enough codex turn state to resume it) is NOT
28431
+ // implemented in this branch - that turn still relies on the server's
28432
+ // rejoin replay. What IS durable is every durably-owned message that was
28433
+ // ENQUEUED into the per-thread inbound queue (the respawn-window / spawn-on-
28434
+ // miss path): flushInboundQueues fsyncs those below, and the boot/rejoin
28435
+ // drain replays them. Left as an explicit no-op (not silently absent) so the
28436
+ // gap is visible rather than implied-handled.
28437
+ checkpointCurrentTurn: () => void 0,
28438
+ flushInboundQueues: () => {
28439
+ drainHooks.flushInboundQueues();
28440
+ },
28441
+ // The final flush also performs the connection teardown via close(): the
28442
+ // cleanup stack runs after kandan.close() so no advance races the final
28443
+ // cursor/watermark write.
28444
+ flushOutbox: () => close().catch(() => void 0)
28445
+ },
28446
+ deadlineMs: runnerDrainExitDeadlineMs,
28447
+ log: (event, fields) => log2(event, fields),
28448
+ // SIGHUP is a drain-AND-EXIT signal too: a detached child of the Electron
28449
+ // shell that receives a parent hangup must drain its inbound queue + cursor
28450
+ // stores and then EXIT, never linger as a defunct lock-holder that does no
28451
+ // work (the prior close()-without-exit handler left a zombie that stopped
28452
+ // accepting, tore down its connection, yet never exited - stranding prior
28453
+ // jobs and holding the runner lock). installDrainOnSigterm's handler exits
28454
+ // after the drain (deadline-bounded), so routing SIGHUP through it fixes the
28455
+ // orphan-zombie regression.
28456
+ signals: ["SIGTERM", "SIGINT", "SIGHUP"]
28457
+ });
26945
28458
  const closeOnExit = () => {
26946
28459
  void close().catch(() => void 0);
26947
28460
  };
26948
- process.once("SIGINT", closeAndExit);
26949
- process.once("SIGTERM", closeAndExit);
26950
- process.once("SIGHUP", closeAndExit);
26951
28461
  process.once("exit", closeOnExit);
26952
28462
  return () => {
26953
- process.off("SIGINT", closeAndExit);
26954
- process.off("SIGTERM", closeAndExit);
26955
- process.off("SIGHUP", closeAndExit);
28463
+ uninstallDrain();
26956
28464
  process.off("exit", closeOnExit);
26957
28465
  };
26958
28466
  }
@@ -27255,7 +28763,7 @@ async function applyCodexUnavailableControl(kandan, topic, instanceId, control,
27255
28763
  }
27256
28764
  throw new Error(error);
27257
28765
  }
27258
- async function applyControl(codex, kandan, topic, instanceId, options, agentProviders, allowedCwds, remoteCodexSandboxRunner, activeRemoteCodexExecRequests, activeClaudeCodeSessions, pendingClaudeCodeApprovals, ensureClaudeCodeForwardPortSession, disposeClaudeCodeForwardPortSession, control, log2, onStartedThread, onThreadProcessStart) {
28766
+ async function applyControl(codex, kandan, topic, instanceId, options, agentProviders, allowedCwds, remoteCodexSandboxRunner, activeRemoteCodexExecRequests, activeClaudeCodeSessions, pendingClaudeCodeApprovals, ensureClaudeCodeForwardPortSession, disposeClaudeCodeForwardPortSession, control, log2, onStartedThread, onThreadProcessStart, leaseEpochFor, onLiveSourceSeqAccepted, captureLiveFollowUpEvent, onUndeliveredFollowUpsAtClose, liveSourceSeqAlreadyApplied) {
27259
28767
  if (codex === void 0) {
27260
28768
  switch (control.type) {
27261
28769
  case "remote_codex_exec":
@@ -27448,6 +28956,9 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
27448
28956
  ensureClaudeCodeForwardPortSession,
27449
28957
  disposeClaudeCodeForwardPortSession,
27450
28958
  resumeSessionId: normalizedWorkDescription(control.resumeSessionId),
28959
+ leaseEpochFor,
28960
+ onLiveSourceSeqAccepted,
28961
+ onUndeliveredFollowUpsAtClose,
27451
28962
  setStartupStage: (stage) => {
27452
28963
  startupStage = stage;
27453
28964
  }
@@ -27650,6 +29161,51 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
27650
29161
  message
27651
29162
  };
27652
29163
  }
29164
+ const followUpThreadId = optionalThreadControlField(
29165
+ control,
29166
+ "threadId"
29167
+ );
29168
+ const followUpChannel = optionalThreadControlField(
29169
+ control,
29170
+ "channel"
29171
+ );
29172
+ if (sourceSeq !== void 0 && followUpThreadId !== void 0 && followUpChannel !== void 0) {
29173
+ captureLiveFollowUpEvent?.(followUpChannel, followUpThreadId, {
29174
+ seq: sourceSeq,
29175
+ type: "thread.message",
29176
+ actorKind: void 0,
29177
+ actorSlug: void 0,
29178
+ actorUserId: void 0,
29179
+ threadId: followUpThreadId,
29180
+ threadTitle: void 0,
29181
+ replyToSeq: integerValue(control.rootSeq),
29182
+ localRunnerEventType: void 0,
29183
+ body: workDescription,
29184
+ attachments
29185
+ });
29186
+ }
29187
+ if (followUpChannel !== void 0 && followUpThreadId !== void 0 && liveSourceSeqAlreadyApplied?.({
29188
+ channel: followUpChannel,
29189
+ threadId: followUpThreadId,
29190
+ sourceSeq
29191
+ }) === true) {
29192
+ log2("claude_code.message_duplicate_ignored", {
29193
+ linzumi_thread_id: followUpThreadId,
29194
+ claude_session_id: codexThreadId,
29195
+ body_preview: claudeConsoleBodyPreview(workDescription),
29196
+ source_seq: sourceSeq ?? null
29197
+ });
29198
+ return {
29199
+ instanceId,
29200
+ controlType: control.type,
29201
+ agentProvider: "claude-code",
29202
+ cwd: cwd.cwd,
29203
+ matchedRoot: cwd.matchedRoot,
29204
+ codexThreadId,
29205
+ queuedInput: true,
29206
+ duplicate: true
29207
+ };
29208
+ }
27653
29209
  try {
27654
29210
  const enqueueResult = activeSession.enqueueInput({
27655
29211
  content: contentResult.content,
@@ -27754,6 +29310,9 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
27754
29310
  pendingClaudeCodeApprovals,
27755
29311
  ensureClaudeCodeForwardPortSession,
27756
29312
  disposeClaudeCodeForwardPortSession,
29313
+ leaseEpochFor,
29314
+ onLiveSourceSeqAccepted,
29315
+ onUndeliveredFollowUpsAtClose,
27757
29316
  setStartupStage: (stage) => {
27758
29317
  startupStage = stage;
27759
29318
  }
@@ -27797,6 +29356,30 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
27797
29356
  if (startedThreadSession === void 0 || sourceSeq === void 0) {
27798
29357
  throw new Error("cannot reconnect a Kandan thread without a session");
27799
29358
  }
29359
+ const reconnectChannel = optionalThreadControlField(control, "channel");
29360
+ const reconnectThreadId = optionalThreadControlField(
29361
+ control,
29362
+ "threadId"
29363
+ );
29364
+ if (reconnectChannel !== void 0 && reconnectThreadId !== void 0 && liveSourceSeqAlreadyApplied?.({
29365
+ channel: reconnectChannel,
29366
+ threadId: reconnectThreadId,
29367
+ sourceSeq
29368
+ }) === true) {
29369
+ log2("kandan.reconnect_thread_seq_already_applied", {
29370
+ linzumi_thread_id: reconnectThreadId,
29371
+ codex_thread_id: codexThreadId,
29372
+ source_seq: sourceSeq
29373
+ });
29374
+ return {
29375
+ instanceId,
29376
+ controlType: control.type,
29377
+ cwd: cwd.cwd,
29378
+ matchedRoot: cwd.matchedRoot,
29379
+ codexThreadId,
29380
+ duplicate: true
29381
+ };
29382
+ }
27800
29383
  await resumeCodexThreadForReconnect(
27801
29384
  codex,
27802
29385
  codexThreadId,
@@ -28543,11 +30126,13 @@ function claudeCodeTurnStallThresholdMs(env) {
28543
30126
  return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultClaudeCodeTurnStallThresholdMs;
28544
30127
  }
28545
30128
  function createClaudeCodeInputQueue(input) {
30129
+ const onSourceSeqDelivered = input.onSourceSeqDelivered;
28546
30130
  const pendingMessages = [
28547
30131
  claudeCodeUserInputMessage(input.content)
28548
30132
  ];
28549
30133
  const pendingSourceSeqs = [input.sourceSeq];
28550
30134
  const deferredFollowUps = [];
30135
+ const deliveredSourceSeqs = /* @__PURE__ */ new Set();
28551
30136
  const waiters = [];
28552
30137
  const state = {
28553
30138
  closed: false
@@ -28563,6 +30148,10 @@ function createClaudeCodeInputQueue(input) {
28563
30148
  };
28564
30149
  const deliverNow = (message, sourceSeq) => {
28565
30150
  pendingSourceSeqs.push(sourceSeq);
30151
+ if (sourceSeq !== void 0 && onSourceSeqDelivered !== void 0 && !deliveredSourceSeqs.has(sourceSeq)) {
30152
+ deliveredSourceSeqs.add(sourceSeq);
30153
+ onSourceSeqDelivered(sourceSeq);
30154
+ }
28566
30155
  const waiter = waiters.shift();
28567
30156
  if (waiter === void 0) {
28568
30157
  pendingMessages.push(message);
@@ -28631,6 +30220,17 @@ function createClaudeCodeInputQueue(input) {
28631
30220
  },
28632
30221
  enqueue,
28633
30222
  enqueueSteer,
30223
+ // BLOCKER A (don't drop deferred follow-ups on a kill): the parked,
30224
+ // never-delivered follow-ups (a turn was in flight at close, so close()'s
30225
+ // drain is intentionally skipped to avoid folding them into the active turn).
30226
+ // Their watermark was NOT committed (commit fires only at deliverNow), so the
30227
+ // runner must re-enqueue them into the DURABLE inbound queue; a kill-before-
30228
+ // run then replays + runs them exactly once. Drains the parked list so a
30229
+ // double-call cannot double-enqueue. Returns the un-run seqs+messages.
30230
+ drainUndeliveredFollowUps: () => {
30231
+ const drained = deferredFollowUps.splice(0);
30232
+ return drained;
30233
+ },
28634
30234
  completeTurn: () => {
28635
30235
  const completed = pendingSourceSeqs.shift();
28636
30236
  if (!state.closed && pendingSourceSeqs.length === 0) {
@@ -28857,7 +30457,21 @@ async function startClaudeCodeProviderInstance(args) {
28857
30457
  }
28858
30458
  const inputQueue = createClaudeCodeInputQueue({
28859
30459
  content: initialContentResult.content,
28860
- sourceSeq
30460
+ sourceSeq,
30461
+ // BLOCKER A: commit the persisted dedup watermark at DISPATCH (deliverNow),
30462
+ // not at enqueue-accept. enqueueInput used to commit unconditionally right
30463
+ // after inputQueue.enqueue, but enqueue PARKS a mid-turn follow-up in
30464
+ // deferredFollowUps (a bare return) and a close-in-flight drops it - so a
30465
+ // never-run seq was marked applied and its replay was gated 'ignored' (the
30466
+ // drop). Committing only when the seq is actually delivered onto the live
30467
+ // stream means a parked-but-unrun seq is never marked applied.
30468
+ onSourceSeqDelivered: (deliveredSeq) => {
30469
+ args.onLiveSourceSeqAccepted?.({
30470
+ channel,
30471
+ threadId,
30472
+ sourceSeq: deliveredSeq
30473
+ });
30474
+ }
28861
30475
  });
28862
30476
  const developerPrompt = normalizedWorkDescription(
28863
30477
  args.control.developerPrompt
@@ -28966,7 +30580,8 @@ async function startClaudeCodeProviderInstance(args) {
28966
30580
  channelSlug: channel,
28967
30581
  kandanThreadId: threadId,
28968
30582
  codexThreadId: activeSessionId ?? args.resumeSessionId,
28969
- rootSeq
30583
+ rootSeq,
30584
+ leaseEpoch: args.leaseEpochFor?.(threadId)
28970
30585
  }),
28971
30586
  push: async (event, payload, _timeoutMs, onSent) => {
28972
30587
  const reply = await args.kandan.push(args.topic, event, payload, onSent);
@@ -29014,7 +30629,7 @@ async function startClaudeCodeProviderInstance(args) {
29014
30629
  log: args.log
29015
30630
  });
29016
30631
  const liveBashCapture = args.options.claudeCodeRunner === void 0 && claudeLiveBashCaptureEnabled(process.env) ? createClaudeCodeLiveBashCapture({
29017
- captureDir: join21(
30632
+ captureDir: join22(
29018
30633
  tmpdir3(),
29019
30634
  `linzumi-claude-live-bash-${args.instanceId}`
29020
30635
  ),
@@ -29133,6 +30748,7 @@ async function startClaudeCodeProviderInstance(args) {
29133
30748
  linzumi_thread_id: threadId,
29134
30749
  claude_session_id: event.sessionId
29135
30750
  });
30751
+ args.onLiveSourceSeqAccepted?.({ channel, threadId, sourceSeq });
29136
30752
  args.activeClaudeCodeSessions.set(event.sessionId, {
29137
30753
  abortController,
29138
30754
  workspace,
@@ -29341,6 +30957,15 @@ async function startClaudeCodeProviderInstance(args) {
29341
30957
  onTranscriptEvent
29342
30958
  });
29343
30959
  } finally {
30960
+ const undeliveredFollowUps = inputQueue.drainUndeliveredFollowUps();
30961
+ const undeliveredSeqs = undeliveredFollowUps.map((followUp) => followUp.sourceSeq).filter((seq) => seq !== void 0);
30962
+ if (undeliveredSeqs.length > 0 && args.onUndeliveredFollowUpsAtClose !== void 0) {
30963
+ args.onUndeliveredFollowUpsAtClose({
30964
+ channel,
30965
+ threadId,
30966
+ sourceSeqs: undeliveredSeqs
30967
+ });
30968
+ }
29344
30969
  inputQueue.close();
29345
30970
  mcpAuthCleanup?.();
29346
30971
  liveBashCapture?.close();
@@ -29864,15 +31489,15 @@ function rehydrationControlSnapshot(args) {
29864
31489
  ...runtimeSettings.fast === void 0 ? {} : { fast: runtimeSettings.fast }
29865
31490
  };
29866
31491
  }
29867
- function kandanChatAttachmentsToWire(attachments) {
29868
- return attachments.map((attachment) => ({
29869
- ...attachment.id === void 0 ? {} : { id: attachment.id },
29870
- ...attachment.kind === void 0 ? {} : { kind: attachment.kind },
29871
- ...attachment.fileName === void 0 ? {} : { file_name: attachment.fileName },
29872
- ...attachment.contentType === void 0 ? {} : { content_type: attachment.contentType },
29873
- ...attachment.sizeBytes === void 0 ? {} : { size_bytes: attachment.sizeBytes },
29874
- ...attachment.url === void 0 ? {} : { url: attachment.url }
29875
- }));
31492
+ function deadLetterFailedReason(reason) {
31493
+ switch (reason) {
31494
+ case "exhausted":
31495
+ return "couldn't resume this job after several attempts; please resend your last message to continue.";
31496
+ case "withheld":
31497
+ return "this message may have already run before an unexpected shutdown; it was not re-run to avoid duplicating work. Please resend if it did not complete.";
31498
+ case "permanent":
31499
+ return "couldn't resume this job's coding session.";
31500
+ }
29876
31501
  }
29877
31502
  function rehydrationReconnectControl(record) {
29878
31503
  const snapshot = record.control;
@@ -30920,8 +32545,8 @@ function onboardingConversationOfficeImport(response) {
30920
32545
  return objectValue(metadata?.office_channel_import);
30921
32546
  }
30922
32547
  function onboardingConversationImportClientMessageId(importKey, itemKey) {
30923
- const importHash = createHash6("sha256").update(importKey).digest("hex");
30924
- const itemHash = createHash6("sha256").update(itemKey).digest("hex");
32548
+ const importHash = createHash7("sha256").update(importKey).digest("hex");
32549
+ const itemHash = createHash7("sha256").update(itemKey).digest("hex");
30925
32550
  return `onboarding-import:${importHash.slice(0, 32)}:${itemHash.slice(0, 24)}`;
30926
32551
  }
30927
32552
  async function uploadLocalConversationAttachments(args) {
@@ -31019,7 +32644,7 @@ async function localConversationAttachmentFile(candidatePath, attachment) {
31019
32644
  };
31020
32645
  }
31021
32646
  case "path": {
31022
- const path2 = isAbsolute5(attachment.path) ? attachment.path : resolve8(dirname14(candidatePath), attachment.path);
32647
+ const path2 = isAbsolute5(attachment.path) ? attachment.path : resolve8(dirname15(candidatePath), attachment.path);
31023
32648
  try {
31024
32649
  const bytes = await readFile2(path2);
31025
32650
  return {
@@ -31474,9 +33099,9 @@ function mcpOwnerUsername(options) {
31474
33099
  return options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
31475
33100
  }
31476
33101
  function writeEphemeralMcpAuthFile(options) {
31477
- const directory = mkdtempSync4(join21(tmpdir3(), "linzumi-mcp-auth-"));
33102
+ const directory = mkdtempSync4(join22(tmpdir3(), "linzumi-mcp-auth-"));
31478
33103
  chmodSync2(directory, 448);
31479
- const path2 = join21(directory, "auth.json");
33104
+ const path2 = join22(directory, "auth.json");
31480
33105
  writeCachedLocalRunnerToken({
31481
33106
  kandanUrl: options.kandanUrl,
31482
33107
  accessToken: options.token,
@@ -31552,7 +33177,7 @@ function configuredAllowedCwds(values, options = {}) {
31552
33177
  const absolutePath = resolve8(expandUserPath(value));
31553
33178
  try {
31554
33179
  if (options.createMissing === true) {
31555
- mkdirSync13(absolutePath, { recursive: true });
33180
+ mkdirSync14(absolutePath, { recursive: true });
31556
33181
  }
31557
33182
  const realPath = realpathSync6(absolutePath);
31558
33183
  allowedCwds.push(
@@ -31589,7 +33214,7 @@ function allowedCwdProjects(allowedCwds) {
31589
33214
  });
31590
33215
  }
31591
33216
  function isGitProjectDirectory(cwd) {
31592
- const gitPath = join21(cwd, ".git");
33217
+ const gitPath = join22(cwd, ".git");
31593
33218
  try {
31594
33219
  const gitPathStats = statSync3(gitPath);
31595
33220
  return gitPathStats.isDirectory() || gitPathStats.isFile();
@@ -31612,9 +33237,9 @@ function browseRunnerDirectory(control, options) {
31612
33237
  error: "not_directory"
31613
33238
  };
31614
33239
  }
31615
- const parent = dirname14(currentPath);
33240
+ const parent = dirname15(currentPath);
31616
33241
  const entries = readdirSync4(currentPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => visibleRunnerDirectoryEntryName(entry.name)).map((entry) => {
31617
- const path2 = join21(currentPath, entry.name);
33242
+ const path2 = join22(currentPath, entry.name);
31618
33243
  return {
31619
33244
  name: entry.name,
31620
33245
  path: path2,
@@ -31655,7 +33280,7 @@ function projectDirectoryName(name) {
31655
33280
  function availableProjectDirectoryName(projectsRoot, baseName, suffix = 0) {
31656
33281
  for (let nextSuffix = suffix; ; nextSuffix += 1) {
31657
33282
  const candidate = nextSuffix === 0 ? baseName : `${baseName}-${nextSuffix}`;
31658
- if (!projectPathExists(join21(projectsRoot, candidate))) {
33283
+ if (!projectPathExists(join22(projectsRoot, candidate))) {
31659
33284
  return candidate;
31660
33285
  }
31661
33286
  }
@@ -31683,9 +33308,9 @@ function createRunnerProject(control, options, allowedCwds) {
31683
33308
  error: "invalid_project_template"
31684
33309
  };
31685
33310
  }
31686
- const projectsRoot = join21(currentHomeDirectory(), "linzumi");
33311
+ const projectsRoot = join22(currentHomeDirectory(), "linzumi");
31687
33312
  const resolvedProjectDirName = template === "hello_linzumi_demo" ? availableProjectDirectoryName(projectsRoot, projectDirName) : projectDirName;
31688
- const projectPath = join21(projectsRoot, resolvedProjectDirName);
33313
+ const projectPath = join22(projectsRoot, resolvedProjectDirName);
31689
33314
  let createdProjectPath = false;
31690
33315
  try {
31691
33316
  if (template !== "hello_linzumi_demo" && projectPathExists(projectPath)) {
@@ -31697,7 +33322,7 @@ function createRunnerProject(control, options, allowedCwds) {
31697
33322
  error: "project_directory_exists"
31698
33323
  };
31699
33324
  }
31700
- mkdirSync13(projectsRoot, { recursive: true });
33325
+ mkdirSync14(projectsRoot, { recursive: true });
31701
33326
  if (template === "hello_linzumi_demo") {
31702
33327
  createdProjectPath = true;
31703
33328
  createHelloLinzumiProject({
@@ -31705,7 +33330,7 @@ function createRunnerProject(control, options, allowedCwds) {
31705
33330
  name: resolvedProjectDirName
31706
33331
  });
31707
33332
  } else {
31708
- mkdirSync13(projectPath, { recursive: false });
33333
+ mkdirSync14(projectPath, { recursive: false });
31709
33334
  createdProjectPath = true;
31710
33335
  }
31711
33336
  const git = spawnSync5("git", ["init"], {
@@ -31818,7 +33443,7 @@ async function suggestRunnerTasks(control, options, allowedCwds) {
31818
33443
  };
31819
33444
  }
31820
33445
  }
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;
33446
+ var THREAD_RUNNER_READY_TIMEOUT_MS, THREAD_RUNNER_REAPER_INTERVAL_MS, onboardingDiscoveryAgentStartKeys, onboardingConversationImportKeys, onboardingConversationDiscoveryReportLimit, CODEX_AUTH_STATUS_PROBE_TIMEOUT_MS, staleStartInstanceWindowMs, connectionStaleThresholdMs, connectionUnrecoverableAfterMs, controlCursorWriteDebounceMs, controlCursorEpochKey, codexTurnTerminalNotificationMethods, runnerDrainExitDeadlineMs, claudeSessionStoreSequenceHighWater, claudeCodeLlmProxySessionHeader, onboardingConversationTitleFirstMessages, onboardingConversationTitleLastMessages, onboardingConversationTitleBodyMaxLength, onboardingConversationImportMessageMaxLength, onboardingConversationImportCandidateLimit, onboardingConversationImportProgressInterval;
31822
33447
  var init_runner = __esm({
31823
33448
  "src/runner.ts"() {
31824
33449
  "use strict";
@@ -31826,6 +33451,7 @@ var init_runner = __esm({
31826
33451
  init_runnerThreadSessionRegistry();
31827
33452
  init_outboxJournal();
31828
33453
  init_channelSessionSupport();
33454
+ init_channelSessionSupport();
31829
33455
  init_commanderAttachments();
31830
33456
  init_claudeCodePipeline();
31831
33457
  init_claudeCodePlanMirror();
@@ -31868,11 +33494,18 @@ var init_runner = __esm({
31868
33494
  init_signupTaskSuggestions();
31869
33495
  init_userFacingErrors();
31870
33496
  init_remoteCodexSandboxRunner();
33497
+ init_durableInboundQueue();
33498
+ init_appliedSourceSeqWatermark();
33499
+ init_threadReconcilerLock();
33500
+ init_runnerLifecycleGuards();
33501
+ init_leaseEpochGuard();
33502
+ init_resumeKeystone();
31871
33503
  THREAD_RUNNER_READY_TIMEOUT_MS = 3e4;
31872
33504
  THREAD_RUNNER_REAPER_INTERVAL_MS = 3e4;
31873
33505
  onboardingDiscoveryAgentStartKeys = /* @__PURE__ */ new Set();
31874
33506
  onboardingConversationImportKeys = /* @__PURE__ */ new Set();
31875
33507
  onboardingConversationDiscoveryReportLimit = 100;
33508
+ CODEX_AUTH_STATUS_PROBE_TIMEOUT_MS = 3e3;
31876
33509
  staleStartInstanceWindowMs = 6e4;
31877
33510
  connectionStaleThresholdMs = 12e4;
31878
33511
  connectionUnrecoverableAfterMs = 10 * 60 * 1e3;
@@ -31885,6 +33518,7 @@ var init_runner = __esm({
31885
33518
  "turn/canceled",
31886
33519
  "turn/cancelled"
31887
33520
  ]);
33521
+ runnerDrainExitDeadlineMs = 8e3;
31888
33522
  claudeSessionStoreSequenceHighWater = /* @__PURE__ */ new Map();
31889
33523
  claudeCodeLlmProxySessionHeader = "x-linzumi-llm-proxy-token";
31890
33524
  onboardingConversationTitleFirstMessages = 4;
@@ -31897,7 +33531,7 @@ var init_runner = __esm({
31897
33531
  });
31898
33532
 
31899
33533
  // src/kandanTls.ts
31900
- import { existsSync as existsSync14, readFileSync as readFileSync18 } from "node:fs";
33534
+ import { existsSync as existsSync14, readFileSync as readFileSync19 } from "node:fs";
31901
33535
  import { Agent } from "undici";
31902
33536
  import WsWebSocket from "ws";
31903
33537
  function kandanTlsTrustFromEnv() {
@@ -31911,7 +33545,7 @@ function kandanTlsTrustFromCaFile(caFile) {
31911
33545
  if (!existsSync14(trimmed)) {
31912
33546
  throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
31913
33547
  }
31914
- const ca = readFileSync18(trimmed, "utf8");
33548
+ const ca = readFileSync19(trimmed, "utf8");
31915
33549
  return {
31916
33550
  caFile: trimmed,
31917
33551
  ca,
@@ -50695,7 +52329,7 @@ var init_RemoveFileError = __esm({
50695
52329
 
50696
52330
  // ../../node_modules/@inquirer/external-editor/dist/esm/index.js
50697
52331
  import { spawn as spawn11, spawnSync as spawnSync6 } from "child_process";
50698
- import { readFileSync as readFileSync21, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "fs";
52332
+ import { readFileSync as readFileSync22, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "fs";
50699
52333
  import path from "node:path";
50700
52334
  import os from "node:os";
50701
52335
  import { randomUUID as randomUUID5 } from "node:crypto";
@@ -50819,7 +52453,7 @@ var init_esm5 = __esm({
50819
52453
  }
50820
52454
  readTemporaryFile() {
50821
52455
  try {
50822
- const tempFileBuffer = readFileSync21(this.tempFile);
52456
+ const tempFileBuffer = readFileSync22(this.tempFile);
50823
52457
  if (tempFileBuffer.length === 0) {
50824
52458
  this.text = "";
50825
52459
  } else {
@@ -52188,17 +53822,17 @@ import { spawn as spawn12, spawnSync as spawnSync7 } from "node:child_process";
52188
53822
  import {
52189
53823
  existsSync as existsSync17,
52190
53824
  constants as fsConstants,
52191
- mkdirSync as mkdirSync16,
53825
+ mkdirSync as mkdirSync17,
52192
53826
  mkdtempSync as mkdtempSync6,
52193
- readFileSync as readFileSync22,
53827
+ readFileSync as readFileSync23,
52194
53828
  readdirSync as readdirSync5,
52195
53829
  rmSync as rmSync7,
52196
53830
  statSync as statSync4,
52197
53831
  writeFileSync as writeFileSync16
52198
53832
  } from "node:fs";
52199
53833
  import { access } from "node:fs/promises";
52200
- import { homedir as homedir17, tmpdir as tmpdir5 } from "node:os";
52201
- import { delimiter as delimiter3, dirname as dirname18, join as join25, resolve as resolve10 } from "node:path";
53834
+ import { homedir as homedir18, tmpdir as tmpdir5 } from "node:os";
53835
+ import { delimiter as delimiter3, dirname as dirname19, join as join26, resolve as resolve10 } from "node:path";
52202
53836
  import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
52203
53837
  import { emitKeypressEvents } from "node:readline";
52204
53838
  function signupHelpText() {
@@ -52319,7 +53953,7 @@ function defaultSignupDraftStore(serviceUrl) {
52319
53953
  }
52320
53954
  let parsed;
52321
53955
  try {
52322
- parsed = JSON.parse(readFileSync22(path2, "utf8"));
53956
+ parsed = JSON.parse(readFileSync23(path2, "utf8"));
52323
53957
  } catch (_error) {
52324
53958
  return void 0;
52325
53959
  }
@@ -52329,7 +53963,7 @@ function defaultSignupDraftStore(serviceUrl) {
52329
53963
  return comparableServiceUrl(parsed.serviceUrl) === comparableServiceUrl(serviceUrl) ? parsed : void 0;
52330
53964
  },
52331
53965
  write: (draft) => {
52332
- mkdirSync16(dirname18(path2), { recursive: true });
53966
+ mkdirSync17(dirname19(path2), { recursive: true });
52333
53967
  writeFileSync16(path2, `${JSON.stringify(draft, null, 2)}
52334
53968
  `, {
52335
53969
  encoding: "utf8",
@@ -52342,8 +53976,8 @@ function defaultSignupDraftStore(serviceUrl) {
52342
53976
  };
52343
53977
  }
52344
53978
  function defaultSignupDraftPath(serviceUrl) {
52345
- return join25(
52346
- dirname18(localConfigPath()),
53979
+ return join26(
53980
+ dirname19(localConfigPath()),
52347
53981
  `signup-draft.${localConfigScopeFileStem(serviceUrl)}.json`
52348
53982
  );
52349
53983
  }
@@ -52377,7 +54011,7 @@ function defaultSignupTaskCacheStore(serviceUrl) {
52377
54011
  }
52378
54012
  }
52379
54013
  };
52380
- mkdirSync16(dirname18(path2), { recursive: true });
54014
+ mkdirSync17(dirname19(path2), { recursive: true });
52381
54015
  writeFileSync16(path2, `${JSON.stringify(next, null, 2)}
52382
54016
  `, {
52383
54017
  encoding: "utf8",
@@ -52387,8 +54021,8 @@ function defaultSignupTaskCacheStore(serviceUrl) {
52387
54021
  };
52388
54022
  }
52389
54023
  function defaultSignupTaskCachePath(serviceUrl) {
52390
- return join25(
52391
- dirname18(localConfigPath()),
54024
+ return join26(
54025
+ dirname19(localConfigPath()),
52392
54026
  `signup-task-cache.${localConfigScopeFileStem(serviceUrl)}.json`
52393
54027
  );
52394
54028
  }
@@ -52398,7 +54032,7 @@ function readSignupTaskCache(path2) {
52398
54032
  }
52399
54033
  let parsed;
52400
54034
  try {
52401
- parsed = JSON.parse(readFileSync22(path2, "utf8"));
54035
+ parsed = JSON.parse(readFileSync23(path2, "utf8"));
52402
54036
  } catch (_error) {
52403
54037
  return { version: 1, entries: {} };
52404
54038
  }
@@ -54283,7 +55917,7 @@ function autocompletePathSuggestions(pathInput) {
54283
55917
  try {
54284
55918
  const showHidden = prefix.startsWith(".");
54285
55919
  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) => join25(directoryPath, entry.name)).map((path2) => ({
55920
+ const childPaths = readdirSync5(directoryPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => showHidden || !entry.name.startsWith(".")).map((entry) => join26(directoryPath, entry.name)).map((path2) => ({
54287
55921
  path: path2,
54288
55922
  score: fuzzyPathSegmentScore(path2.split("/").at(-1) ?? path2, prefix)
54289
55923
  })).filter((candidate) => candidate.score !== void 0).sort((left, right) => {
@@ -54529,7 +56163,7 @@ async function runSignupPreflights(runtime) {
54529
56163
  function defaultPreflightRuntime() {
54530
56164
  return {
54531
56165
  cwd: process.cwd(),
54532
- homeDir: homedir17(),
56166
+ homeDir: homedir18(),
54533
56167
  probeTool,
54534
56168
  readGitEmail,
54535
56169
  discoverCodeRoots,
@@ -54674,9 +56308,9 @@ async function suggestTasksWithCodex(projectPath, retryPolicy) {
54674
56308
  );
54675
56309
  }
54676
56310
  async function runCodexTaskSuggestion2(projectPath, previousResponse, retryPolicy) {
54677
- const tempRoot = mkdtempSync6(join25(tmpdir5(), "linzumi-signup-codex-tasks-"));
54678
- const schemaPath = join25(tempRoot, "task-suggestions.schema.json");
54679
- const outputPath = join25(tempRoot, "task-suggestions.json");
56311
+ const tempRoot = mkdtempSync6(join26(tmpdir5(), "linzumi-signup-codex-tasks-"));
56312
+ const schemaPath = join26(tempRoot, "task-suggestions.schema.json");
56313
+ const outputPath = join26(tempRoot, "task-suggestions.json");
54680
56314
  const model = process.env.LINZUMI_SIGNUP_TASK_MODEL ?? "gpt-5.4-mini";
54681
56315
  const prompt = taskSuggestionPrompt2(previousResponse);
54682
56316
  const codexCommand = await resolveSignupCodexCommand();
@@ -54701,7 +56335,7 @@ async function runCodexTaskSuggestion2(projectPath, previousResponse, retryPolic
54701
56335
  ),
54702
56336
  retryPolicy
54703
56337
  );
54704
- return readFileSync22(outputPath, "utf8");
56338
+ return readFileSync23(outputPath, "utf8");
54705
56339
  } finally {
54706
56340
  rmSync7(tempRoot, { recursive: true, force: true });
54707
56341
  }
@@ -54738,7 +56372,7 @@ function codexTaskSuggestionProcess2(args) {
54738
56372
  function signupCodexTaskSuggestionProcessForTest(args) {
54739
56373
  return codexTaskSuggestionProcess2(args);
54740
56374
  }
54741
- async function resolveSignupCodexCommand(env = process.env, homeDir = homedir17(), executableExists = fileIsExecutable) {
56375
+ async function resolveSignupCodexCommand(env = process.env, homeDir = homedir18(), executableExists = fileIsExecutable) {
54742
56376
  const override = firstConfiguredValue([
54743
56377
  env.LINZUMI_SIGNUP_CODEX_BIN,
54744
56378
  env.LINZUMI_CODEX_BIN
@@ -54793,7 +56427,7 @@ function resolveHomePath(path2, homeDir) {
54793
56427
  return homeDir;
54794
56428
  }
54795
56429
  if (path2.startsWith("~/")) {
54796
- return join25(homeDir, path2.slice(2));
56430
+ return join26(homeDir, path2.slice(2));
54797
56431
  }
54798
56432
  return resolve10(path2);
54799
56433
  }
@@ -54806,9 +56440,9 @@ function commandLooksPathLike(command) {
54806
56440
  }
54807
56441
  function homeManagedCodexCandidates(homeDir) {
54808
56442
  return [
54809
- join25(homeDir, ".volta", "bin", "codex"),
54810
- join25(homeDir, ".local", "bin", "codex"),
54811
- join25(homeDir, "bin", "codex")
56443
+ join26(homeDir, ".volta", "bin", "codex"),
56444
+ join26(homeDir, ".local", "bin", "codex"),
56445
+ join26(homeDir, "bin", "codex")
54812
56446
  ];
54813
56447
  }
54814
56448
  async function firstExecutablePath(paths, executableExists) {
@@ -54823,7 +56457,7 @@ function commandPathCandidates(path2) {
54823
56457
  if (path2 === void 0 || path2.trim() === "") {
54824
56458
  return [];
54825
56459
  }
54826
- return path2.split(delimiter3).filter((entry) => entry.trim() !== "").map((entry) => join25(entry, "codex"));
56460
+ return path2.split(delimiter3).filter((entry) => entry.trim() !== "").map((entry) => join26(entry, "codex"));
54827
56461
  }
54828
56462
  async function fileIsExecutable(path2) {
54829
56463
  try {
@@ -55056,11 +56690,11 @@ function codexPreflightLocation(command, homeDir) {
55056
56690
  switch (command) {
55057
56691
  case "codex":
55058
56692
  return "on PATH";
55059
- case join25(homeDir, ".volta", "bin", "codex"):
56693
+ case join26(homeDir, ".volta", "bin", "codex"):
55060
56694
  return "via Volta";
55061
- case join25(homeDir, ".local", "bin", "codex"):
56695
+ case join26(homeDir, ".local", "bin", "codex"):
55062
56696
  return "via ~/.local/bin";
55063
- case join25(homeDir, "bin", "codex"):
56697
+ case join26(homeDir, "bin", "codex"):
55064
56698
  return "via ~/bin";
55065
56699
  default:
55066
56700
  return "from configured path";
@@ -55219,7 +56853,7 @@ function probeToolWithArgs(command, args, cwd) {
55219
56853
  }
55220
56854
  async function discoverCodeRoots(homeDir) {
55221
56855
  const candidates = ["src", "code", "projects"].map(
55222
- (name) => join25(homeDir, name)
56856
+ (name) => join26(homeDir, name)
55223
56857
  );
55224
56858
  return candidates.filter((path2) => existsSync17(path2)).flatMap((path2) => discoveredProjectNames(path2));
55225
56859
  }
@@ -55273,7 +56907,7 @@ function discoverProjectsFromCurrentDirectory(cwd) {
55273
56907
  }
55274
56908
  function discoverProjectsFromGuessedRoots(homeDir) {
55275
56909
  const guessedRoots = ["src", "code", "projects"].map(
55276
- (name) => join25(homeDir, name)
56910
+ (name) => join26(homeDir, name)
55277
56911
  );
55278
56912
  const projects = guessedRoots.flatMap(
55279
56913
  (root) => discoverProjectsUnderRoot(root)
@@ -55325,25 +56959,25 @@ function looksLikeProject(path2) {
55325
56959
  "pnpm-lock.yaml",
55326
56960
  "yarn.lock",
55327
56961
  "package-lock.json"
55328
- ].some((name) => existsSync17(join25(path2, name)));
56962
+ ].some((name) => existsSync17(join26(path2, name)));
55329
56963
  }
55330
56964
  function detectProjectLanguage(path2) {
55331
- if (existsSync17(join25(path2, "pyproject.toml")) || existsSync17(join25(path2, "requirements.txt"))) {
56965
+ if (existsSync17(join26(path2, "pyproject.toml")) || existsSync17(join26(path2, "requirements.txt"))) {
55332
56966
  return "Python";
55333
56967
  }
55334
- if (existsSync17(join25(path2, "Cargo.toml"))) {
56968
+ if (existsSync17(join26(path2, "Cargo.toml"))) {
55335
56969
  return "Rust";
55336
56970
  }
55337
- if (existsSync17(join25(path2, "go.mod"))) {
56971
+ if (existsSync17(join26(path2, "go.mod"))) {
55338
56972
  return "Go";
55339
56973
  }
55340
- if (existsSync17(join25(path2, "mix.exs"))) {
56974
+ if (existsSync17(join26(path2, "mix.exs"))) {
55341
56975
  return "Elixir";
55342
56976
  }
55343
- if (existsSync17(join25(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
56977
+ if (existsSync17(join26(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
55344
56978
  return "TypeScript";
55345
56979
  }
55346
- if (existsSync17(join25(path2, "package.json"))) {
56980
+ if (existsSync17(join26(path2, "package.json"))) {
55347
56981
  return "JavaScript";
55348
56982
  }
55349
56983
  return "Project";
@@ -55351,7 +56985,7 @@ function detectProjectLanguage(path2) {
55351
56985
  function packageJsonMentionsTypeScript(path2) {
55352
56986
  try {
55353
56987
  const packageJson2 = JSON.parse(
55354
- readFileSync22(join25(path2, "package.json"), "utf8")
56988
+ readFileSync23(join26(path2, "package.json"), "utf8")
55355
56989
  );
55356
56990
  return packageJson2.dependencies?.typescript !== void 0 || packageJson2.devDependencies?.typescript !== void 0;
55357
56991
  } catch {
@@ -55359,11 +56993,11 @@ function packageJsonMentionsTypeScript(path2) {
55359
56993
  }
55360
56994
  }
55361
56995
  function hasGitMetadata(path2) {
55362
- return existsSync17(join25(path2, ".git"));
56996
+ return existsSync17(join26(path2, ".git"));
55363
56997
  }
55364
56998
  function childDirectories(root) {
55365
56999
  try {
55366
- return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join25(root, entry.name));
57000
+ return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join26(root, entry.name));
55367
57001
  } catch {
55368
57002
  return [];
55369
57003
  }
@@ -55389,10 +57023,10 @@ function directoryExists(path2) {
55389
57023
  }
55390
57024
  function expandHomePath(path2) {
55391
57025
  if (path2 === "~") {
55392
- return homedir17();
57026
+ return homedir18();
55393
57027
  }
55394
57028
  if (path2.startsWith("~/")) {
55395
- return join25(homedir17(), path2.slice(2));
57029
+ return join26(homedir18(), path2.slice(2));
55396
57030
  }
55397
57031
  return resolve10(path2);
55398
57032
  }
@@ -55483,8 +57117,8 @@ init_runner();
55483
57117
  init_runnerLockTakeover();
55484
57118
  init_claudeCodeSession();
55485
57119
  init_authCache();
55486
- import { existsSync as existsSync18, readFileSync as readFileSync23, realpathSync as realpathSync7 } from "node:fs";
55487
- import { homedir as homedir18 } from "node:os";
57120
+ import { existsSync as existsSync18, readFileSync as readFileSync24, realpathSync as realpathSync7 } from "node:fs";
57121
+ import { homedir as homedir19 } from "node:os";
55488
57122
  import { resolve as resolve11 } from "node:path";
55489
57123
  import { fileURLToPath as fileURLToPath4 } from "node:url";
55490
57124
 
@@ -55579,9 +57213,9 @@ init_kandanTls();
55579
57213
  init_protocol();
55580
57214
  init_json();
55581
57215
  init_defaultUrls();
55582
- import { existsSync as existsSync15, mkdirSync as mkdirSync14, readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "node:fs";
55583
- import { dirname as dirname15, join as join22 } from "node:path";
55584
- import { homedir as homedir15 } from "node:os";
57216
+ import { existsSync as existsSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync20, writeFileSync as writeFileSync13 } from "node:fs";
57217
+ import { dirname as dirname16, join as join23 } from "node:path";
57218
+ import { homedir as homedir16 } from "node:os";
55585
57219
  async function runAgentCliCommand(args, deps = {
55586
57220
  fetchImpl: fetch,
55587
57221
  stdout: process.stdout,
@@ -56289,7 +57923,7 @@ function agentTokenFile(flags) {
56289
57923
  return flags.get("agent-token-file") ?? defaultAgentTokenFilePath();
56290
57924
  }
56291
57925
  function defaultAgentTokenFilePath() {
56292
- return join22(homedir15(), ".linzumi", "agent-token.json");
57926
+ return join23(homedir16(), ".linzumi", "agent-token.json");
56293
57927
  }
56294
57928
  function normalizedApiUrl(apiUrl) {
56295
57929
  return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
@@ -56298,10 +57932,10 @@ function authorizationHeaders(token) {
56298
57932
  return { authorization: `Bearer ${token}` };
56299
57933
  }
56300
57934
  function readOptionalTextFile(path2) {
56301
- return existsSync15(path2) ? readFileSync19(path2, "utf8") : void 0;
57935
+ return existsSync15(path2) ? readFileSync20(path2, "utf8") : void 0;
56302
57936
  }
56303
57937
  function writeTextFile(path2, content) {
56304
- mkdirSync14(dirname15(path2), { recursive: true });
57938
+ mkdirSync15(dirname16(path2), { recursive: true });
56305
57939
  writeFileSync13(path2, content);
56306
57940
  }
56307
57941
  function readStoredAgentTokenFile(path2, readTextFile = readOptionalTextFile) {
@@ -56390,26 +58024,26 @@ init_helloLinzumiProject();
56390
58024
  init_runnerLogger();
56391
58025
  import {
56392
58026
  existsSync as existsSync16,
56393
- closeSync as closeSync3,
56394
- mkdirSync as mkdirSync15,
56395
- openSync as openSync4,
56396
- readFileSync as readFileSync20,
58027
+ closeSync as closeSync5,
58028
+ mkdirSync as mkdirSync16,
58029
+ openSync as openSync6,
58030
+ readFileSync as readFileSync21,
56397
58031
  watch,
56398
58032
  writeFileSync as writeFileSync14
56399
58033
  } from "node:fs";
56400
- import { homedir as homedir16 } from "node:os";
56401
- import { dirname as dirname16, join as join23, resolve as resolve9 } from "node:path";
58034
+ import { homedir as homedir17 } from "node:os";
58035
+ import { dirname as dirname17, join as join24, resolve as resolve9 } from "node:path";
56402
58036
  import { execFileSync, spawn as spawn10 } from "node:child_process";
56403
58037
  import { fileURLToPath as fileURLToPath3 } from "node:url";
56404
58038
  var connectedMarkers = ["Connected to Linzumi", "Runner connected:"];
56405
58039
  function commanderStatusDir() {
56406
- return join23(homedir16(), ".linzumi", "commanders");
58040
+ return join24(homedir17(), ".linzumi", "commanders");
56407
58041
  }
56408
58042
  function commanderStatusFile(runnerId, statusDir = commanderStatusDir()) {
56409
- return join23(statusDir, `${safeRunnerId(runnerId)}.json`);
58043
+ return join24(statusDir, `${safeRunnerId(runnerId)}.json`);
56410
58044
  }
56411
58045
  function defaultCommanderLogFile(runnerId) {
56412
- return join23(homedir16(), ".linzumi", "logs", `${safeRunnerId(runnerId)}.log`);
58046
+ return join24(homedir17(), ".linzumi", "logs", `${safeRunnerId(runnerId)}.log`);
56413
58047
  }
56414
58048
  function commanderLogIsConnected(log2) {
56415
58049
  return connectedMarkers.some((marker) => log2.includes(marker));
@@ -56430,10 +58064,10 @@ function startCommanderDaemon(options) {
56430
58064
  "--log-file",
56431
58065
  logFile
56432
58066
  ];
56433
- mkdirSync15(statusDir, { recursive: true });
56434
- mkdirSync15(dirname16(logFile), { recursive: true });
56435
- const out = openSync4(logFile, "a");
56436
- const err = openSync4(logFile, "a");
58067
+ mkdirSync16(statusDir, { recursive: true });
58068
+ mkdirSync16(dirname17(logFile), { recursive: true });
58069
+ const out = openSync6(logFile, "a");
58070
+ const err = openSync6(logFile, "a");
56437
58071
  writeCliAuditEvent(
56438
58072
  "process.spawn",
56439
58073
  {
@@ -56460,8 +58094,8 @@ function startCommanderDaemon(options) {
56460
58094
  },
56461
58095
  { sessionId: options.runnerId }
56462
58096
  );
56463
- closeSync3(out);
56464
- closeSync3(err);
58097
+ closeSync5(out);
58098
+ closeSync5(err);
56465
58099
  child.unref();
56466
58100
  if (child.pid === void 0) {
56467
58101
  throw new Error("commander daemon did not report a pid");
@@ -56486,12 +58120,12 @@ function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), proce
56486
58120
  if (!existsSync16(statusFile)) {
56487
58121
  return { status: "missing", runnerId, statusFile };
56488
58122
  }
56489
- const record = parseRecord2(readFileSync20(statusFile, "utf8"));
58123
+ const record = parseRecord2(readFileSync21(statusFile, "utf8"));
56490
58124
  return processIsRunning(record.pid) && processMatchesRecord(record, processIdentityReader) ? { status: "running", record } : { status: "stopped", record };
56491
58125
  }
56492
58126
  async function waitForCommanderDaemon(options) {
56493
58127
  const now = options.now ?? (() => Date.now());
56494
- const readTextFile = options.readTextFile ?? ((path2) => existsSync16(path2) ? readFileSync20(path2, "utf8") : void 0);
58128
+ const readTextFile = options.readTextFile ?? ((path2) => existsSync16(path2) ? readFileSync21(path2, "utf8") : void 0);
56495
58129
  const statusImpl = options.statusImpl ?? commanderDaemonStatus;
56496
58130
  const deadline = now() + options.timeoutMs;
56497
58131
  while (now() <= deadline) {
@@ -69384,7 +71018,7 @@ init_userFacingErrors();
69384
71018
  init_version();
69385
71019
  import { chmodSync as chmodSync3, mkdtempSync as mkdtempSync5, rmSync as rmSync6 } from "node:fs";
69386
71020
  import { tmpdir as tmpdir4 } from "node:os";
69387
- import { basename as basename9, dirname as dirname17, join as join24 } from "node:path";
71021
+ import { basename as basename9, dirname as dirname18, join as join25 } from "node:path";
69388
71022
  var configEnvKey = "LINZUMI_REMOTE_CODEX_HARNESS_CONFIG_B64";
69389
71023
  var remoteHarnessEnvironmentId = "linzumi-remote-codex-harness";
69390
71024
  async function runRemoteCodexHarnessWorkerFromEnv(env = process.env) {
@@ -69617,18 +71251,18 @@ function resolveLinzumiCliEntrypoint(entrypoint) {
69617
71251
  case "remote-codex-harness-worker.js":
69618
71252
  case "remote-codex-harness-worker.mjs":
69619
71253
  case "remote-codex-harness-worker.cjs":
69620
- return join24(dirname17(entrypoint), "linzumi.js");
71254
+ return join25(dirname18(entrypoint), "linzumi.js");
69621
71255
  case "remoteCodexHarnessWorkerEntrypoint.ts":
69622
71256
  case "remoteCodexHarnessWorkerEntrypoint.js":
69623
- return join24(dirname17(entrypoint), "index.ts");
71257
+ return join25(dirname18(entrypoint), "index.ts");
69624
71258
  default:
69625
71259
  return entrypoint;
69626
71260
  }
69627
71261
  }
69628
71262
  function writeEphemeralMcpAuthFile2(options) {
69629
- const directory = mkdtempSync5(join24(tmpdir4(), "linzumi-mcp-auth-"));
71263
+ const directory = mkdtempSync5(join25(tmpdir4(), "linzumi-mcp-auth-"));
69630
71264
  chmodSync3(directory, 448);
69631
- const path2 = join24(directory, "auth.json");
71265
+ const path2 = join25(directory, "auth.json");
69632
71266
  try {
69633
71267
  writeCachedLocalRunnerToken({
69634
71268
  kandanUrl: options.kandanUrl,
@@ -70579,7 +72213,7 @@ async function parseAgentRunnerArgs(args, deps = {
70579
72213
  };
70580
72214
  }
70581
72215
  function readAgentTokenTextFile(path2) {
70582
- return existsSync18(path2) ? readFileSync23(path2, "utf8") : void 0;
72216
+ return existsSync18(path2) ? readFileSync24(path2, "utf8") : void 0;
70583
72217
  }
70584
72218
  function rejectAgentRunnerTargetingFlags(values) {
70585
72219
  const unsupportedFlags = [
@@ -70898,10 +72532,10 @@ function rejectStartTargetingFlags(values) {
70898
72532
  }
70899
72533
  function resolveUserPath(pathValue) {
70900
72534
  if (pathValue === "~") {
70901
- return homedir18();
72535
+ return homedir19();
70902
72536
  }
70903
72537
  if (pathValue.startsWith("~/")) {
70904
- return resolve11(homedir18(), pathValue.slice(2));
72538
+ return resolve11(homedir19(), pathValue.slice(2));
70905
72539
  }
70906
72540
  return resolve11(pathValue);
70907
72541
  }