@linzumi/cli 0.0.94-beta → 0.0.96-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 +193 -31
  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.94-beta --version
61
+ npx -y @linzumi/cli@0.0.96-beta --version
62
62
  linzumi --version
63
63
  ```
64
64
 
package/dist/index.js CHANGED
@@ -112,6 +112,14 @@ import { spawnSync } from "node:child_process";
112
112
  import { homedir } from "node:os";
113
113
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
114
114
  import { basename, dirname, join } from "node:path";
115
+ function projectScanBudgetMs() {
116
+ const raw = process.env.LINZUMI_ONBOARDING_PROJECT_SCAN_BUDGET_MS?.trim();
117
+ if (raw === void 0 || raw === "") {
118
+ return defaultProjectScanBudgetMs;
119
+ }
120
+ const parsed = Number.parseInt(raw, 10);
121
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultProjectScanBudgetMs;
122
+ }
115
123
  function discoverCurrentGitProject(args) {
116
124
  const startedAtMs = args.nowMs ?? Date.now();
117
125
  const responsiveness = args.placeResponsiveness;
@@ -136,7 +144,19 @@ function discoverCurrentGitProject(args) {
136
144
  });
137
145
  const sourceRoots = args.sourceRoots ?? defaultLocalProjectSourceRoots(args.cwd);
138
146
  let placesSearched = 1;
147
+ const scanBudgetMs = projectScanBudgetMs();
139
148
  for (const sourceRoot of sourceRoots) {
149
+ if (Date.now() - startedAtMs >= scanBudgetMs) {
150
+ searchStats.push({
151
+ place: sourceRoot,
152
+ source: "git",
153
+ duration_ms: 0,
154
+ result_count: 0,
155
+ skipped_scan_budget_exceeded: true
156
+ });
157
+ placesSearched += 1;
158
+ continue;
159
+ }
140
160
  const sourceRootStartedAtMs = Date.now();
141
161
  const rootResponsive = isPlaceResponsive(sourceRoot, responsiveness);
142
162
  if (!rootResponsive) {
@@ -296,6 +316,9 @@ function isGitWorktreeRoot(path2) {
296
316
  return existsSync(join(path2, ".git"));
297
317
  }
298
318
  function defaultLocalProjectSourceRoots(cwd) {
319
+ if (cwd === dirname(cwd)) {
320
+ return [];
321
+ }
299
322
  return uniqueStrings([
300
323
  ...ancestorSourceRoots(cwd),
301
324
  ...homeSourceRoots(),
@@ -409,13 +432,14 @@ function compactJsonObject(value) {
409
432
  })
410
433
  );
411
434
  }
412
- var worktreePathSampleLimit, gitSpawnTimeoutMs;
435
+ var worktreePathSampleLimit, gitSpawnTimeoutMs, defaultProjectScanBudgetMs;
413
436
  var init_onboardingProjectDiscovery = __esm({
414
437
  "src/onboardingProjectDiscovery.ts"() {
415
438
  "use strict";
416
439
  init_onboardingPlaceResponsiveness();
417
440
  worktreePathSampleLimit = 25;
418
441
  gitSpawnTimeoutMs = 4e3;
442
+ defaultProjectScanBudgetMs = 2e4;
419
443
  }
420
444
  });
421
445
 
@@ -8437,6 +8461,38 @@ var init_userFacingErrors = __esm({
8437
8461
 
8438
8462
  // src/channelSession.ts
8439
8463
  import { createHash as createHash3, randomUUID } from "node:crypto";
8464
+ function codexStartRequestTimeoutMs() {
8465
+ const raw = process.env.LINZUMI_CODEX_START_REQUEST_TIMEOUT_MS?.trim();
8466
+ if (raw === void 0 || raw === "") {
8467
+ return CODEX_START_REQUEST_TIMEOUT_MS;
8468
+ }
8469
+ const parsed = Number.parseInt(raw, 10);
8470
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : CODEX_START_REQUEST_TIMEOUT_MS;
8471
+ }
8472
+ function isCodexStartRequestTimeoutError(error) {
8473
+ return error instanceof CodexStartRequestTimeoutError;
8474
+ }
8475
+ function withCodexStartRequestTimeout(method, request, timeoutMs = codexStartRequestTimeoutMs()) {
8476
+ if (!(timeoutMs > 0)) {
8477
+ return request;
8478
+ }
8479
+ return new Promise((resolve12, reject) => {
8480
+ const timer = setTimeout(() => {
8481
+ reject(new CodexStartRequestTimeoutError(method, timeoutMs));
8482
+ }, timeoutMs);
8483
+ timer.unref?.();
8484
+ request.then(
8485
+ (value) => {
8486
+ clearTimeout(timer);
8487
+ resolve12(value);
8488
+ },
8489
+ (error) => {
8490
+ clearTimeout(timer);
8491
+ reject(error);
8492
+ }
8493
+ );
8494
+ });
8495
+ }
8440
8496
  async function attachChannelSession(args) {
8441
8497
  const session = args.options.channelSession;
8442
8498
  const chatTopic = `chat:${session.workspaceSlug}:${session.channelSlug}`;
@@ -8941,6 +8997,7 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
8941
8997
  failedSeqs: /* @__PURE__ */ new Map(),
8942
8998
  drainInFlight: false,
8943
8999
  drainRequested: false,
9000
+ boundStartTimeout: false,
8944
9001
  fusedQueuedSeqs: /* @__PURE__ */ new Map(),
8945
9002
  reconnectRetryTimer: void 0,
8946
9003
  reconnectRetryAttempts: 0,
@@ -10295,6 +10352,9 @@ async function startThreadMessageTurn(args, state, payloadContext, message) {
10295
10352
  if (state.kandanThreadId === void 0 || state.codexThreadId === void 0) {
10296
10353
  throw new Error("cannot start a local Codex turn before thread binding");
10297
10354
  }
10355
+ if (message.boundStartTimeout === true) {
10356
+ state.boundStartTimeout = true;
10357
+ }
10298
10358
  const queued = {
10299
10359
  seq: message.seq,
10300
10360
  actorSlug: message.actorSlug,
@@ -10558,7 +10618,9 @@ async function drainKandanMessageQueueOnce(args, state, payloadContext) {
10558
10618
  if (state.kandanThreadId !== void 0) {
10559
10619
  startCodexTypingHeartbeat(args, state, state.kandanThreadId);
10560
10620
  }
10561
- const started = await args.codex.request("turn/start", {
10621
+ const boundStartTimeout = state.boundStartTimeout;
10622
+ state.boundStartTimeout = false;
10623
+ const turnStartRequest = args.codex.request("turn/start", {
10562
10624
  threadId: codexThreadId,
10563
10625
  input: await codexInputItemsForQueuedKandanMessage(
10564
10626
  args,
@@ -10569,6 +10631,7 @@ async function drainKandanMessageQueueOnce(args, state, payloadContext) {
10569
10631
  runtimeOptionsForSettings(args.options, state.runtimeSettings)
10570
10632
  )
10571
10633
  });
10634
+ const started = boundStartTimeout ? await withCodexStartRequestTimeout("turn/start", turnStartRequest) : await turnStartRequest;
10572
10635
  const turnId = extractTurnIdFromResponse(started);
10573
10636
  state.pendingReconnectContextInjection = void 0;
10574
10637
  const interruptAfterStart = state.turn.status === "starting" && state.turn.interruptAfterStart;
@@ -10641,6 +10704,9 @@ async function drainKandanMessageQueueOnce(args, state, payloadContext) {
10641
10704
  queued_seq: next.seq,
10642
10705
  message: error instanceof Error ? error.message : String(error)
10643
10706
  });
10707
+ if (isCodexStartRequestTimeoutError(error)) {
10708
+ throw error;
10709
+ }
10644
10710
  }
10645
10711
  }
10646
10712
  function drainKandanMessageQueueBlocked(args, state) {
@@ -11654,7 +11720,7 @@ async function pushOptional(kandan, topic, event, payload, log2) {
11654
11720
  });
11655
11721
  }
11656
11722
  }
11657
- var codexTypingHeartbeatMs, maxTrackedFailedChatEventSeqs, maxChatEventFailureAttempts, maxForwardedTurnIds, maxClaimedKandanMessageKeys, claimedKandanMessageKeys, nextChatJoinRegistrationSerial, defaultReconnectHandlingRetryDelaysMs, defaultTuiInputMirrorRetryDelaysMs, pipelineCloseTimeoutMs;
11723
+ var codexTypingHeartbeatMs, CODEX_START_REQUEST_TIMEOUT_MS, CodexStartRequestTimeoutError, maxTrackedFailedChatEventSeqs, maxChatEventFailureAttempts, maxForwardedTurnIds, maxClaimedKandanMessageKeys, claimedKandanMessageKeys, nextChatJoinRegistrationSerial, defaultReconnectHandlingRetryDelaysMs, defaultTuiInputMirrorRetryDelaysMs, pipelineCloseTimeoutMs;
11658
11724
  var init_channelSession = __esm({
11659
11725
  "src/channelSession.ts"() {
11660
11726
  "use strict";
@@ -11679,6 +11745,15 @@ var init_channelSession = __esm({
11679
11745
  init_processNameCatalog();
11680
11746
  init_userFacingErrors();
11681
11747
  codexTypingHeartbeatMs = 5e3;
11748
+ CODEX_START_REQUEST_TIMEOUT_MS = 12e4;
11749
+ CodexStartRequestTimeoutError = class extends Error {
11750
+ constructor(method, timeoutMs) {
11751
+ super(
11752
+ `Codex ${method} did not reply within ${timeoutMs}ms (start request timed out)`
11753
+ );
11754
+ this.name = "CodexStartRequestTimeoutError";
11755
+ }
11756
+ };
11682
11757
  maxTrackedFailedChatEventSeqs = 256;
11683
11758
  maxChatEventFailureAttempts = 5;
11684
11759
  maxForwardedTurnIds = 64;
@@ -19376,7 +19451,7 @@ var linzumiCliVersion, linzumiCliVersionText;
19376
19451
  var init_version = __esm({
19377
19452
  "src/version.ts"() {
19378
19453
  "use strict";
19379
- linzumiCliVersion = "0.0.94-beta";
19454
+ linzumiCliVersion = "0.0.96-beta";
19380
19455
  linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
19381
19456
  }
19382
19457
  });
@@ -22265,6 +22340,11 @@ function connectThreadCodexWorkerIpc(child) {
22265
22340
  onRequest: (callback) => {
22266
22341
  requestCallbacks.add(callback);
22267
22342
  },
22343
+ // The worker child exited/disconnected (onClosed) or close() was called.
22344
+ // A reconnect uses this to detect a DEAD cached client and respawn a fresh
22345
+ // worker instead of send()-ing on a closed channel (which throws "thread
22346
+ // Codex worker IPC client is closed" and strands the resume).
22347
+ isClosed: () => state.closed || child.connected === false,
22268
22348
  close: () => {
22269
22349
  state.closed = true;
22270
22350
  child.off("message", onMessage);
@@ -23035,6 +23115,34 @@ async function runThreadCodexWorker(options) {
23035
23115
  async function openLocalCodexRunner(options, log2, cleanup, close) {
23036
23116
  const agentProviders = availableRunnerAgentProviders(options);
23037
23117
  const localCodexAppServerAvailable = hasLocalCodexAppServerCapability(options);
23118
+ const dependencyStatusRef = {
23119
+ value: options.dependencyStatus
23120
+ };
23121
+ const refreshDependencyStatus = async () => {
23122
+ if (dependencyStatusRef.value === void 0) {
23123
+ return;
23124
+ }
23125
+ try {
23126
+ const refreshed = await buildRunnerDependencyStatus({
23127
+ cwd: options.cwd,
23128
+ codexBin: options.codexBin,
23129
+ codeServerBin: options.codeServerBin,
23130
+ editorRuntime: dependencyStatusRef.value?.editorRuntime
23131
+ });
23132
+ const previousCodexAvailable = dependencyStatusRef.value?.codex.available;
23133
+ dependencyStatusRef.value = refreshed;
23134
+ log2("runner.dependency_status_reprobed", {
23135
+ runnerId: options.runnerId,
23136
+ codex_available: refreshed.codex.available,
23137
+ codex_available_changed: previousCodexAvailable !== refreshed.codex.available
23138
+ });
23139
+ } catch (error) {
23140
+ log2("runner.dependency_status_reprobe_failed", {
23141
+ runnerId: options.runnerId,
23142
+ message: error instanceof Error ? error.message : String(error)
23143
+ });
23144
+ }
23145
+ };
23038
23146
  const allowedForwardPorts = options.allowedForwardPorts ?? [];
23039
23147
  const liveForwardPorts = new Set(allowedForwardPorts);
23040
23148
  const managedForwardPorts = /* @__PURE__ */ new Set();
@@ -23207,8 +23315,8 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
23207
23315
  ),
23208
23316
  forwardedPortAttributions: buildForwardPortAttributionPayload(),
23209
23317
  pendingEditorForwardPortRequests: pendingEditorForwardPortRequests(),
23210
- toolStatus: options.dependencyStatus === void 0 ? null : dependencyStatusPayload(options.dependencyStatus),
23211
- editorRuntime: options.dependencyStatus?.editorRuntime === void 0 ? null : dependencyStatusPayload(options.dependencyStatus).editorRuntime,
23318
+ toolStatus: dependencyStatusRef.value === void 0 ? null : dependencyStatusPayload(dependencyStatusRef.value),
23319
+ editorRuntime: dependencyStatusRef.value?.editorRuntime === void 0 ? null : dependencyStatusPayload(dependencyStatusRef.value).editorRuntime,
23212
23320
  ...localEditorCapabilities(
23213
23321
  options.editorRuntime,
23214
23322
  allowedCwds.value,
@@ -24221,6 +24329,35 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
24221
24329
  );
24222
24330
  threadRunnerProcesses.clear();
24223
24331
  });
24332
+ const evictStaleDynamicChannelSession = async (codexThreadId, expectClient) => {
24333
+ const session = dynamicChannelSessions.get(codexThreadId);
24334
+ const boundClient = dynamicChannelSessionCodexClients.get(codexThreadId);
24335
+ if (session === void 0) {
24336
+ return false;
24337
+ }
24338
+ if (expectClient !== void 0 && boundClient !== expectClient) {
24339
+ return false;
24340
+ }
24341
+ if (boundClient?.isClosed?.() !== true) {
24342
+ return false;
24343
+ }
24344
+ dynamicChannelSessions.delete(codexThreadId);
24345
+ dynamicChannelSessionCodexClients.delete(codexThreadId);
24346
+ dynamicChannelSessionAttachInFlight.delete(codexThreadId);
24347
+ log2("runner.thread_session_evicted_dead_worker", {
24348
+ codex_thread_id: codexThreadId,
24349
+ kandan_thread_id: session.currentKandanThreadId() ?? null
24350
+ });
24351
+ try {
24352
+ await session.close();
24353
+ } catch (error) {
24354
+ log2("runner.thread_session_evict_close_failed", {
24355
+ codex_thread_id: codexThreadId,
24356
+ message: error instanceof Error ? error.message : String(error)
24357
+ });
24358
+ }
24359
+ return true;
24360
+ };
24224
24361
  const attachThreadSessionWithCodex = async (args) => {
24225
24362
  const { control, cwd, codexThreadId, sessionCodex } = args;
24226
24363
  const portForwardWatcherRootPid = args.portForwardWatcherRootPid;
@@ -24232,7 +24369,10 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
24232
24369
  }
24233
24370
  const existingSession = dynamicChannelSessions.get(codexThreadId);
24234
24371
  if (existingSession !== void 0) {
24235
- return existingSession;
24372
+ const evicted = await evictStaleDynamicChannelSession(codexThreadId);
24373
+ if (!evicted) {
24374
+ return existingSession;
24375
+ }
24236
24376
  }
24237
24377
  const alreadyAttaching = dynamicChannelSessionAttachInFlight.get(codexThreadId);
24238
24378
  if (alreadyAttaching !== void 0) {
@@ -24538,6 +24678,18 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
24538
24678
  threadRunnerProcesses.delete(kandanThreadId);
24539
24679
  }
24540
24680
  });
24681
+ handle.onExit?.(() => {
24682
+ if (cleanup.closePromise !== void 0) {
24683
+ return;
24684
+ }
24685
+ for (const [codexThreadId, client] of Array.from(
24686
+ dynamicChannelSessionCodexClients.entries()
24687
+ )) {
24688
+ if (client === threadCodex) {
24689
+ void evictStaleDynamicChannelSession(codexThreadId, threadCodex);
24690
+ }
24691
+ }
24692
+ });
24541
24693
  log2("runner.thread_process_started", {
24542
24694
  kandanThreadId,
24543
24695
  cwd
@@ -25071,14 +25223,15 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
25071
25223
  log2,
25072
25224
  pendingControls,
25073
25225
  controlDispatcher,
25074
- dispatchControl
25226
+ dispatchControl,
25227
+ refreshDependencyStatus
25075
25228
  );
25076
25229
  return { instanceId, codexUrl, close };
25077
25230
  }
25078
- function finishRunnerStartup(options, log2, pendingControls, controlDispatcher, dispatchControl) {
25231
+ function finishRunnerStartup(options, log2, pendingControls, controlDispatcher, dispatchControl, onOnboardingDiscoveryComplete) {
25079
25232
  controlDispatcher.value = dispatchControl;
25080
25233
  pendingControls.splice(0).forEach(({ control, ack }) => dispatchControl(control, ack));
25081
- startOnboardingDiscoveryAgents(options, log2);
25234
+ startOnboardingDiscoveryAgents(options, log2, onOnboardingDiscoveryComplete);
25082
25235
  }
25083
25236
  function commanderOutboxPersistDir() {
25084
25237
  const override = process.env.LINZUMI_COMMANDER_OUTBOX_DIR?.trim();
@@ -27175,22 +27328,25 @@ async function startCodexProviderInstance(args) {
27175
27328
  args.options,
27176
27329
  args.control
27177
27330
  );
27178
- const response = await args.codex.request("thread/start", {
27179
- cwd: args.cwd,
27180
- serviceName: "kandan-local-runner",
27181
- personality: "pragmatic",
27182
- developerInstructions: commanderDeveloperInstructions({
27331
+ const response = await withCodexStartRequestTimeout(
27332
+ "thread/start",
27333
+ args.codex.request("thread/start", {
27183
27334
  cwd: args.cwd,
27184
- planTool: "linzumi-mcp",
27185
- developerPrompt,
27186
- linzumiContext
27187
- }),
27188
- ...runtimeSettings.model === void 0 ? {} : { model: runtimeSettings.model },
27189
- ...runtimeSettings.reasoningEffort === void 0 ? {} : { reasoningEffort: runtimeSettings.reasoningEffort },
27190
- ...runtimeSettings.approvalPolicy === void 0 ? {} : { approvalPolicy: runtimeSettings.approvalPolicy },
27191
- ...runtimeSettings.sandbox === void 0 ? {} : { sandbox: runtimeSettings.sandbox },
27192
- ...runtimeSettings.fast === true ? { serviceTier: "fast" } : {}
27193
- });
27335
+ serviceName: "kandan-local-runner",
27336
+ personality: "pragmatic",
27337
+ developerInstructions: commanderDeveloperInstructions({
27338
+ cwd: args.cwd,
27339
+ planTool: "linzumi-mcp",
27340
+ developerPrompt,
27341
+ linzumiContext
27342
+ }),
27343
+ ...runtimeSettings.model === void 0 ? {} : { model: runtimeSettings.model },
27344
+ ...runtimeSettings.reasoningEffort === void 0 ? {} : { reasoningEffort: runtimeSettings.reasoningEffort },
27345
+ ...runtimeSettings.approvalPolicy === void 0 ? {} : { approvalPolicy: runtimeSettings.approvalPolicy },
27346
+ ...runtimeSettings.sandbox === void 0 ? {} : { sandbox: runtimeSettings.sandbox },
27347
+ ...runtimeSettings.fast === true ? { serviceTier: "fast" } : {}
27348
+ })
27349
+ );
27194
27350
  const codexThreadId = extractStartedThreadId(response);
27195
27351
  args.setStartedCodexThreadId(codexThreadId);
27196
27352
  const workDescription = normalizedWorkDescription(
@@ -27224,13 +27380,17 @@ async function startCodexProviderInstance(args) {
27224
27380
  seq: sourceSeq,
27225
27381
  body: workDescription,
27226
27382
  actorSlug: identity.actorUsername,
27227
- actorUserId: identity.actorUserId
27383
+ actorUserId: identity.actorUserId,
27384
+ boundStartTimeout: true
27228
27385
  });
27229
27386
  } else {
27230
- await args.codex.request("turn/start", {
27231
- threadId: codexThreadId,
27232
- input: [{ type: "text", text: workDescription }]
27233
- });
27387
+ await withCodexStartRequestTimeout(
27388
+ "turn/start",
27389
+ args.codex.request("turn/start", {
27390
+ threadId: codexThreadId,
27391
+ input: [{ type: "text", text: workDescription }]
27392
+ })
27393
+ );
27234
27394
  }
27235
27395
  }
27236
27396
  return {
@@ -29130,9 +29290,10 @@ function redactedThreadRunnerCliArgs(args) {
29130
29290
  function optionalCliValue(flag, value) {
29131
29291
  return value === void 0 || value === "" ? [] : [flag, value];
29132
29292
  }
29133
- function startOnboardingDiscoveryAgents(options, log2) {
29293
+ function startOnboardingDiscoveryAgents(options, log2, onOnboardingDiscoveryComplete) {
29134
29294
  const workspaceSlug = runnerWorkspaceSlug(options);
29135
29295
  if (!shouldStartOnboardingDiscoveryAgents(options, workspaceSlug)) {
29296
+ void onOnboardingDiscoveryComplete?.();
29136
29297
  return;
29137
29298
  }
29138
29299
  const startKey = onboardingDiscoveryAgentStartKey(options, workspaceSlug);
@@ -29190,6 +29351,7 @@ function startOnboardingDiscoveryAgents(options, log2) {
29190
29351
  workers_completed: 2,
29191
29352
  successful_write_count: successfulWriteCount
29192
29353
  });
29354
+ await onOnboardingDiscoveryComplete?.();
29193
29355
  });
29194
29356
  }
29195
29357
  function withSuccessfulWriteCount(report, successfulPostStartWriteCount) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.94-beta",
3
+ "version": "0.0.96-beta",
4
4
  "description": "Linzumi CLI — 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": {