@linzumi/cli 0.0.105-beta → 0.0.107-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 +156 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -58,7 +58,7 @@ Install the CLI or run it with `npx`:
58
58
  ```bash
59
59
  npm install -g @linzumi/cli@latest
60
60
  npx -y @linzumi/cli@latest signup
61
- npx -y @linzumi/cli@0.0.105-beta --version
61
+ npx -y @linzumi/cli@0.0.107-beta --version
62
62
  linzumi --version
63
63
  ```
64
64
 
package/dist/index.js CHANGED
@@ -15076,6 +15076,15 @@ import { createServer } from "node:net";
15076
15076
  import { homedir as homedir8 } from "node:os";
15077
15077
  import { join as join10 } from "node:path";
15078
15078
  import { WebSocket as NodeWebSocket } from "ws";
15079
+ function pingWebSocketIfSupported(websocket) {
15080
+ const candidate = websocket;
15081
+ if (typeof candidate.ping === "function" && candidate.readyState === WebSocket.OPEN) {
15082
+ try {
15083
+ candidate.ping();
15084
+ } catch {
15085
+ }
15086
+ }
15087
+ }
15079
15088
  async function chooseLoopbackPort() {
15080
15089
  return new Promise((resolve12, reject) => {
15081
15090
  const server = createServer();
@@ -15363,7 +15372,13 @@ async function connectCodexAppServer(websocketUrl, socketFactory = (url) => new
15363
15372
  pending.forEach((pendingRequest) => pendingRequest.reject(error));
15364
15373
  pending.clear();
15365
15374
  };
15375
+ const keepalive = setInterval(
15376
+ () => pingWebSocketIfSupported(websocket),
15377
+ codexAppServerKeepaliveIntervalMs
15378
+ );
15379
+ keepalive.unref?.();
15366
15380
  const handleConnectionClosed = (message) => {
15381
+ clearInterval(keepalive);
15367
15382
  rejectPending(message);
15368
15383
  if (state.closeError !== void 0) {
15369
15384
  return;
@@ -15443,7 +15458,10 @@ async function connectCodexAppServer(websocketUrl, socketFactory = (url) => new
15443
15458
  }
15444
15459
  closeCallbacks.add(callback);
15445
15460
  },
15446
- close: () => websocket.close()
15461
+ close: () => {
15462
+ clearInterval(keepalive);
15463
+ websocket.close();
15464
+ }
15447
15465
  };
15448
15466
  }
15449
15467
  function parseCodexAppServerFrame(data) {
@@ -15653,7 +15671,7 @@ function readyzUrlForWebsocket(websocketUrl) {
15653
15671
  parsed.hash = "";
15654
15672
  return parsed.toString();
15655
15673
  }
15656
- var codexAppServerWatchdogPollMs, blockedCodexAppServerEnvKeys, codexDisableAppsConfigArgs;
15674
+ var codexAppServerWatchdogPollMs, codexAppServerKeepaliveIntervalMs, blockedCodexAppServerEnvKeys, codexDisableAppsConfigArgs;
15657
15675
  var init_codexAppServer = __esm({
15658
15676
  "src/codexAppServer.ts"() {
15659
15677
  "use strict";
@@ -15663,6 +15681,14 @@ var init_codexAppServer = __esm({
15663
15681
  init_engineChildReaper();
15664
15682
  init_engineParentDeathWatchdog();
15665
15683
  codexAppServerWatchdogPollMs = 2e3;
15684
+ codexAppServerKeepaliveIntervalMs = (() => {
15685
+ const raw = process.env.LINZUMI_CODEX_WS_KEEPALIVE_MS;
15686
+ if (raw === void 0) {
15687
+ return 2e4;
15688
+ }
15689
+ const parsed = Number.parseInt(raw, 10);
15690
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 2e4;
15691
+ })();
15666
15692
  blockedCodexAppServerEnvKeys = [
15667
15693
  "LINZUMI_MCP_ACCESS_TOKEN",
15668
15694
  "LINZUMI_MCP_OWNER_USERNAME"
@@ -19972,7 +19998,7 @@ var linzumiCliVersion, linzumiCliVersionText;
19972
19998
  var init_version = __esm({
19973
19999
  "src/version.ts"() {
19974
20000
  "use strict";
19975
- linzumiCliVersion = "0.0.105-beta";
20001
+ linzumiCliVersion = "0.0.107-beta";
19976
20002
  linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
19977
20003
  }
19978
20004
  });
@@ -22702,6 +22728,9 @@ var init_authCache = __esm({
22702
22728
  });
22703
22729
 
22704
22730
  // src/threadCodexWorkerIpc.ts
22731
+ function isThreadCodexWorkerSessionClosedMessage(message) {
22732
+ return isJsonObject(message) && message.type === "linzumi_thread_codex_worker_session_closed";
22733
+ }
22705
22734
  function bindThreadCodexWorkerIpc(codex, ipc = process) {
22706
22735
  const pendingServerRequests = /* @__PURE__ */ new Map();
22707
22736
  const state = { nextServerRequestId: 1, sendDropLogged: false };
@@ -23654,15 +23683,33 @@ async function runThreadCodexWorker(options) {
23654
23683
  const log2 = makeRunnerLogger(options);
23655
23684
  const started = await startOwnedCodexAppServer(options);
23656
23685
  const codex = await connectCodexAppServer(started.url);
23686
+ const kandanThreadId = options.threadProcess.kandanThreadId;
23657
23687
  const stop = once(() => {
23658
23688
  codex.close();
23659
23689
  started.stop();
23660
23690
  log2.close();
23661
23691
  });
23662
23692
  bindThreadCodexWorkerIpc(codex);
23693
+ codex.onClose?.((error) => {
23694
+ log2("runner.thread_codex_worker_session_closed", {
23695
+ kandanThreadId,
23696
+ reason: error.message
23697
+ });
23698
+ if (process.send !== void 0 && process.connected !== false) {
23699
+ try {
23700
+ process.send({
23701
+ type: "linzumi_thread_codex_worker_session_closed",
23702
+ kandanThreadId,
23703
+ reason: error.message
23704
+ });
23705
+ } catch {
23706
+ }
23707
+ }
23708
+ stop();
23709
+ });
23663
23710
  process.send({
23664
23711
  type: "linzumi_thread_codex_worker_ready",
23665
- kandanThreadId: options.threadProcess.kandanThreadId,
23712
+ kandanThreadId,
23666
23713
  commanderManagedPorts: commanderManagedPortsForStartedCodex(started)
23667
23714
  });
23668
23715
  await waitForThreadCodexWorkerStop(started.process);
@@ -25472,6 +25519,17 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25472
25519
  (port) => port.port
25473
25520
  )
25474
25521
  });
25522
+ const evictOnFailedResult = async (result) => {
25523
+ if (result !== void 0 && result.ok !== false) {
25524
+ return result;
25525
+ }
25526
+ const entry = threadRunnerProcesses.get(kandanThreadId);
25527
+ if (entry === startingEntry || entry?.kind === "running") {
25528
+ threadRunnerProcesses.delete(kandanThreadId);
25529
+ }
25530
+ await (entry?.kind === "running" ? entry.handle.close() : handle.close()).catch(() => void 0);
25531
+ return result;
25532
+ };
25475
25533
  switch (control.type) {
25476
25534
  case "start_instance": {
25477
25535
  const result = await startCodexProviderInstance({
@@ -25487,27 +25545,29 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25487
25545
  setStartupStage: () => void 0,
25488
25546
  setStartedCodexThreadId: () => void 0
25489
25547
  });
25490
- return result.controlResponse;
25548
+ return await evictOnFailedResult(result.controlResponse);
25491
25549
  }
25492
25550
  case "reconnect_thread":
25493
- return await applyControl(
25494
- threadCodex,
25495
- kandan,
25496
- topic,
25497
- instanceId,
25498
- options,
25499
- agentProviders,
25500
- allowedCwds,
25501
- remoteCodexSandboxRunner,
25502
- activeRemoteCodexExecRequests,
25503
- activeClaudeCodeSessions,
25504
- pendingClaudeCodeApprovals,
25505
- ensureClaudeCodeForwardPortSession,
25506
- disposeClaudeCodeForwardPortSession,
25507
- control,
25508
- log2,
25509
- onStartedThread,
25510
- void 0
25551
+ return await evictOnFailedResult(
25552
+ await applyControl(
25553
+ threadCodex,
25554
+ kandan,
25555
+ topic,
25556
+ instanceId,
25557
+ options,
25558
+ agentProviders,
25559
+ allowedCwds,
25560
+ remoteCodexSandboxRunner,
25561
+ activeRemoteCodexExecRequests,
25562
+ activeClaudeCodeSessions,
25563
+ pendingClaudeCodeApprovals,
25564
+ ensureClaudeCodeForwardPortSession,
25565
+ disposeClaudeCodeForwardPortSession,
25566
+ control,
25567
+ log2,
25568
+ onStartedThread,
25569
+ void 0
25570
+ )
25511
25571
  );
25512
25572
  }
25513
25573
  } catch (error) {
@@ -25520,6 +25580,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25520
25580
  }
25521
25581
  };
25522
25582
  const spawnOnMissInFlight = /* @__PURE__ */ new Map();
25583
+ const spawnOnMissHandledMessages = /* @__PURE__ */ new Map();
25523
25584
  const replaySpawnOnMissMessageOnLiveSession = async (event, record, reconnectControl) => {
25524
25585
  try {
25525
25586
  log2("runner.spawn_on_miss_replay_on_sibling", {
@@ -25564,13 +25625,24 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25564
25625
  }
25565
25626
  };
25566
25627
  const handleUnroutedThreadMessage = async (event) => {
25567
- if (!shouldUseThreadProcesses(options)) {
25568
- return "ignored";
25569
- }
25570
25628
  const kandanThreadId = event.threadId;
25571
- if (kandanThreadId === void 0) {
25629
+ if (!shouldUseThreadProcesses(options) || kandanThreadId === void 0) {
25572
25630
  return "ignored";
25573
25631
  }
25632
+ const messageKey = `${kandanThreadId}:${event.seq}`;
25633
+ const inFlightMessage = spawnOnMissHandledMessages.get(messageKey);
25634
+ if (inFlightMessage !== void 0) {
25635
+ return await inFlightMessage;
25636
+ }
25637
+ const run = handleUnroutedThreadMessageOnce(event, kandanThreadId);
25638
+ spawnOnMissHandledMessages.set(messageKey, run);
25639
+ try {
25640
+ return await run;
25641
+ } finally {
25642
+ spawnOnMissHandledMessages.delete(messageKey);
25643
+ }
25644
+ };
25645
+ const handleUnroutedThreadMessageOnce = async (event, kandanThreadId) => {
25574
25646
  const existing = threadRunnerProcesses.get(kandanThreadId);
25575
25647
  const existingLiveness = existing === void 0 ? void 0 : threadRunnerEntryLiveness(existing);
25576
25648
  if (existingLiveness === "live") {
@@ -25586,6 +25658,12 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25586
25658
  ...baseControl,
25587
25659
  sourceSeq: event.seq,
25588
25660
  workDescription: event.body,
25661
+ // Carry the user's attachments on the reconstructed control (Codex P2) so
25662
+ // the FIRST spawn-on-miss message - the one that owns the respawn and runs
25663
+ // its turn inside startThreadRunnerProcess, not via the replay helper -
25664
+ // still resumes with its files/images. Re-serialized to the wire shape the
25665
+ // resume turn parses (parseKandanChatAttachments).
25666
+ ...event.attachments.length === 0 ? {} : { attachments: kandanChatAttachmentsToWire(event.attachments) },
25589
25667
  ...providerBinding?.modelProvider === void 0 ? {} : { modelProvider: providerBinding.modelProvider },
25590
25668
  ...providerBinding?.llmProxy === void 0 ? {} : { llmProxy: providerBinding.llmProxy }
25591
25669
  };
@@ -25658,6 +25736,23 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25658
25736
  );
25659
25737
  return "spawned";
25660
25738
  }
25739
+ if (startResult === void 0 || startResult.ok === false) {
25740
+ spawnSettle("no_session");
25741
+ const startError = startResult !== void 0 && typeof startResult.error === "string" ? startResult.error : "the thread could not be resumed (no resumable codex session)";
25742
+ log2("runner.spawn_on_miss_failed", {
25743
+ kandanThreadId,
25744
+ codex_thread_id: record.codexThreadId,
25745
+ seq: event.seq,
25746
+ message: startError
25747
+ });
25748
+ await publishSpawnOnMissMessageState(
25749
+ reconnectControl,
25750
+ "failed",
25751
+ `could not resume the thread's coding session: ${startError}`,
25752
+ record.codexThreadId
25753
+ );
25754
+ return "spawned";
25755
+ }
25661
25756
  spawnSettle("spawned");
25662
25757
  return "spawned";
25663
25758
  } catch (error) {
@@ -27717,11 +27812,15 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
27717
27812
  })
27718
27813
  );
27719
27814
  const identity = identityFromAccessToken(options.token);
27815
+ const reconnectAttachments = parseKandanChatAttachments(
27816
+ arrayValue(control.attachments) ?? []
27817
+ );
27720
27818
  await startedThreadSession.startThreadMessageTurn({
27721
27819
  seq: sourceSeq,
27722
27820
  body: workDescription,
27723
27821
  actorSlug: identity.actorUsername,
27724
27822
  actorUserId: identity.actorUserId,
27823
+ attachments: reconnectAttachments,
27725
27824
  boundStartTimeout: true
27726
27825
  });
27727
27826
  }
@@ -28396,6 +28495,9 @@ async function startCodexProviderInstance(args) {
28396
28495
  args.setStartupStage("starting_first_turn");
28397
28496
  const rootSeq = integerValue(args.control.rootSeq);
28398
28497
  const sourceSeq = integerValue(args.control.sourceSeq) ?? rootSeq;
28498
+ const firstTurnAttachments = parseKandanChatAttachments(
28499
+ arrayValue(args.control.attachments) ?? []
28500
+ );
28399
28501
  if (startedThreadSession !== void 0 && sourceSeq !== void 0) {
28400
28502
  const identity = identityFromAccessToken(args.options.token);
28401
28503
  await startedThreadSession.startThreadMessageTurn({
@@ -28403,6 +28505,7 @@ async function startCodexProviderInstance(args) {
28403
28505
  body: workDescription,
28404
28506
  actorSlug: identity.actorUsername,
28405
28507
  actorUserId: identity.actorUserId,
28508
+ attachments: firstTurnAttachments,
28406
28509
  boundStartTimeout: true
28407
28510
  });
28408
28511
  } else {
@@ -29761,6 +29864,16 @@ function rehydrationControlSnapshot(args) {
29761
29864
  ...runtimeSettings.fast === void 0 ? {} : { fast: runtimeSettings.fast }
29762
29865
  };
29763
29866
  }
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
+ }));
29876
+ }
29764
29877
  function rehydrationReconnectControl(record) {
29765
29878
  const snapshot = record.control;
29766
29879
  const rootSeq = integerValue(snapshot.rootSeq);
@@ -29972,6 +30085,9 @@ function threadRunnerEntryLiveness(entry) {
29972
30085
  if (ipcClosed) {
29973
30086
  return "dead";
29974
30087
  }
30088
+ if (handle.sessionClosed?.() === true) {
30089
+ return "dead";
30090
+ }
29975
30091
  return processIsAlive2(handle.processPid) ? "live" : "dead";
29976
30092
  }
29977
30093
  async function closeThreadRunnerEntry(entry) {
@@ -30029,6 +30145,18 @@ async function spawnLocalThreadRunnerProcess(options) {
30029
30145
  });
30030
30146
  const exitListeners = /* @__PURE__ */ new Set();
30031
30147
  const exitState = { exited: false };
30148
+ const sessionState = { closed: false };
30149
+ child.on("message", (message) => {
30150
+ if (isThreadCodexWorkerSessionClosedMessage(message)) {
30151
+ sessionState.closed = true;
30152
+ writeCliAuditEvent("runner.thread_runner_session_detached", {
30153
+ purpose: "linzumi.thread_runner",
30154
+ kandanThreadId: message.kandanThreadId,
30155
+ reason: message.reason,
30156
+ pid: child.pid
30157
+ });
30158
+ }
30159
+ });
30032
30160
  child.once("exit", (code, signal) => {
30033
30161
  writeCliAuditEvent("process.exit", {
30034
30162
  command: process.execPath,
@@ -30104,6 +30232,7 @@ async function spawnLocalThreadRunnerProcess(options) {
30104
30232
  codex: connectThreadCodexWorkerIpc(child),
30105
30233
  processPid: child.pid,
30106
30234
  commanderManagedPorts: ready2.commanderManagedPorts,
30235
+ sessionClosed: () => sessionState.closed,
30107
30236
  onExit: (listener) => {
30108
30237
  if (exitState.exited) {
30109
30238
  queueMicrotask(listener);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.105-beta",
3
+ "version": "0.0.107-beta",
4
4
  "description": "Linzumi CLI \u2014 point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {