@linzumi/cli 0.0.84-beta → 0.0.86-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 +1344 -491
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -39,6 +39,70 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
39
39
  mod
40
40
  ));
41
41
 
42
+ // src/onboardingPlaceResponsiveness.ts
43
+ import { Worker } from "node:worker_threads";
44
+ function isPlaceResponsive(path2, deps = {}) {
45
+ const timeoutMs = deps.timeoutMs ?? defaultPlaceProbeTimeoutMs;
46
+ if (deps.probe !== void 0) {
47
+ return deps.probe(path2, timeoutMs);
48
+ }
49
+ return statProbe(path2, timeoutMs);
50
+ }
51
+ function statProbe(path2, timeoutMs) {
52
+ const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
53
+ const status = new Int32Array(sharedBuffer);
54
+ Atomics.store(status, 0, probeStatusPending);
55
+ let worker;
56
+ try {
57
+ worker = new Worker(probeWorkerSource, {
58
+ eval: true,
59
+ workerData: { sharedBuffer, path: path2 }
60
+ });
61
+ } catch {
62
+ return true;
63
+ }
64
+ worker.unref();
65
+ worker.on("error", () => {
66
+ if (Atomics.load(status, 0) === probeStatusPending) {
67
+ Atomics.store(status, 0, probeStatusUnresponsive);
68
+ Atomics.notify(status, 0);
69
+ }
70
+ });
71
+ Atomics.wait(status, 0, probeStatusPending, timeoutMs);
72
+ const result = Atomics.load(status, 0);
73
+ void worker.terminate().catch(() => {
74
+ });
75
+ return result === probeStatusResponsive;
76
+ }
77
+ var defaultPlaceProbeTimeoutMs, probeStatusPending, probeStatusResponsive, probeStatusUnresponsive, probeWorkerSource;
78
+ var init_onboardingPlaceResponsiveness = __esm({
79
+ "src/onboardingPlaceResponsiveness.ts"() {
80
+ "use strict";
81
+ defaultPlaceProbeTimeoutMs = 1e3;
82
+ probeStatusPending = 0;
83
+ probeStatusResponsive = 1;
84
+ probeStatusUnresponsive = 2;
85
+ probeWorkerSource = `
86
+ const { workerData, parentPort } = require('node:worker_threads');
87
+ const { statSync } = require('node:fs');
88
+ const status = new Int32Array(workerData.sharedBuffer);
89
+ let result = ${probeStatusUnresponsive};
90
+ try {
91
+ statSync(workerData.path);
92
+ result = ${probeStatusResponsive};
93
+ } catch {
94
+ // A path that throws quickly (ENOENT/EACCES/ENOTDIR) is still a *responsive*
95
+ // volume - discovery's own existence checks will handle it. Only a wedged
96
+ // syscall (which never returns here) counts as unresponsive.
97
+ result = ${probeStatusResponsive};
98
+ }
99
+ Atomics.store(status, 0, result);
100
+ Atomics.notify(status, 0);
101
+ if (parentPort) parentPort.postMessage(result);
102
+ `;
103
+ }
104
+ });
105
+
42
106
  // src/onboardingProjectDiscovery.ts
43
107
  var onboardingProjectDiscovery_exports = {};
44
108
  __export(onboardingProjectDiscovery_exports, {
@@ -50,10 +114,12 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
50
114
  import { basename, dirname, join } from "node:path";
51
115
  function discoverCurrentGitProject(args) {
52
116
  const startedAtMs = args.nowMs ?? Date.now();
117
+ const responsiveness = args.placeResponsiveness;
53
118
  const candidates = /* @__PURE__ */ new Map();
54
119
  const searchStats = [];
120
+ const cwdResponsive = isPlaceResponsive(args.cwd, responsiveness);
55
121
  const gitStartedAtMs = Date.now();
56
- const topLevel = gitOutput(args.cwd, ["rev-parse", "--show-toplevel"]);
122
+ const topLevel = cwdResponsive ? gitOutput(args.cwd, ["rev-parse", "--show-toplevel"]) : void 0;
57
123
  const gitDurationMs = Date.now() - gitStartedAtMs;
58
124
  if (topLevel !== void 0) {
59
125
  mergeCandidate(
@@ -65,12 +131,28 @@ function discoverCurrentGitProject(args) {
65
131
  place: args.cwd,
66
132
  source: "git",
67
133
  duration_ms: gitDurationMs,
68
- result_count: topLevel === void 0 ? 0 : 1
134
+ result_count: topLevel === void 0 ? 0 : 1,
135
+ skipped_unresponsive: cwdResponsive ? void 0 : true
69
136
  });
70
137
  const sourceRoots = args.sourceRoots ?? defaultLocalProjectSourceRoots(args.cwd);
71
138
  let placesSearched = 1;
72
139
  for (const sourceRoot of sourceRoots) {
73
140
  const sourceRootStartedAtMs = Date.now();
141
+ const rootResponsive = isPlaceResponsive(sourceRoot, responsiveness);
142
+ if (!rootResponsive) {
143
+ searchStats.push({
144
+ place: sourceRoot,
145
+ source: "git",
146
+ duration_ms: Date.now() - sourceRootStartedAtMs,
147
+ result_count: 0,
148
+ skipped_unresponsive: true
149
+ });
150
+ placesSearched += 1;
151
+ continue;
152
+ }
153
+ if (!isDirectory(sourceRoot)) {
154
+ continue;
155
+ }
74
156
  const foundInRoot = discoverGitWorktreesInSourceRoot(sourceRoot);
75
157
  for (const worktreePath of foundInRoot) {
76
158
  mergeCandidate(
@@ -218,7 +300,7 @@ function defaultLocalProjectSourceRoots(cwd) {
218
300
  ...ancestorSourceRoots(cwd),
219
301
  ...homeSourceRoots(),
220
302
  ...externalVolumeSourceRoots()
221
- ]).filter(isDirectory);
303
+ ]);
222
304
  }
223
305
  function ancestorSourceRoots(cwd) {
224
306
  const sourceRootNames = /* @__PURE__ */ new Set([
@@ -331,6 +413,7 @@ var worktreePathSampleLimit, gitSpawnTimeoutMs;
331
413
  var init_onboardingProjectDiscovery = __esm({
332
414
  "src/onboardingProjectDiscovery.ts"() {
333
415
  "use strict";
416
+ init_onboardingPlaceResponsiveness();
334
417
  worktreePathSampleLimit = 25;
335
418
  gitSpawnTimeoutMs = 4e3;
336
419
  }
@@ -6977,6 +7060,7 @@ function startPortForwardWatcher(options) {
6977
7060
  const scanProcessCwds = options.scanProcessCwds ?? readProcessCwdRows;
6978
7061
  const nowMs = options.nowMs ?? Date.now;
6979
7062
  const commanderBoundPids = validPidSet(options.commanderBoundPids ?? []);
7063
+ const commanderBoundPorts = validPortSet(options.commanderBoundPorts ?? []);
6980
7064
  const candidateStabilityByPort = /* @__PURE__ */ new Map();
6981
7065
  const emittedByPort = /* @__PURE__ */ new Map();
6982
7066
  const missingByPort = /* @__PURE__ */ new Map();
@@ -6990,12 +7074,15 @@ function startPortForwardWatcher(options) {
6990
7074
  const descendants = descendantPidSet(scanProcesses(), rootPid);
6991
7075
  const sockets = scanListenSockets();
6992
7076
  const observedPids = /* @__PURE__ */ new Set([...descendants, ...commanderBoundPids]);
6993
- const candidatePids = sockets.filter((socket) => observedPids.has(socket.pid)).map((socket) => socket.pid);
7077
+ const candidatePids = sockets.filter(
7078
+ (socket) => observedPids.has(socket.pid) || commanderBoundPorts.has(socket.port)
7079
+ ).map((socket) => socket.pid);
6994
7080
  const candidates = detectedForwardCandidates(
6995
7081
  sockets,
6996
7082
  observedPids,
6997
7083
  scanProcessCwds(candidatePids),
6998
- commanderBoundPids
7084
+ commanderBoundPids,
7085
+ commanderBoundPorts
6999
7086
  );
7000
7087
  const scanTimeMs = nowMs();
7001
7088
  const stable = stableForwardCandidates(
@@ -7113,10 +7200,12 @@ function descendantPidSet(rows, rootPid) {
7113
7200
  }
7114
7201
  return descendants;
7115
7202
  }
7116
- function detectedForwardCandidates(sockets, descendantPids, processCwds = /* @__PURE__ */ new Map(), commanderBoundPids = /* @__PURE__ */ new Set()) {
7117
- return sockets.filter((socket) => descendantPids.has(socket.pid)).filter((socket) => socket.port > 0 && socket.port < 65536).sort((left, right) => left.port - right.port).map((socket) => {
7203
+ function detectedForwardCandidates(sockets, descendantPids, processCwds = /* @__PURE__ */ new Map(), commanderBoundPids = /* @__PURE__ */ new Set(), commanderBoundPorts = /* @__PURE__ */ new Set()) {
7204
+ return sockets.filter(
7205
+ (socket) => descendantPids.has(socket.pid) || commanderBoundPorts.has(socket.port)
7206
+ ).filter((socket) => socket.port > 0 && socket.port < 65536).sort((left, right) => left.port - right.port).map((socket) => {
7118
7207
  const cwd = processCwds.get(socket.pid);
7119
- const portKind = commanderBoundPids.size === 0 ? void 0 : commanderBoundPids.has(socket.pid) ? "commander_bound" : "descendant";
7208
+ const portKind = commanderBoundPids.size === 0 && commanderBoundPorts.size === 0 ? void 0 : commanderBoundPids.has(socket.pid) || commanderBoundPorts.has(socket.port) ? "commander_bound" : "descendant";
7120
7209
  return {
7121
7210
  port: socket.port,
7122
7211
  pid: socket.pid,
@@ -7129,6 +7218,11 @@ function detectedForwardCandidates(sockets, descendantPids, processCwds = /* @__
7129
7218
  function validPidSet(pids) {
7130
7219
  return new Set(pids.filter((pid) => Number.isInteger(pid) && pid > 0));
7131
7220
  }
7221
+ function validPortSet(ports) {
7222
+ return new Set(
7223
+ ports.filter((port) => Number.isInteger(port) && port > 0 && port < 65536)
7224
+ );
7225
+ }
7132
7226
  function normalizedPortKind(portKind) {
7133
7227
  return portKind ?? "descendant";
7134
7228
  }
@@ -9822,7 +9916,11 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
9822
9916
  seq: event.seq,
9823
9917
  actor_slug: event.actorSlug ?? null,
9824
9918
  actor_user_id: event.actorUserId ?? null,
9825
- reason: "different_thread"
9919
+ reason: "different_thread",
9920
+ thread_id: event.threadId ?? null,
9921
+ bound_thread_id: state.kandanThreadId ?? null,
9922
+ codex_thread_id: state.codexThreadId ?? null,
9923
+ body_preview: runnerConsoleBodyPreview(event.body)
9826
9924
  });
9827
9925
  return;
9828
9926
  }
@@ -11960,14 +12058,336 @@ var init_claudeCodePlanMirror = __esm({
11960
12058
  }
11961
12059
  });
11962
12060
 
12061
+ // src/runnerLogger.ts
12062
+ import { appendFileSync, openSync as openSync2 } from "node:fs";
12063
+ import { createWriteStream } from "node:fs";
12064
+ import { homedir as homedir5 } from "node:os";
12065
+ import { dirname as dirname3, join as join6 } from "node:path";
12066
+ import { mkdirSync as mkdirSync2 } from "node:fs";
12067
+ function createRunnerLogger(logFile, consoleReporter) {
12068
+ mkdirSync2(dirname3(logFile), { recursive: true });
12069
+ const fd = openSync2(logFile, "a");
12070
+ const stream = createWriteStream("", { fd, flags: "a", autoClose: true });
12071
+ const logger = ((event, payload) => {
12072
+ const redacted = redactForCliLog(payload);
12073
+ stream.write(
12074
+ `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), event, ...redacted })}
12075
+ `,
12076
+ "utf8"
12077
+ );
12078
+ consoleReporter?.(event, runnerConsolePayload(redacted, payload));
12079
+ });
12080
+ Object.defineProperty(logger, "close", {
12081
+ value: () => closeStream(stream)
12082
+ });
12083
+ return logger;
12084
+ }
12085
+ function writeCliAuditEvent(event, payload, options = {}) {
12086
+ const logFile = options.logFile ?? defaultCliAuditLogFile();
12087
+ try {
12088
+ mkdirSync2(dirname3(logFile), { recursive: true });
12089
+ appendFileSync(
12090
+ logFile,
12091
+ `${JSON.stringify({
12092
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
12093
+ event,
12094
+ ...options.sessionId === void 0 ? {} : { sessionId: options.sessionId },
12095
+ ...redactForCliLog(payload)
12096
+ })}
12097
+ `,
12098
+ "utf8"
12099
+ );
12100
+ } catch (_error) {
12101
+ return;
12102
+ }
12103
+ }
12104
+ function defaultCliAuditLogFile() {
12105
+ const override = process.env.LINZUMI_CLI_AUDIT_LOG?.trim();
12106
+ return override === void 0 || override === "" ? join6(homedir5(), ".linzumi", "logs", "command-events.jsonl") : override;
12107
+ }
12108
+ function defaultRunnerLogFile() {
12109
+ return join6(homedir5(), ".linzumi", "logs", "linzumi-runner.log");
12110
+ }
12111
+ function redactForCliLog(value) {
12112
+ return redactObject(value);
12113
+ }
12114
+ function redactValue(value, key) {
12115
+ if (sensitiveKey(key)) {
12116
+ return sensitiveMarker;
12117
+ }
12118
+ if (typeof value === "string") {
12119
+ return redactString(value);
12120
+ }
12121
+ if (Array.isArray(value)) {
12122
+ return key === "args" ? redactArgs(value) : value.map((item) => redactValue(item, void 0));
12123
+ }
12124
+ if (value !== null && typeof value === "object") {
12125
+ return redactObject(value);
12126
+ }
12127
+ return value;
12128
+ }
12129
+ function redactObject(value) {
12130
+ const headerName = typeof value.name === "string" ? value.name.toLowerCase() : void 0;
12131
+ const shouldRedactHeaderValue = headerName === "authorization" || headerName === "cookie" || headerName === "set-cookie";
12132
+ return Object.fromEntries(
12133
+ Object.entries(value).filter(([key]) => key !== "runner_console").map(([key, entry]) => [
12134
+ key,
12135
+ shouldRedactHeaderValue && key === "value" ? sensitiveMarker : redactValue(entry, key)
12136
+ ])
12137
+ );
12138
+ }
12139
+ function runnerConsolePayload(redacted, original) {
12140
+ const consoleFields = objectValue3(original.runner_console);
12141
+ return consoleFields === void 0 ? redacted : { ...redacted, ...redactForCliLog(consoleFields) };
12142
+ }
12143
+ function objectValue3(value) {
12144
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
12145
+ }
12146
+ function redactArgs(args) {
12147
+ let redactNext = false;
12148
+ return args.map((arg) => {
12149
+ if (typeof arg !== "string") {
12150
+ redactNext = false;
12151
+ return redactValue(arg, void 0);
12152
+ }
12153
+ if (redactNext) {
12154
+ redactNext = false;
12155
+ return sensitiveMarker;
12156
+ }
12157
+ const [flag, value] = splitArgAssignment(arg);
12158
+ if (sensitiveArgFlags.has(flag)) {
12159
+ if (value === void 0) {
12160
+ redactNext = true;
12161
+ return arg;
12162
+ }
12163
+ return `${flag}=${sensitiveMarker}`;
12164
+ }
12165
+ return redactString(arg);
12166
+ });
12167
+ }
12168
+ function splitArgAssignment(value) {
12169
+ const index = value.indexOf("=");
12170
+ return index === -1 ? [value, void 0] : [value.slice(0, index), value.slice(index + 1)];
12171
+ }
12172
+ function sensitiveKey(key) {
12173
+ if (key === void 0) {
12174
+ return false;
12175
+ }
12176
+ return /^(authorization|cookie|set-cookie|password)$/i.test(key) || /(^|[_-])(access[_-]?token|api[_-]?key|auth[_-]?token|oauth[_-]?code|refresh[_-]?token|secret|token)$/i.test(
12177
+ key
12178
+ );
12179
+ }
12180
+ function redactString(value) {
12181
+ const withRedactedQuery = redactUrlQuery(value);
12182
+ return withRedactedQuery.replace(
12183
+ /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
12184
+ sensitiveMarker
12185
+ );
12186
+ }
12187
+ function redactUrlQuery(value) {
12188
+ let parsed;
12189
+ try {
12190
+ parsed = new URL(value);
12191
+ } catch (_error) {
12192
+ return redactRelativeUrlQuery(value);
12193
+ }
12194
+ for (const name of [...parsed.searchParams.keys()]) {
12195
+ if (sensitiveQueryParams.has(name.toLowerCase())) {
12196
+ parsed.searchParams.set(name, sensitiveMarker);
12197
+ }
12198
+ }
12199
+ return parsed.toString();
12200
+ }
12201
+ function redactRelativeUrlQuery(value) {
12202
+ const queryStart = value.indexOf("?");
12203
+ const hashStart = value.indexOf("#");
12204
+ if (queryStart === -1 || hashStart !== -1 && hashStart < queryStart) {
12205
+ return value;
12206
+ }
12207
+ const path2 = value.slice(0, queryStart);
12208
+ const query = hashStart === -1 ? value.slice(queryStart + 1) : value.slice(queryStart + 1, hashStart);
12209
+ const hash = hashStart === -1 ? "" : value.slice(hashStart);
12210
+ const searchParams = new URLSearchParams(query);
12211
+ let redacted = false;
12212
+ for (const name of [...searchParams.keys()]) {
12213
+ if (sensitiveQueryParams.has(name.toLowerCase())) {
12214
+ searchParams.set(name, sensitiveMarker);
12215
+ redacted = true;
12216
+ }
12217
+ }
12218
+ switch (redacted) {
12219
+ case true:
12220
+ return `${path2}?${searchParams.toString()}${hash}`;
12221
+ case false:
12222
+ return value;
12223
+ }
12224
+ }
12225
+ function closeStream(stream) {
12226
+ if (stream.closed || stream.destroyed) {
12227
+ return Promise.resolve();
12228
+ }
12229
+ return new Promise((resolve12, reject) => {
12230
+ stream.once("error", reject);
12231
+ stream.end(resolve12);
12232
+ });
12233
+ }
12234
+ var sensitiveMarker, sensitiveQueryParams, sensitiveArgFlags;
12235
+ var init_runnerLogger = __esm({
12236
+ "src/runnerLogger.ts"() {
12237
+ "use strict";
12238
+ sensitiveMarker = "<SENSITIVE_DATA>";
12239
+ sensitiveQueryParams = /* @__PURE__ */ new Set([
12240
+ "access_token",
12241
+ "authorization",
12242
+ "code",
12243
+ "cookie",
12244
+ "kandan_preview_ticket",
12245
+ "refresh_token",
12246
+ "token"
12247
+ ]);
12248
+ sensitiveArgFlags = /* @__PURE__ */ new Set([
12249
+ "--access-token",
12250
+ "--api-key",
12251
+ "--authorization",
12252
+ "--cookie",
12253
+ "--oauth-code",
12254
+ "--password",
12255
+ "--refresh-token",
12256
+ "--secret",
12257
+ "--token"
12258
+ ]);
12259
+ }
12260
+ });
12261
+
12262
+ // src/engineChildReaper.ts
12263
+ function registerEngineChild(registration, _killProcess = process.kill) {
12264
+ reaperState.children.add(registration);
12265
+ ensureExitHandlersInstalled();
12266
+ return {
12267
+ unregister: () => {
12268
+ reaperState.children.delete(registration);
12269
+ }
12270
+ };
12271
+ }
12272
+ function reapAllEngineChildren(killProcess = process.kill, reason = "exit") {
12273
+ const children = [...reaperState.children];
12274
+ reaperState.children.clear();
12275
+ for (const child of children) {
12276
+ reapEngineChild(child, killProcess, reason);
12277
+ }
12278
+ }
12279
+ function reapEngineChild(child, killProcess, reason) {
12280
+ try {
12281
+ child.stop();
12282
+ } catch (error) {
12283
+ auditReapFailure(child, reason, "stop", error);
12284
+ }
12285
+ if (child.pid === void 0) {
12286
+ return;
12287
+ }
12288
+ if (child.ownProcessGroup) {
12289
+ try {
12290
+ killProcess(-child.pid, reapSignal);
12291
+ return;
12292
+ } catch (error) {
12293
+ if (processSignalErrorCode(error) === "ESRCH") {
12294
+ return;
12295
+ }
12296
+ auditReapFailure(child, reason, "group_kill", error);
12297
+ }
12298
+ }
12299
+ try {
12300
+ killProcess(child.pid, reapSignal);
12301
+ } catch (error) {
12302
+ if (processSignalErrorCode(error) === "ESRCH") {
12303
+ return;
12304
+ }
12305
+ auditReapFailure(child, reason, "pid_kill", error);
12306
+ }
12307
+ }
12308
+ function ensureExitHandlersInstalled() {
12309
+ if (reaperState.handlersInstalled) {
12310
+ return;
12311
+ }
12312
+ reaperState.handlersInstalled = true;
12313
+ const onExit2 = () => {
12314
+ reapAllEngineChildren(process.kill, "process_exit");
12315
+ };
12316
+ process.on("exit", onExit2);
12317
+ const ownsUncaught = process.listenerCount("uncaughtException") === 0;
12318
+ const ownsRejection = process.listenerCount("unhandledRejection") === 0;
12319
+ const onUncaughtException = (error) => {
12320
+ reapAllEngineChildren(process.kill, "uncaught_exception");
12321
+ process.stderr.write(
12322
+ `linzumi runner uncaughtException: ${error instanceof Error ? error.stack ?? error.message : String(error)}
12323
+ `
12324
+ );
12325
+ process.exit(1);
12326
+ };
12327
+ const onUnhandledRejection = (reason) => {
12328
+ reapAllEngineChildren(process.kill, "unhandled_rejection");
12329
+ process.stderr.write(
12330
+ `linzumi runner unhandledRejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
12331
+ `
12332
+ );
12333
+ process.exit(1);
12334
+ };
12335
+ if (ownsUncaught) {
12336
+ process.on("uncaughtException", onUncaughtException);
12337
+ }
12338
+ if (ownsRejection) {
12339
+ process.on("unhandledRejection", onUnhandledRejection);
12340
+ }
12341
+ reaperState.removeHandlers = () => {
12342
+ process.off("exit", onExit2);
12343
+ if (ownsUncaught) {
12344
+ process.off("uncaughtException", onUncaughtException);
12345
+ }
12346
+ if (ownsRejection) {
12347
+ process.off("unhandledRejection", onUnhandledRejection);
12348
+ }
12349
+ };
12350
+ }
12351
+ function auditReapFailure(child, reason, stage, error) {
12352
+ writeCliAuditEvent("process.reap_failed", {
12353
+ purpose: child.kind,
12354
+ pid: child.pid ?? null,
12355
+ ownProcessGroup: child.ownProcessGroup,
12356
+ reason,
12357
+ stage,
12358
+ code: processSignalErrorCode(error) ?? null,
12359
+ message: error instanceof Error ? error.message : String(error)
12360
+ });
12361
+ }
12362
+ function processSignalErrorCode(error) {
12363
+ if (error !== null && typeof error === "object" && "code" in error) {
12364
+ const code = error.code;
12365
+ return typeof code === "string" ? code : void 0;
12366
+ }
12367
+ return void 0;
12368
+ }
12369
+ var reaperState, reapSignal;
12370
+ var init_engineChildReaper = __esm({
12371
+ "src/engineChildReaper.ts"() {
12372
+ "use strict";
12373
+ init_runnerLogger();
12374
+ reaperState = {
12375
+ children: /* @__PURE__ */ new Set(),
12376
+ handlersInstalled: false,
12377
+ removeHandlers: void 0
12378
+ };
12379
+ reapSignal = "SIGKILL";
12380
+ }
12381
+ });
12382
+
11963
12383
  // src/claudeCodeLiveBashOutput.ts
11964
12384
  import {
11965
- mkdirSync as mkdirSync2,
12385
+ mkdirSync as mkdirSync3,
11966
12386
  rmSync,
11967
12387
  writeFileSync,
11968
12388
  promises as fsPromises
11969
12389
  } from "node:fs";
11970
- import { join as join6 } from "node:path";
12390
+ import { join as join7 } from "node:path";
11971
12391
  import { StringDecoder } from "node:string_decoder";
11972
12392
  function shellSingleQuoted(value) {
11973
12393
  return `'${value.replaceAll("'", `'\\''`)}'`;
@@ -12092,12 +12512,12 @@ function createClaudeCodeLiveBashCapture(host) {
12092
12512
  isClaudeLiveBashWrappedCommand(command)) {
12093
12513
  return void 0;
12094
12514
  }
12095
- const file = join6(
12515
+ const file = join7(
12096
12516
  host.captureDir,
12097
12517
  `${toolUseId.replaceAll(/[^\w-]/g, "_")}.out`
12098
12518
  );
12099
12519
  try {
12100
- mkdirSync2(host.captureDir, { recursive: true });
12520
+ mkdirSync3(host.captureDir, { recursive: true });
12101
12521
  writeFileSync(file, "");
12102
12522
  } catch (error) {
12103
12523
  host.log?.("claude_live_bash.capture_file_failed", {
@@ -12179,8 +12599,8 @@ var init_claudeCodeLiveBashOutput = __esm({
12179
12599
 
12180
12600
  // src/claudeCodeSession.ts
12181
12601
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
12182
- import { homedir as homedir5 } from "node:os";
12183
- import { join as join7 } from "node:path";
12602
+ import { homedir as homedir6 } from "node:os";
12603
+ import { join as join8 } from "node:path";
12184
12604
  function claudeCodeSettingSources() {
12185
12605
  return ["user", "project", "local"];
12186
12606
  }
@@ -12324,7 +12744,7 @@ function claudeCodeRateLimitSummaryText(rateLimit, nowMs) {
12324
12744
  async function probeClaudeCodeAvailability(args) {
12325
12745
  if (!hasClaudeCodeAuthHint(process.env, {
12326
12746
  cwd: args.cwd,
12327
- homeDir: homedir5(),
12747
+ homeDir: homedir6(),
12328
12748
  platform: process.platform,
12329
12749
  fileExists: existsSync4,
12330
12750
  readTextFile: readTextFileIfPresent
@@ -12357,7 +12777,7 @@ async function probeClaudeCodeAvailability(args) {
12357
12777
  }
12358
12778
  }
12359
12779
  function hasClaudeCodeAuthHint(env, deps) {
12360
- const configDir = env.CLAUDE_CONFIG_DIR ?? join7(deps.homeDir, ".claude");
12780
+ const configDir = env.CLAUDE_CONFIG_DIR ?? join8(deps.homeDir, ".claude");
12361
12781
  return hasAnthropicCredentialEnv(env) || hasClaudeCloudProviderEnv(env) || hasClaudeCodeFileCredential(configDir, deps) || hasClaudeCodeApiKeyHelper(configDir, deps) || hasMacClaudeCodeKeychainAnchor(deps);
12362
12782
  }
12363
12783
  function claudeCodePolicyDeferHooks() {
@@ -12392,14 +12812,14 @@ function hasClaudeCloudProviderEnv(env) {
12392
12812
  return trueishEnv(env.CLAUDE_CODE_USE_BEDROCK) || trueishEnv(env.CLAUDE_CODE_USE_VERTEX) || trueishEnv(env.CLAUDE_CODE_USE_FOUNDRY) || trueishEnv(env.CLAUDE_CODE_USE_ANTHROPIC_AWS) || trueishEnv(env.CLAUDE_CODE_USE_MANTLE);
12393
12813
  }
12394
12814
  function hasClaudeCodeFileCredential(configDir, deps) {
12395
- return deps.fileExists(join7(configDir, ".credentials.json")) || deps.fileExists(join7(configDir, ".claude.json")) || deps.fileExists(join7(deps.homeDir, ".claude.json"));
12815
+ return deps.fileExists(join8(configDir, ".credentials.json")) || deps.fileExists(join8(configDir, ".claude.json")) || deps.fileExists(join8(deps.homeDir, ".claude.json"));
12396
12816
  }
12397
12817
  function hasClaudeCodeApiKeyHelper(configDir, deps) {
12398
12818
  return [
12399
- join7(configDir, "settings.json"),
12400
- join7(configDir, "settings.local.json"),
12401
- join7(deps.cwd, ".claude", "settings.json"),
12402
- join7(deps.cwd, ".claude", "settings.local.json")
12819
+ join8(configDir, "settings.json"),
12820
+ join8(configDir, "settings.local.json"),
12821
+ join8(deps.cwd, ".claude", "settings.json"),
12822
+ join8(deps.cwd, ".claude", "settings.local.json")
12403
12823
  ].some((path2) => settingsFileHasApiKeyHelper(path2, deps));
12404
12824
  }
12405
12825
  function settingsFileHasApiKeyHelper(path2, deps) {
@@ -12419,9 +12839,9 @@ function hasMacClaudeCodeKeychainAnchor(deps) {
12419
12839
  return false;
12420
12840
  }
12421
12841
  return [
12422
- join7(deps.homeDir, "Library", "Application Support", "claude-cli-nodejs"),
12423
- join7(deps.homeDir, "Library", "Application Support", "Claude"),
12424
- join7(deps.homeDir, "Library", "Preferences", "claude-cli-nodejs")
12842
+ join8(deps.homeDir, "Library", "Application Support", "claude-cli-nodejs"),
12843
+ join8(deps.homeDir, "Library", "Application Support", "Claude"),
12844
+ join8(deps.homeDir, "Library", "Preferences", "claude-cli-nodejs")
12425
12845
  ].some((path2) => deps.fileExists(path2));
12426
12846
  }
12427
12847
  function readTextFileIfPresent(path2) {
@@ -12440,87 +12860,83 @@ async function startClaudeCodeSession(options) {
12440
12860
  assistantTextByKey: /* @__PURE__ */ new Map(),
12441
12861
  usage: void 0,
12442
12862
  startedSessionIds: /* @__PURE__ */ new Set(),
12443
- completedTurnCount: 0
12863
+ completedTurnCount: 0,
12864
+ lastCompletedTurnBody: void 0
12444
12865
  };
12445
12866
  const streamUsageTracker = createClaudeCodeStreamUsageTracker();
12446
- for await (const message of runner(options)) {
12447
- state.sessionId = state.sessionId ?? extractClaudeSessionId(message);
12448
- const sessionId = extractClaudeSessionId(message) ?? state.sessionId;
12449
- if (sessionId !== void 0 && !state.startedSessionIds.has(sessionId)) {
12450
- state.startedSessionIds.add(sessionId);
12451
- await options.onTranscriptEvent?.({ type: "session_started", sessionId });
12452
- }
12453
- for (const event of transcriptEventsForClaudeMessage(
12454
- message,
12455
- sessionId,
12456
- streamUsageTracker
12457
- )) {
12458
- if (event.type === "assistant_message") {
12459
- recordClaudeAssistantAggregate(state, event);
12460
- }
12461
- if (event.type === "usage") {
12462
- state.usage = event.usage;
12463
- }
12464
- await options.onTranscriptEvent?.(event);
12465
- }
12466
- const resultOutcome = extractClaudeResultOutcome(message);
12467
- switch (resultOutcome.type) {
12468
- case "success": {
12469
- if (resultOutcome.text !== void 0) {
12470
- state.resultText = resultOutcome.text;
12471
- }
12472
- state.usage = extractClaudeUsage(message) ?? state.usage;
12473
- state.completedTurnCount += 1;
12474
- const queuedTurnCountBeforeTranscript = options.streamingInput === void 0 ? void 0 : options.streamingInput.queuedTurnCount();
12475
- if (options.streamingInput !== void 0) {
12476
- await emitClaudeCodeTurnCompleted(options, state);
12477
- }
12478
- if (options.streamingInput !== void 0 && queuedTurnCountBeforeTranscript !== void 0 && state.completedTurnCount >= Math.max(
12479
- queuedTurnCountBeforeTranscript,
12480
- options.streamingInput.queuedTurnCount()
12481
- )) {
12482
- options.streamingInput.close();
12483
- return completeClaudeCodeSession(options, state);
12484
- }
12485
- if (options.streamingInput !== void 0) {
12486
- await options.onTurnCompleted?.();
12487
- resetClaudeAssistantAggregate(state);
12488
- state.resultText = void 0;
12867
+ try {
12868
+ for await (const message of runner(options)) {
12869
+ state.sessionId = state.sessionId ?? extractClaudeSessionId(message);
12870
+ const sessionId = extractClaudeSessionId(message) ?? state.sessionId;
12871
+ if (sessionId !== void 0 && !state.startedSessionIds.has(sessionId)) {
12872
+ state.startedSessionIds.add(sessionId);
12873
+ await options.onTranscriptEvent?.({
12874
+ type: "session_started",
12875
+ sessionId
12876
+ });
12877
+ }
12878
+ for (const event of transcriptEventsForClaudeMessage(
12879
+ message,
12880
+ sessionId,
12881
+ streamUsageTracker
12882
+ )) {
12883
+ if (event.type === "assistant_message") {
12884
+ recordClaudeAssistantAggregate(state, event);
12489
12885
  }
12490
- break;
12886
+ if (event.type === "usage") {
12887
+ state.usage = event.usage;
12888
+ }
12889
+ await options.onTranscriptEvent?.(event);
12491
12890
  }
12492
- case "interrupted": {
12493
- state.completedTurnCount += 1;
12494
- if (state.sessionId !== void 0) {
12495
- await options.onTranscriptEvent?.({
12496
- type: "turn_interrupted",
12497
- sessionId: state.sessionId
12498
- });
12891
+ const resultOutcome = extractClaudeResultOutcome(message);
12892
+ switch (resultOutcome.type) {
12893
+ case "success": {
12894
+ if (resultOutcome.text !== void 0) {
12895
+ state.resultText = resultOutcome.text;
12896
+ }
12897
+ state.usage = extractClaudeUsage(message) ?? state.usage;
12898
+ state.completedTurnCount += 1;
12899
+ if (options.streamingInput !== void 0) {
12900
+ await emitClaudeCodeTurnCompleted(options, state);
12901
+ state.lastCompletedTurnBody = state.resultText ?? nonEmptyText(claudeAssistantAggregateText(state)) ?? "";
12902
+ await options.onTurnCompleted?.();
12903
+ resetClaudeAssistantAggregate(state);
12904
+ state.resultText = void 0;
12905
+ }
12906
+ break;
12499
12907
  }
12500
- if (options.streamingInput !== void 0) {
12501
- await options.onTurnCompleted?.();
12502
- resetClaudeAssistantAggregate(state);
12503
- state.resultText = void 0;
12908
+ case "interrupted": {
12909
+ state.completedTurnCount += 1;
12910
+ if (state.sessionId !== void 0) {
12911
+ await options.onTranscriptEvent?.({
12912
+ type: "turn_interrupted",
12913
+ sessionId: state.sessionId
12914
+ });
12915
+ }
12916
+ if (options.streamingInput !== void 0) {
12917
+ await options.onTurnCompleted?.();
12918
+ resetClaudeAssistantAggregate(state);
12919
+ state.resultText = void 0;
12920
+ }
12921
+ break;
12504
12922
  }
12505
- break;
12923
+ case "error":
12924
+ return await failClaudeCodeSession(
12925
+ options,
12926
+ state.sessionId,
12927
+ `Claude Code failed: ${resultOutcome.reason}`
12928
+ );
12929
+ case "none":
12930
+ break;
12506
12931
  }
12507
- case "error":
12508
- return await failClaudeCodeSession(
12509
- options,
12510
- state.sessionId,
12511
- `Claude Code failed: ${resultOutcome.reason}`
12512
- );
12513
- case "none":
12514
- break;
12515
12932
  }
12933
+ } finally {
12934
+ options.streamingInput?.close();
12516
12935
  }
12517
12936
  return completeClaudeCodeSession(options, state);
12518
12937
  }
12519
12938
  async function emitClaudeCodeTurnCompleted(options, state) {
12520
- const body = state.resultText ?? nonEmptyText(claudeAssistantAggregateText(state));
12521
- if (body === void 0) {
12522
- return;
12523
- }
12939
+ const body = state.resultText ?? nonEmptyText(claudeAssistantAggregateText(state)) ?? "";
12524
12940
  if (state.sessionId === void 0) {
12525
12941
  return;
12526
12942
  }
@@ -12532,7 +12948,7 @@ async function emitClaudeCodeTurnCompleted(options, state) {
12532
12948
  });
12533
12949
  }
12534
12950
  async function completeClaudeCodeSession(options, state) {
12535
- const body = state.resultText ?? nonEmptyText(claudeAssistantAggregateText(state));
12951
+ const body = state.resultText ?? nonEmptyText(claudeAssistantAggregateText(state)) ?? state.lastCompletedTurnBody;
12536
12952
  if (body === void 0) {
12537
12953
  return await failClaudeCodeSession(
12538
12954
  options,
@@ -13076,207 +13492,6 @@ var init_claudeCodeSession = __esm({
13076
13492
  }
13077
13493
  });
13078
13494
 
13079
- // src/runnerLogger.ts
13080
- import { appendFileSync, openSync as openSync2 } from "node:fs";
13081
- import { createWriteStream } from "node:fs";
13082
- import { homedir as homedir6 } from "node:os";
13083
- import { dirname as dirname3, join as join8 } from "node:path";
13084
- import { mkdirSync as mkdirSync3 } from "node:fs";
13085
- function createRunnerLogger(logFile, consoleReporter) {
13086
- mkdirSync3(dirname3(logFile), { recursive: true });
13087
- const fd = openSync2(logFile, "a");
13088
- const stream = createWriteStream("", { fd, flags: "a", autoClose: true });
13089
- const logger = ((event, payload) => {
13090
- const redacted = redactForCliLog(payload);
13091
- stream.write(
13092
- `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), event, ...redacted })}
13093
- `,
13094
- "utf8"
13095
- );
13096
- consoleReporter?.(event, runnerConsolePayload(redacted, payload));
13097
- });
13098
- Object.defineProperty(logger, "close", {
13099
- value: () => closeStream(stream)
13100
- });
13101
- return logger;
13102
- }
13103
- function writeCliAuditEvent(event, payload, options = {}) {
13104
- const logFile = options.logFile ?? defaultCliAuditLogFile();
13105
- try {
13106
- mkdirSync3(dirname3(logFile), { recursive: true });
13107
- appendFileSync(
13108
- logFile,
13109
- `${JSON.stringify({
13110
- ts: (/* @__PURE__ */ new Date()).toISOString(),
13111
- event,
13112
- ...options.sessionId === void 0 ? {} : { sessionId: options.sessionId },
13113
- ...redactForCliLog(payload)
13114
- })}
13115
- `,
13116
- "utf8"
13117
- );
13118
- } catch (_error) {
13119
- return;
13120
- }
13121
- }
13122
- function defaultCliAuditLogFile() {
13123
- const override = process.env.LINZUMI_CLI_AUDIT_LOG?.trim();
13124
- return override === void 0 || override === "" ? join8(homedir6(), ".linzumi", "logs", "command-events.jsonl") : override;
13125
- }
13126
- function defaultRunnerLogFile() {
13127
- return join8(homedir6(), ".linzumi", "logs", "linzumi-runner.log");
13128
- }
13129
- function redactForCliLog(value) {
13130
- return redactObject(value);
13131
- }
13132
- function redactValue(value, key) {
13133
- if (sensitiveKey(key)) {
13134
- return sensitiveMarker;
13135
- }
13136
- if (typeof value === "string") {
13137
- return redactString(value);
13138
- }
13139
- if (Array.isArray(value)) {
13140
- return key === "args" ? redactArgs(value) : value.map((item) => redactValue(item, void 0));
13141
- }
13142
- if (value !== null && typeof value === "object") {
13143
- return redactObject(value);
13144
- }
13145
- return value;
13146
- }
13147
- function redactObject(value) {
13148
- const headerName = typeof value.name === "string" ? value.name.toLowerCase() : void 0;
13149
- const shouldRedactHeaderValue = headerName === "authorization" || headerName === "cookie" || headerName === "set-cookie";
13150
- return Object.fromEntries(
13151
- Object.entries(value).filter(([key]) => key !== "runner_console").map(([key, entry]) => [
13152
- key,
13153
- shouldRedactHeaderValue && key === "value" ? sensitiveMarker : redactValue(entry, key)
13154
- ])
13155
- );
13156
- }
13157
- function runnerConsolePayload(redacted, original) {
13158
- const consoleFields = objectValue3(original.runner_console);
13159
- return consoleFields === void 0 ? redacted : { ...redacted, ...redactForCliLog(consoleFields) };
13160
- }
13161
- function objectValue3(value) {
13162
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
13163
- }
13164
- function redactArgs(args) {
13165
- let redactNext = false;
13166
- return args.map((arg) => {
13167
- if (typeof arg !== "string") {
13168
- redactNext = false;
13169
- return redactValue(arg, void 0);
13170
- }
13171
- if (redactNext) {
13172
- redactNext = false;
13173
- return sensitiveMarker;
13174
- }
13175
- const [flag, value] = splitArgAssignment(arg);
13176
- if (sensitiveArgFlags.has(flag)) {
13177
- if (value === void 0) {
13178
- redactNext = true;
13179
- return arg;
13180
- }
13181
- return `${flag}=${sensitiveMarker}`;
13182
- }
13183
- return redactString(arg);
13184
- });
13185
- }
13186
- function splitArgAssignment(value) {
13187
- const index = value.indexOf("=");
13188
- return index === -1 ? [value, void 0] : [value.slice(0, index), value.slice(index + 1)];
13189
- }
13190
- function sensitiveKey(key) {
13191
- if (key === void 0) {
13192
- return false;
13193
- }
13194
- return /^(authorization|cookie|set-cookie|password)$/i.test(key) || /(^|[_-])(access[_-]?token|api[_-]?key|auth[_-]?token|oauth[_-]?code|refresh[_-]?token|secret|token)$/i.test(
13195
- key
13196
- );
13197
- }
13198
- function redactString(value) {
13199
- const withRedactedQuery = redactUrlQuery(value);
13200
- return withRedactedQuery.replace(
13201
- /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
13202
- sensitiveMarker
13203
- );
13204
- }
13205
- function redactUrlQuery(value) {
13206
- let parsed;
13207
- try {
13208
- parsed = new URL(value);
13209
- } catch (_error) {
13210
- return redactRelativeUrlQuery(value);
13211
- }
13212
- for (const name of [...parsed.searchParams.keys()]) {
13213
- if (sensitiveQueryParams.has(name.toLowerCase())) {
13214
- parsed.searchParams.set(name, sensitiveMarker);
13215
- }
13216
- }
13217
- return parsed.toString();
13218
- }
13219
- function redactRelativeUrlQuery(value) {
13220
- const queryStart = value.indexOf("?");
13221
- const hashStart = value.indexOf("#");
13222
- if (queryStart === -1 || hashStart !== -1 && hashStart < queryStart) {
13223
- return value;
13224
- }
13225
- const path2 = value.slice(0, queryStart);
13226
- const query = hashStart === -1 ? value.slice(queryStart + 1) : value.slice(queryStart + 1, hashStart);
13227
- const hash = hashStart === -1 ? "" : value.slice(hashStart);
13228
- const searchParams = new URLSearchParams(query);
13229
- let redacted = false;
13230
- for (const name of [...searchParams.keys()]) {
13231
- if (sensitiveQueryParams.has(name.toLowerCase())) {
13232
- searchParams.set(name, sensitiveMarker);
13233
- redacted = true;
13234
- }
13235
- }
13236
- switch (redacted) {
13237
- case true:
13238
- return `${path2}?${searchParams.toString()}${hash}`;
13239
- case false:
13240
- return value;
13241
- }
13242
- }
13243
- function closeStream(stream) {
13244
- if (stream.closed || stream.destroyed) {
13245
- return Promise.resolve();
13246
- }
13247
- return new Promise((resolve12, reject) => {
13248
- stream.once("error", reject);
13249
- stream.end(resolve12);
13250
- });
13251
- }
13252
- var sensitiveMarker, sensitiveQueryParams, sensitiveArgFlags;
13253
- var init_runnerLogger = __esm({
13254
- "src/runnerLogger.ts"() {
13255
- "use strict";
13256
- sensitiveMarker = "<SENSITIVE_DATA>";
13257
- sensitiveQueryParams = /* @__PURE__ */ new Set([
13258
- "access_token",
13259
- "authorization",
13260
- "code",
13261
- "cookie",
13262
- "kandan_preview_ticket",
13263
- "refresh_token",
13264
- "token"
13265
- ]);
13266
- sensitiveArgFlags = /* @__PURE__ */ new Set([
13267
- "--access-token",
13268
- "--api-key",
13269
- "--authorization",
13270
- "--cookie",
13271
- "--oauth-code",
13272
- "--password",
13273
- "--refresh-token",
13274
- "--secret",
13275
- "--token"
13276
- ]);
13277
- }
13278
- });
13279
-
13280
13495
  // src/mcpConfig.ts
13281
13496
  function linzumiMcpServerConfig(options) {
13282
13497
  return {
@@ -13403,6 +13618,76 @@ var init_mcpConfig = __esm({
13403
13618
  }
13404
13619
  });
13405
13620
 
13621
+ // src/engineParentDeathWatchdog.ts
13622
+ function encodeParentDeathWatchdogConfig(config) {
13623
+ return JSON.stringify(config);
13624
+ }
13625
+ function parentDeathWatchdogProgram() {
13626
+ return `
13627
+ const { spawn } = require('node:child_process');
13628
+ const config = JSON.parse(process.argv[process.argv.length - 1]);
13629
+ const originalParentPid = process.ppid;
13630
+ const child = spawn(config.command, config.args, { stdio: 'inherit' });
13631
+ let done = false;
13632
+ function shutdown(signal) {
13633
+ if (done) return;
13634
+ done = true;
13635
+ try { process.kill(-process.pid, signal); }
13636
+ catch (_e) { try { child.kill(signal); } catch (_e2) {} }
13637
+ }
13638
+ process.on('SIGINT', () => shutdown('SIGINT'));
13639
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
13640
+ process.on('SIGHUP', () => shutdown('SIGTERM'));
13641
+ child.on('exit', (code, signal) => {
13642
+ clearInterval(watchdog);
13643
+ // The child is gone; the wrapper must follow it down rather than linger as an
13644
+ // orphan. On the forwarded-signal path (shutdown() above caught SIGINT/SIGTERM
13645
+ // /SIGHUP and propagated it to the child), the wrapper still has its own
13646
+ // handler installed for that signal, so a bare process.kill(self, signal)
13647
+ // would just re-enter shutdown() - a no-op once done is set - and the wrapper
13648
+ // would never exit. Remove the handlers first so the signal's DEFAULT
13649
+ // disposition (terminate) applies, re-raise to mirror the child's death
13650
+ // signal, then exit() as a belt-and-suspenders fallback in case re-raising
13651
+ // does not terminate (e.g. a signal with no default-terminate disposition).
13652
+ process.removeAllListeners('SIGINT');
13653
+ process.removeAllListeners('SIGTERM');
13654
+ process.removeAllListeners('SIGHUP');
13655
+ if (signal) { try { process.kill(process.pid, signal); } catch (_e) {} }
13656
+ process.exit(signal ? 1 : code == null ? 0 : code);
13657
+ });
13658
+ child.on('error', (err) => {
13659
+ process.stderr.write('engine watchdog spawn failed: ' + (err && err.message) + '\\n');
13660
+ process.exit(1);
13661
+ });
13662
+ const watchdog = setInterval(() => {
13663
+ // Inlined parentDeathWatchdogShouldReap (keep in sync): reap PURELY on a ppid
13664
+ // change from boot - covers reparent-to-subreaper AND reparent-to-init. NO
13665
+ // \`=== 1\` arm: a runner that is PID 1 captures originalParentPid === 1, so
13666
+ // such an arm would false-fire on the first poll and kill a healthy codex.
13667
+ if (process.ppid !== originalParentPid) {
13668
+ try { child.kill('SIGKILL'); } catch (_e) {}
13669
+ try { process.kill(-process.pid, 'SIGKILL'); } catch (_e) {}
13670
+ process.exit(0);
13671
+ }
13672
+ }, config.pollIntervalMs);
13673
+ `.trim();
13674
+ }
13675
+ function parentDeathWatchdogSpawn(nodeExecPath, config) {
13676
+ return {
13677
+ command: nodeExecPath,
13678
+ args: [
13679
+ "-e",
13680
+ parentDeathWatchdogProgram(),
13681
+ encodeParentDeathWatchdogConfig(config)
13682
+ ]
13683
+ };
13684
+ }
13685
+ var init_engineParentDeathWatchdog = __esm({
13686
+ "src/engineParentDeathWatchdog.ts"() {
13687
+ "use strict";
13688
+ }
13689
+ });
13690
+
13406
13691
  // src/codexAppServer.ts
13407
13692
  import {
13408
13693
  spawn as spawn2
@@ -13444,13 +13729,18 @@ async function startCodexAppServerAttempt(codexBin, cwd, options, attempt) {
13444
13729
  let stderrText = "";
13445
13730
  const configuredStdio = codexAppServerStdio(process.stdout.isTTY === true);
13446
13731
  const stdio = [configuredStdio[0], configuredStdio[1], "pipe"];
13732
+ const watchdogSpawn = parentDeathWatchdogSpawn(process.execPath, {
13733
+ command: codexBin,
13734
+ args,
13735
+ pollIntervalMs: codexAppServerWatchdogPollMs
13736
+ });
13447
13737
  writeCliAuditEvent("process.spawn", {
13448
13738
  command: codexBin,
13449
13739
  args,
13450
13740
  cwd,
13451
13741
  purpose: "codex.app_server"
13452
13742
  });
13453
- const child = spawn2(codexBin, args, {
13743
+ const child = spawn2(watchdogSpawn.command, [...watchdogSpawn.args], {
13454
13744
  cwd,
13455
13745
  env: codexAppServerEnv(options.env, options.inheritEnv ?? true),
13456
13746
  stdio,
@@ -13467,7 +13757,17 @@ async function startCodexAppServerAttempt(codexBin, cwd, options, attempt) {
13467
13757
  }
13468
13758
  });
13469
13759
  }
13470
- const stop = () => stopCodexAppServerProcess(child);
13760
+ const registered = registerEngineChild({
13761
+ stop: () => stopCodexAppServerProcess(child),
13762
+ pid: child.pid,
13763
+ ownProcessGroup: true,
13764
+ kind: "codex.app_server"
13765
+ });
13766
+ child.once("exit", () => registered.unregister());
13767
+ const stop = () => {
13768
+ registered.unregister();
13769
+ stopCodexAppServerProcess(child);
13770
+ };
13471
13771
  writeCliAuditEvent("process.spawned", {
13472
13772
  command: codexBin,
13473
13773
  args,
@@ -13571,7 +13871,7 @@ function stopCodexAppServerProcess(child, killProcess = process.kill) {
13571
13871
  child.kill("SIGINT");
13572
13872
  }
13573
13873
  function logProcessGroupSignalFailure(pid, error) {
13574
- const code = processSignalErrorCode(error);
13874
+ const code = processSignalErrorCode2(error);
13575
13875
  const message = error instanceof Error ? error.message : String(error);
13576
13876
  const event = code === "EPERM" ? "process.group_signal_denied" : code === "ESRCH" ? "process.group_signal_missing" : "process.group_signal_failed";
13577
13877
  writeCliAuditEvent(event, {
@@ -13589,7 +13889,7 @@ function logProcessGroupSignalFailure(pid, error) {
13589
13889
  );
13590
13890
  }
13591
13891
  }
13592
- function processSignalErrorCode(error) {
13892
+ function processSignalErrorCode2(error) {
13593
13893
  if (error !== null && typeof error === "object" && "code" in error) {
13594
13894
  const code = error.code;
13595
13895
  return typeof code === "string" ? code : void 0;
@@ -13973,13 +14273,16 @@ function readyzUrlForWebsocket(websocketUrl) {
13973
14273
  parsed.hash = "";
13974
14274
  return parsed.toString();
13975
14275
  }
13976
- var blockedCodexAppServerEnvKeys;
14276
+ var codexAppServerWatchdogPollMs, blockedCodexAppServerEnvKeys;
13977
14277
  var init_codexAppServer = __esm({
13978
14278
  "src/codexAppServer.ts"() {
13979
14279
  "use strict";
13980
14280
  init_protocol();
13981
14281
  init_runnerLogger();
13982
14282
  init_mcpConfig();
14283
+ init_engineChildReaper();
14284
+ init_engineParentDeathWatchdog();
14285
+ codexAppServerWatchdogPollMs = 2e3;
13983
14286
  blockedCodexAppServerEnvKeys = [
13984
14287
  "LINZUMI_MCP_ACCESS_TOKEN",
13985
14288
  "LINZUMI_MCP_OWNER_USERNAME"
@@ -18127,7 +18430,7 @@ var linzumiCliVersion, linzumiCliVersionText;
18127
18430
  var init_version = __esm({
18128
18431
  "src/version.ts"() {
18129
18432
  "use strict";
18130
- linzumiCliVersion = "0.0.84-beta";
18433
+ linzumiCliVersion = "0.0.86-beta";
18131
18434
  linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
18132
18435
  }
18133
18436
  });
@@ -18406,6 +18709,9 @@ function releaseRunnerLock(path2, record) {
18406
18709
  }
18407
18710
  }
18408
18711
  function readRunnerLockForRelease(path2) {
18712
+ return readRunnerLockOwner(path2);
18713
+ }
18714
+ function readRunnerLockOwner(path2) {
18409
18715
  try {
18410
18716
  return readRunnerLockIfPresent(path2);
18411
18717
  } catch (_error) {
@@ -18526,8 +18832,216 @@ var init_runnerLock = __esm({
18526
18832
  }
18527
18833
  });
18528
18834
 
18529
- // src/runnerConsoleReporter.ts
18835
+ // src/runnerLockTakeover.ts
18836
+ function runnerLockTakeoverSleep(ms) {
18837
+ return new Promise((resolve12) => {
18838
+ setTimeout(resolve12, ms);
18839
+ });
18840
+ }
18841
+ function runnerLockConflictReport(baseMessage) {
18842
+ const bar = "=".repeat(64);
18843
+ return [
18844
+ "",
18845
+ bar,
18846
+ "ERROR: linzumi connect could not start - workspace already in use",
18847
+ bar,
18848
+ baseMessage,
18849
+ bar,
18850
+ ""
18851
+ ].join("\n");
18852
+ }
18853
+ function isRunnerLockConflictReportedError(error) {
18854
+ return error instanceof RunnerLockConflictReportedError || error instanceof Error && error.name === runnerLockConflictReportedErrorName;
18855
+ }
18856
+ function shouldResolveRunnerLockConflict(lockTakeover) {
18857
+ return lockTakeover !== void 0;
18858
+ }
18859
+ async function resolveRunnerLockConflict(args) {
18860
+ const report = runnerLockConflictReport(args.baseMessage);
18861
+ if (args.takeOverWithoutPrompt) {
18862
+ args.prompt.writeReport(report);
18863
+ await stopHolderAndAwaitRelease(args.holder, args.lockPath, args.takeover);
18864
+ return { outcome: "take_over" };
18865
+ }
18866
+ if (!args.prompt.isInteractive()) {
18867
+ args.prompt.writeReport(report);
18868
+ return { outcome: "declined", report };
18869
+ }
18870
+ args.prompt.writeReport(report);
18871
+ const yes = await args.prompt.promptYesNo(
18872
+ `Another runner (pid ${args.holder.pid}) is already connected to this workspace. Stop it and run here instead? [y/N] `
18873
+ );
18874
+ if (!yes) {
18875
+ return { outcome: "declined", report };
18876
+ }
18877
+ await stopHolderAndAwaitRelease(args.holder, args.lockPath, args.takeover);
18878
+ return { outcome: "take_over" };
18879
+ }
18880
+ async function stopHolderAndAwaitRelease(holder, lockPath, deps) {
18881
+ deps.log("runner.lock_takeover_requested", {
18882
+ holderPid: holder.pid,
18883
+ holderRunnerId: holder.runnerId,
18884
+ lockPath
18885
+ });
18886
+ if (!deps.isPidAlive(holder.pid)) {
18887
+ deps.log("runner.lock_takeover_holder_already_gone", {
18888
+ holderPid: holder.pid,
18889
+ lockPath
18890
+ });
18891
+ return;
18892
+ }
18893
+ if (!stillHeldBy(deps, holder)) {
18894
+ deps.log("runner.lock_takeover_owner_changed", {
18895
+ holderPid: holder.pid,
18896
+ lockPath
18897
+ });
18898
+ return;
18899
+ }
18900
+ sendSignalTolerant(deps, holder.pid, "SIGTERM");
18901
+ if (await waitForHolderRelease(holder, deps, runnerLockTakeoverGracefulStopMs)) {
18902
+ deps.log("runner.lock_takeover_graceful", {
18903
+ holderPid: holder.pid,
18904
+ lockPath
18905
+ });
18906
+ return;
18907
+ }
18908
+ if (!stillHeldBy(deps, holder)) {
18909
+ deps.log("runner.lock_takeover_owner_changed", {
18910
+ holderPid: holder.pid,
18911
+ lockPath
18912
+ });
18913
+ return;
18914
+ }
18915
+ deps.log("runner.lock_takeover_escalating_sigkill", {
18916
+ holderPid: holder.pid,
18917
+ lockPath
18918
+ });
18919
+ sendSignalTolerant(deps, holder.pid, "SIGKILL");
18920
+ if (await waitForHolderRelease(holder, deps, runnerLockTakeoverForcefulStopMs)) {
18921
+ deps.log("runner.lock_takeover_forceful", {
18922
+ holderPid: holder.pid,
18923
+ lockPath
18924
+ });
18925
+ return;
18926
+ }
18927
+ throw new Error(
18928
+ `Could not stop the runner holding this workspace (pid ${holder.pid}). It did not exit after SIGTERM and SIGKILL within the timeout. Stop it manually or remove the lock file (${lockPath}) and retry.`
18929
+ );
18930
+ }
18931
+ async function waitForHolderRelease(holder, deps, timeoutMs) {
18932
+ const deadline = deps.now() + timeoutMs;
18933
+ while (deps.now() < deadline) {
18934
+ if (!deps.isPidAlive(holder.pid) || deps.lockReleased()) {
18935
+ return true;
18936
+ }
18937
+ await deps.sleep(runnerLockTakeoverPollMs);
18938
+ }
18939
+ return !deps.isPidAlive(holder.pid) || deps.lockReleased();
18940
+ }
18941
+ function stillHeldBy(deps, holder) {
18942
+ const current = deps.currentLockOwner();
18943
+ return current !== void 0 && current.machineId === holder.machineId && current.runnerId === holder.runnerId && current.pid === holder.pid && current.startedAt === holder.startedAt;
18944
+ }
18945
+ function sendSignalTolerant(deps, pid, signal) {
18946
+ try {
18947
+ deps.signalPid(pid, signal);
18948
+ } catch (error) {
18949
+ const code = error !== null && typeof error === "object" && "code" in error ? error.code : void 0;
18950
+ if (code !== "ESRCH") {
18951
+ deps.log("runner.lock_takeover_signal_failed", {
18952
+ holderPid: pid,
18953
+ signal,
18954
+ code: typeof code === "string" ? code : null,
18955
+ message: error instanceof Error ? error.message : String(error)
18956
+ });
18957
+ }
18958
+ }
18959
+ }
18960
+ var runnerLockTakeoverGracefulStopMs, runnerLockTakeoverForcefulStopMs, runnerLockTakeoverPollMs, runnerLockConflictReportedErrorName, RunnerLockConflictReportedError;
18961
+ var init_runnerLockTakeover = __esm({
18962
+ "src/runnerLockTakeover.ts"() {
18963
+ "use strict";
18964
+ runnerLockTakeoverGracefulStopMs = 1e4;
18965
+ runnerLockTakeoverForcefulStopMs = 5e3;
18966
+ runnerLockTakeoverPollMs = 200;
18967
+ runnerLockConflictReportedErrorName = "RunnerLockConflictReportedError";
18968
+ RunnerLockConflictReportedError = class extends Error {
18969
+ constructor() {
18970
+ super("");
18971
+ this.name = runnerLockConflictReportedErrorName;
18972
+ }
18973
+ };
18974
+ }
18975
+ });
18976
+
18977
+ // src/blessedTputSetulcShim.ts
18530
18978
  import blessed from "blessed";
18979
+ function balanceTerminfoConditionals(cap) {
18980
+ if (cap.indexOf("%;") === -1) {
18981
+ return cap;
18982
+ }
18983
+ let depth = 0;
18984
+ let repaired = "";
18985
+ let index = 0;
18986
+ while (index < cap.length) {
18987
+ const here = cap[index];
18988
+ const next = cap[index + 1];
18989
+ if (here === "%" && next === "%") {
18990
+ repaired += "%%";
18991
+ index += 2;
18992
+ continue;
18993
+ }
18994
+ if (here === "%" && next === "?") {
18995
+ depth += 1;
18996
+ repaired += "%?";
18997
+ index += 2;
18998
+ continue;
18999
+ }
19000
+ if (here === "%" && next === ";") {
19001
+ if (depth > 0) {
19002
+ depth -= 1;
19003
+ repaired += "%;";
19004
+ }
19005
+ index += 2;
19006
+ continue;
19007
+ }
19008
+ repaired += here;
19009
+ index += 1;
19010
+ }
19011
+ return repaired;
19012
+ }
19013
+ function installBlessedSetulcShim(blessedModule = blessed) {
19014
+ const tput = blessedModule.Tput;
19015
+ const proto = tput?.prototype;
19016
+ const original = proto?._compile;
19017
+ if (proto === void 0 || typeof original !== "function") {
19018
+ return;
19019
+ }
19020
+ if (proto[SHIM_FLAG] === true) {
19021
+ return;
19022
+ }
19023
+ const patched = function patchedCompile(info, key, str) {
19024
+ const safeStr = typeof str === "string" ? balanceTerminfoConditionals(str) : str;
19025
+ return original.call(this, info, key, safeStr);
19026
+ };
19027
+ proto._compile = patched;
19028
+ Object.defineProperty(proto, SHIM_FLAG, {
19029
+ value: true,
19030
+ enumerable: false,
19031
+ configurable: true,
19032
+ writable: true
19033
+ });
19034
+ }
19035
+ var SHIM_FLAG;
19036
+ var init_blessedTputSetulcShim = __esm({
19037
+ "src/blessedTputSetulcShim.ts"() {
19038
+ "use strict";
19039
+ SHIM_FLAG = "__linzumiSetulcShimInstalled";
19040
+ }
19041
+ });
19042
+
19043
+ // src/runnerConsoleReporter.ts
19044
+ import blessed2 from "blessed";
18531
19045
  function reportRunnerConsoleEvent(event, payload) {
18532
19046
  if (shouldRenderDashboard()) {
18533
19047
  const tui = initializeDashboardTui(dashboardState);
@@ -19250,7 +19764,9 @@ function ignoredMessage(payload) {
19250
19764
  `reason=${text(payload.reason)}`,
19251
19765
  optionalField("type", payload.type),
19252
19766
  optionalField("thread", payload.thread_id),
19767
+ optionalField("bound_thread", payload.bound_thread_id),
19253
19768
  optionalField("body_chars", payload.body_length),
19769
+ optionalField("body", payload.body_preview),
19254
19770
  optionalField("attachments", payload.attachment_count),
19255
19771
  optionalField("local_event", payload.local_runner_event_type)
19256
19772
  ].filter((part) => part !== void 0).join(" ");
@@ -19510,20 +20026,21 @@ function handleDashboardKey(state, key, exitProcess = () => process.kill(process
19510
20026
  function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill(process.pid, "SIGINT")) {
19511
20027
  let rendering = false;
19512
20028
  let rawScrollAnchor;
19513
- const screen = blessed.screen({
20029
+ installBlessedSetulcShim();
20030
+ const screen = blessed2.screen({
19514
20031
  title: "Linzumi Commander",
19515
20032
  smartCSR: true,
19516
20033
  fullUnicode: true,
19517
20034
  mouse: true
19518
20035
  });
19519
- const header = blessed.box({
20036
+ const header = blessed2.box({
19520
20037
  top: 0,
19521
20038
  left: 0,
19522
20039
  width: "100%",
19523
20040
  height: 1,
19524
20041
  tags: false
19525
20042
  });
19526
- const tableElement = blessed.listtable({
20043
+ const tableElement = blessed2.listtable({
19527
20044
  top: 1,
19528
20045
  left: 0,
19529
20046
  width: "100%",
@@ -19552,7 +20069,7 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
19552
20069
  }
19553
20070
  }
19554
20071
  });
19555
- const rawTitle = blessed.box({
20072
+ const rawTitle = blessed2.box({
19556
20073
  top: 0,
19557
20074
  left: 12,
19558
20075
  right: 0,
@@ -19560,7 +20077,7 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
19560
20077
  tags: false,
19561
20078
  hidden: true
19562
20079
  });
19563
- const backButton = blessed.button({
20080
+ const backButton = blessed2.button({
19564
20081
  top: 0,
19565
20082
  left: 0,
19566
20083
  width: 10,
@@ -19575,7 +20092,7 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
19575
20092
  hover: { inverse: true }
19576
20093
  }
19577
20094
  });
19578
- const rawBox = blessed.box({
20095
+ const rawBox = blessed2.box({
19579
20096
  top: 3,
19580
20097
  left: 0,
19581
20098
  width: "100%",
@@ -20054,6 +20571,7 @@ var dashboardState, maxRawLines, escapeKey, ctrlCKey, enterKey, upKey, downKey,
20054
20571
  var init_runnerConsoleReporter = __esm({
20055
20572
  "src/runnerConsoleReporter.ts"() {
20056
20573
  "use strict";
20574
+ init_blessedTputSetulcShim();
20057
20575
  dashboardState = {
20058
20576
  jobs: /* @__PURE__ */ new Map(),
20059
20577
  discovery: /* @__PURE__ */ new Map(),
@@ -21379,6 +21897,7 @@ import { spawn as spawn9, spawnSync as spawnSync5 } from "node:child_process";
21379
21897
  import { createHash as createHash5, randomUUID as randomUUID4 } from "node:crypto";
21380
21898
  import {
21381
21899
  chmodSync as chmodSync2,
21900
+ existsSync as existsSync13,
21382
21901
  lstatSync,
21383
21902
  mkdirSync as mkdirSync12,
21384
21903
  mkdtempSync as mkdtempSync4,
@@ -21393,6 +21912,7 @@ import {
21393
21912
  import { readFile as readFile2 } from "node:fs/promises";
21394
21913
  import { createServer as createServer3 } from "node:http";
21395
21914
  import { homedir as homedir13, hostname as hostname2, tmpdir as tmpdir3 } from "node:os";
21915
+ import { createInterface } from "node:readline";
21396
21916
  import {
21397
21917
  basename as basename8,
21398
21918
  dirname as dirname13,
@@ -21417,42 +21937,35 @@ async function runLocalCodexRunner(options) {
21417
21937
  });
21418
21938
  try {
21419
21939
  if (options.machineId !== void 0) {
21420
- let runnerLock;
21421
- try {
21422
- runnerLock = acquireRunnerLock({
21423
- machineId: options.machineId,
21424
- runnerId: options.runnerId,
21425
- cwd: options.cwd,
21426
- workspace: runnerWorkspaceSlug(options) ?? null,
21427
- linzumiUrl: options.kandanUrl,
21428
- launchSource: options.launchSource,
21429
- configPath: options.runnerLockConfigPath,
21430
- // Wedged-runner takeover: a lock holder whose pid is alive but
21431
- // whose lock heartbeat went stale (>3 min) gets SIGKILLed and its
21432
- // lock replaced, instead of blocking recovery until someone runs
21433
- // kill -9 by hand. Log it so the runner log explains where the old
21434
- // pid went.
21435
- onTakeover: (takeover) => {
21436
- log2("runner.lock_takeover", {
21437
- runnerId: options.runnerId,
21438
- holderRunnerId: takeover.holderRunnerId,
21439
- holderPid: takeover.holderPid,
21440
- reason: takeover.reason,
21441
- lockPath: takeover.lockPath
21442
- });
21443
- }
21444
- });
21445
- } catch (error) {
21446
- if (isRunnerLockHeldError(error)) {
21447
- log2("runner.lock_held_by_live_process", {
21940
+ const machineId = options.machineId;
21941
+ const acquire = () => acquireRunnerLock({
21942
+ machineId,
21943
+ runnerId: options.runnerId,
21944
+ cwd: options.cwd,
21945
+ workspace: runnerWorkspaceSlug(options) ?? null,
21946
+ linzumiUrl: options.kandanUrl,
21947
+ launchSource: options.launchSource,
21948
+ configPath: options.runnerLockConfigPath,
21949
+ // Wedged-runner takeover: a lock holder whose pid is alive but
21950
+ // whose lock heartbeat went stale (>3 min) gets SIGKILLed and its
21951
+ // lock replaced, instead of blocking recovery until someone runs
21952
+ // kill -9 by hand. Log it so the runner log explains where the old
21953
+ // pid went.
21954
+ onTakeover: (takeover) => {
21955
+ log2("runner.lock_takeover", {
21448
21956
  runnerId: options.runnerId,
21449
- heldByRunnerId: error.heldBy.runnerId,
21450
- heldByPid: error.heldBy.pid,
21451
- lockPath: error.lockPath
21957
+ holderRunnerId: takeover.holderRunnerId,
21958
+ holderPid: takeover.holderPid,
21959
+ reason: takeover.reason,
21960
+ lockPath: takeover.lockPath
21452
21961
  });
21453
21962
  }
21454
- throw error;
21455
- }
21963
+ });
21964
+ const runnerLock = await acquireRunnerLockWithConflictResolution(
21965
+ acquire,
21966
+ options,
21967
+ log2
21968
+ );
21456
21969
  cleanup.actions.push(() => runnerLock.release());
21457
21970
  log2("runner.lock_acquired", {
21458
21971
  path: runnerLock.path,
@@ -22472,6 +22985,11 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22472
22985
  if (options.channelSession !== void 0 && codex === void 0) {
22473
22986
  throw new Error("channel session requires a Codex app-server connection");
22474
22987
  }
22988
+ const ownsRespawnableAppServer = runnerOwnsRespawnableAppServer({
22989
+ codexUrl: options.codexUrl,
22990
+ threadProcessRole: options.threadProcess?.role,
22991
+ ownsStartedAppServer: started !== void 0
22992
+ });
22475
22993
  const seq = { value: 0 };
22476
22994
  const discoveredCodexThreads = { value: [] };
22477
22995
  const runtimeDefaults = runnerRuntimeDefaults(options);
@@ -22563,6 +23081,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22563
23081
  const dynamicChannelSessions = /* @__PURE__ */ new Map();
22564
23082
  const dynamicChannelSessionCodexClients = /* @__PURE__ */ new Map();
22565
23083
  const codexTurnFailureGuards = /* @__PURE__ */ new Map();
23084
+ const lossReportPushes = [];
22566
23085
  const codexTurnFailureGuardFor = (client) => {
22567
23086
  const existing = codexTurnFailureGuards.get(client);
22568
23087
  if (existing !== void 0) {
@@ -22575,7 +23094,36 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22575
23094
  ([codexThreadId]) => dynamicChannelSessionCodexClients.get(codexThreadId) === client
22576
23095
  ).map(([, session]) => session)
22577
23096
  ],
22578
- log: log2
23097
+ log: log2,
23098
+ // Spec: plans/2026-06-13-orphaned-mid-turn-reconnect-recovery-spec.md
23099
+ // The runner process and its channel survive an app-server death, so the
23100
+ // server otherwise keeps these thread bindings "connected" and never
23101
+ // offers reconnect. Report the loss per affected thread (best-effort)
23102
+ // so the bindings flip to disconnected/reconnectable upstream.
23103
+ //
23104
+ // Only do this for a runner that owns a respawnable app-server. For an
23105
+ // external/remote `codexUrl` backend (or a thread-process worker) this
23106
+ // runner cannot relaunch the dead app-server, so a reconnect could never
23107
+ // recover the thread - marking it reconnectable would be misleading. In
23108
+ // that case we still fail the in-flight turns terminally, but leave the
23109
+ // binding as-is rather than offering a reconnect that cannot succeed.
23110
+ onActiveThreadsFailed: ownsRespawnableAppServer ? (threadIds) => {
23111
+ for (const codexThreadId of threadIds) {
23112
+ lossReportPushes.push(
23113
+ kandan.push(topic, "session:app_server_lost", {
23114
+ codex_thread_id: codexThreadId
23115
+ }).then(
23116
+ () => void 0,
23117
+ (error) => {
23118
+ log2("kandan.session_app_server_lost_push_failed", {
23119
+ codex_thread_id: codexThreadId,
23120
+ message: error instanceof Error ? error.message : String(error)
23121
+ });
23122
+ }
23123
+ )
23124
+ );
23125
+ }
23126
+ } : void 0
22579
23127
  });
22580
23128
  codexTurnFailureGuards.set(client, guard);
22581
23129
  return guard;
@@ -22592,7 +23140,29 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22592
23140
  );
22593
23141
  dynamicChannelSessions.clear();
22594
23142
  dynamicChannelSessionCodexClients.clear();
22595
- for (const session of activeClaudeCodeSessions.values()) {
23143
+ const claudeSessions = [...activeClaudeCodeSessions.values()];
23144
+ for (const session of claudeSessions) {
23145
+ try {
23146
+ session.closeInput();
23147
+ } catch (error) {
23148
+ log2("claude_code.session_input_close_failed", {
23149
+ linzumi_thread_id: session.threadId,
23150
+ message: error instanceof Error ? error.message : String(error)
23151
+ });
23152
+ }
23153
+ }
23154
+ await Promise.race([
23155
+ Promise.allSettled(
23156
+ claudeSessions.flatMap((session) => {
23157
+ const work = session.sessionWork();
23158
+ return work === void 0 ? [] : [work];
23159
+ })
23160
+ ),
23161
+ new Promise((resolve12) => {
23162
+ setTimeout(resolve12, 1e4).unref?.();
23163
+ })
23164
+ ]);
23165
+ for (const session of claudeSessions) {
22596
23166
  session.abortController.abort(new Error("local runner stopped"));
22597
23167
  }
22598
23168
  activeClaudeCodeSessions.clear();
@@ -22657,7 +23227,8 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22657
23227
  portForwardWatcher: channelSessionPortForwardWatcherOptions({
22658
23228
  rootPid: portForwardWatcherRootPid,
22659
23229
  start: options.portForwardWatcher,
22660
- commanderBoundPids: args.commanderBoundPids
23230
+ commanderBoundPids: args.commanderBoundPids,
23231
+ commanderBoundPorts: args.commanderBoundPorts
22661
23232
  }),
22662
23233
  suppressedForwardPorts,
22663
23234
  onForwardPortApproved: (port, attribution) => {
@@ -22852,6 +23423,12 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22852
23423
  portForwardWatcherRootPid: handle.processPid,
22853
23424
  commanderBoundPids: (handle.commanderManagedPorts ?? []).flatMap(
22854
23425
  (port) => port.pid === void 0 ? [] : [port.pid]
23426
+ ),
23427
+ // The exact codex app-server ports are commander-managed even though
23428
+ // their listening pid is the wrapped grandchild, not the recorded
23429
+ // wrapper pid (Codex P1).
23430
+ commanderBoundPorts: (handle.commanderManagedPorts ?? []).map(
23431
+ (port) => port.port
22855
23432
  )
22856
23433
  });
22857
23434
  switch (control.type) {
@@ -23001,6 +23578,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
23001
23578
  }
23002
23579
  });
23003
23580
  if (sharedCodexTurnFailureGuard !== void 0) {
23581
+ const appServerLost = { handled: false };
23004
23582
  const failCodexSessionTurns = (cause) => {
23005
23583
  if (cleanup.closePromise !== void 0) {
23006
23584
  return;
@@ -23008,6 +23586,27 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
23008
23586
  log2("codex.app_server_connection_lost", { cause });
23009
23587
  sharedCodexTurnFailureGuard.failActiveTurns(cause);
23010
23588
  started?.stop();
23589
+ if (ownsRespawnableAppServer && !appServerLost.handled) {
23590
+ appServerLost.handled = true;
23591
+ console.error(
23592
+ `Codex app-server connection was lost (${cause}); exiting so a fresh runner can relaunch it. Run \`linzumi connect\` to reconnect.`
23593
+ );
23594
+ const sessionDrains = [
23595
+ ...channelSession !== void 0 ? [channelSession] : [],
23596
+ ...dynamicChannelSessions.values()
23597
+ ].map(
23598
+ (session) => session.close().catch((error) => {
23599
+ log2("codex.app_server_lost_session_drain_failed", {
23600
+ message: error instanceof Error ? error.message : String(error)
23601
+ });
23602
+ })
23603
+ );
23604
+ void awaitLossReportsThenExit({
23605
+ pushes: [...lossReportPushes, ...sessionDrains],
23606
+ exit: () => process.exit(75),
23607
+ safetyCapMs: 5e3
23608
+ });
23609
+ }
23011
23610
  };
23012
23611
  codex?.onClose?.((error) => failCodexSessionTurns(error.message));
23013
23612
  started?.process.once(
@@ -23763,6 +24362,19 @@ async function resolveSessionControl(channelSession, dynamicChannelSessions, con
23763
24362
  }
23764
24363
  return void 0;
23765
24364
  }
24365
+ function runnerOwnsRespawnableAppServer(args) {
24366
+ return args.codexUrl === void 0 && args.threadProcessRole !== "thread" && args.ownsStartedAppServer;
24367
+ }
24368
+ async function awaitLossReportsThenExit(args) {
24369
+ const setTimeoutImpl = args.setTimeoutFn ?? setTimeout;
24370
+ const drained = Promise.allSettled(args.pushes).then(() => void 0);
24371
+ const safetyCap = new Promise((resolve12) => {
24372
+ const handle = setTimeoutImpl(() => resolve12(), args.safetyCapMs);
24373
+ handle?.unref?.();
24374
+ });
24375
+ await Promise.race([drained, safetyCap]);
24376
+ args.exit();
24377
+ }
23766
24378
  function createCodexTurnFailureGuard(args) {
23767
24379
  const activeTurnsByThread = /* @__PURE__ */ new Map();
23768
24380
  const state = { failed: false };
@@ -23798,6 +24410,7 @@ function createCodexTurnFailureGuard(args) {
23798
24410
  if (activeTurns.length === 0) {
23799
24411
  return;
23800
24412
  }
24413
+ args.onActiveThreadsFailed?.(activeTurns.map(([threadId]) => threadId));
23801
24414
  const sessions = Array.from(args.sessions());
23802
24415
  for (const [threadId, turnId] of activeTurns) {
23803
24416
  args.log("codex.active_turn_failed_on_connection_loss", {
@@ -23955,6 +24568,101 @@ function installCleanupHandlers(close) {
23955
24568
  process.off("exit", closeOnExit);
23956
24569
  };
23957
24570
  }
24571
+ async function acquireRunnerLockWithConflictResolution(acquire, options, log2) {
24572
+ try {
24573
+ return acquire();
24574
+ } catch (error) {
24575
+ if (!isRunnerLockHeldError(error)) {
24576
+ throw error;
24577
+ }
24578
+ log2("runner.lock_held_by_live_process", {
24579
+ runnerId: options.runnerId,
24580
+ heldByRunnerId: error.heldBy.runnerId,
24581
+ heldByPid: error.heldBy.pid,
24582
+ lockPath: error.lockPath
24583
+ });
24584
+ const lockTakeover = options.lockTakeover;
24585
+ if (!shouldResolveRunnerLockConflict(lockTakeover)) {
24586
+ throw error;
24587
+ }
24588
+ const promptDeps = lockTakeover.prompt ?? defaultRunnerLockTakeoverPromptDeps(lockTakeover);
24589
+ const takeoverDeps = resolveRunnerLockTakeoverDeps(
24590
+ error.lockPath,
24591
+ lockTakeover.takeover,
24592
+ log2
24593
+ );
24594
+ const resolution = await resolveRunnerLockConflict({
24595
+ holder: error.heldBy,
24596
+ lockPath: error.lockPath,
24597
+ baseMessage: error.message,
24598
+ takeOverWithoutPrompt: lockTakeover.takeOverWithoutPrompt === true,
24599
+ prompt: promptDeps,
24600
+ takeover: takeoverDeps
24601
+ });
24602
+ if (resolution.outcome === "declined") {
24603
+ throw new RunnerLockConflictReportedError();
24604
+ }
24605
+ log2("runner.lock_takeover_interactive", {
24606
+ runnerId: options.runnerId,
24607
+ heldByRunnerId: error.heldBy.runnerId,
24608
+ heldByPid: error.heldBy.pid,
24609
+ lockPath: error.lockPath
24610
+ });
24611
+ return acquire();
24612
+ }
24613
+ }
24614
+ function defaultRunnerLockTakeoverPromptDeps(takeover) {
24615
+ return {
24616
+ isInteractive: () => takeover?.forceNonInteractive !== true && process.stdin.isTTY === true && process.stderr.isTTY === true,
24617
+ writeReport: (text2) => {
24618
+ process.stderr.write(`${text2}
24619
+ `);
24620
+ },
24621
+ promptYesNo: (question) => promptYesNoOnStdin(question)
24622
+ };
24623
+ }
24624
+ function promptYesNoOnStdin(question) {
24625
+ return new Promise((resolve12) => {
24626
+ const rl = createInterface({
24627
+ input: process.stdin,
24628
+ output: process.stderr
24629
+ });
24630
+ rl.question(question, (answer) => {
24631
+ rl.close();
24632
+ resolve12(/^y(es)?$/i.test(answer.trim()));
24633
+ });
24634
+ });
24635
+ }
24636
+ function resolveRunnerLockTakeoverDeps(lockPath, overrides, log2) {
24637
+ return {
24638
+ isPidAlive: overrides?.isPidAlive ?? runnerLockTakeoverPidIsAlive,
24639
+ signalPid: overrides?.signalPid ?? ((pid, signal) => {
24640
+ process.kill(pid, signal);
24641
+ }),
24642
+ lockReleased: overrides?.lockReleased ?? (() => !existsSync13(lockPath)),
24643
+ // Re-reads the on-disk owner so the takeover flow can revalidate identity
24644
+ // before signaling (never throws on a torn/partial lock file).
24645
+ currentLockOwner: overrides?.currentLockOwner ?? (() => readRunnerLockOwner(lockPath)),
24646
+ // runnerLockTakeoverSleep is deliberately NOT unref'ed - see its definition;
24647
+ // an unref'ed timer here would let `linzumi connect` exit before retrying
24648
+ // the lock acquisition during a takeover.
24649
+ sleep: overrides?.sleep ?? runnerLockTakeoverSleep,
24650
+ now: overrides?.now ?? (() => Date.now()),
24651
+ log: overrides?.log ?? ((event, payload) => {
24652
+ log2(event, payload);
24653
+ writeCliAuditEvent(event, payload);
24654
+ })
24655
+ };
24656
+ }
24657
+ function runnerLockTakeoverPidIsAlive(pid) {
24658
+ try {
24659
+ process.kill(pid, 0);
24660
+ return true;
24661
+ } catch (error) {
24662
+ const code = error !== null && typeof error === "object" && "code" in error ? error.code : void 0;
24663
+ return code !== "ESRCH";
24664
+ }
24665
+ }
23958
24666
  function launchCodexTui(codexBin, codexUrl, cwd, codexThreadId, session, fast) {
23959
24667
  const args = codexTuiArgs(codexUrl, codexThreadId, session, fast);
23960
24668
  writeCliAuditEvent("process.spawn", {
@@ -24449,10 +25157,28 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
24449
25157
  };
24450
25158
  }
24451
25159
  try {
24452
- activeSession.enqueueInput({
25160
+ const enqueueResult = activeSession.enqueueInput({
24453
25161
  content: contentResult.content,
24454
25162
  sourceSeq
24455
25163
  });
25164
+ if (enqueueResult === "duplicate") {
25165
+ log2("claude_code.message_duplicate_ignored", {
25166
+ linzumi_thread_id: optionalThreadControlField(control, "threadId") ?? null,
25167
+ claude_session_id: codexThreadId,
25168
+ body_preview: claudeConsoleBodyPreview(workDescription),
25169
+ source_seq: sourceSeq ?? null
25170
+ });
25171
+ return {
25172
+ instanceId,
25173
+ controlType: control.type,
25174
+ agentProvider: "claude-code",
25175
+ cwd: cwd.cwd,
25176
+ matchedRoot: cwd.matchedRoot,
25177
+ codexThreadId,
25178
+ queuedInput: true,
25179
+ duplicate: true
25180
+ };
25181
+ }
24456
25182
  log2("claude_code.message_queued", {
24457
25183
  linzumi_thread_id: optionalThreadControlField(control, "threadId") ?? null,
24458
25184
  claude_session_id: codexThreadId,
@@ -24956,7 +25682,24 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
24956
25682
  };
24957
25683
  }
24958
25684
  case "stop_instance":
24959
- case "kill_instance":
25685
+ case "kill_instance": {
25686
+ const controlThreadId2 = "threadId" in control && typeof control.threadId === "string" ? control.threadId : void 0;
25687
+ const activeSession = controlThreadId2 === void 0 ? void 0 : activeClaudeCodeSessions.get(controlThreadId2);
25688
+ if (activeSession !== void 0) {
25689
+ await activeSession.interruptTurn("Claude Code stopped by Linzumi");
25690
+ activeSession.abortController.abort(
25691
+ new Error("Claude Code stopped by Linzumi")
25692
+ );
25693
+ return {
25694
+ instanceId,
25695
+ controlType: control.type,
25696
+ agentProvider: "claude-code",
25697
+ threadId: controlThreadId2,
25698
+ stopped: true
25699
+ };
25700
+ }
25701
+ return { instanceId, controlType: control.type, skipped: true };
25702
+ }
24960
25703
  case "resolve_port_forward_request":
24961
25704
  case "update_thread_interaction_access":
24962
25705
  case "set_port_forward_enabled":
@@ -25456,10 +26199,10 @@ function createClaudeCodeInputQueue(input) {
25456
26199
  claudeCodeUserInputMessage(input.content)
25457
26200
  ];
25458
26201
  const pendingSourceSeqs = [input.sourceSeq];
26202
+ const deferredFollowUps = [];
25459
26203
  const waiters = [];
25460
26204
  const state = {
25461
- closed: false,
25462
- queuedTurnCount: 1
26205
+ closed: false
25463
26206
  };
25464
26207
  const drainClosedWaiters = () => {
25465
26208
  while (state.closed) {
@@ -25470,19 +26213,25 @@ function createClaudeCodeInputQueue(input) {
25470
26213
  waiter({ done: true, value: void 0 });
25471
26214
  }
25472
26215
  };
26216
+ const deliverNow = (message, sourceSeq) => {
26217
+ pendingSourceSeqs.push(sourceSeq);
26218
+ const waiter = waiters.shift();
26219
+ if (waiter === void 0) {
26220
+ pendingMessages.push(message);
26221
+ return;
26222
+ }
26223
+ waiter({ done: false, value: message });
26224
+ };
25473
26225
  const enqueue = (next) => {
25474
26226
  if (state.closed) {
25475
26227
  throw new Error("Claude Code streaming input is closed");
25476
26228
  }
25477
26229
  const message = claudeCodeUserInputMessage(next.content);
25478
- pendingSourceSeqs.push(next.sourceSeq);
25479
- state.queuedTurnCount += 1;
25480
- const waiter = waiters.shift();
25481
- if (waiter === void 0) {
25482
- pendingMessages.push(message);
26230
+ if (pendingSourceSeqs.length > 0) {
26231
+ deferredFollowUps.push({ message, sourceSeq: next.sourceSeq });
25483
26232
  return;
25484
26233
  }
25485
- waiter({ done: false, value: message });
26234
+ deliverNow(message, next.sourceSeq);
25486
26235
  };
25487
26236
  const enqueueSteer = (content) => {
25488
26237
  if (state.closed) {
@@ -25520,14 +26269,30 @@ function createClaudeCodeInputQueue(input) {
25520
26269
  };
25521
26270
  }
25522
26271
  },
25523
- queuedTurnCount: () => state.queuedTurnCount,
25524
26272
  close: () => {
26273
+ if (pendingSourceSeqs.length === 0) {
26274
+ while (deferredFollowUps.length > 0) {
26275
+ const followUp = deferredFollowUps.shift();
26276
+ if (followUp !== void 0) {
26277
+ deliverNow(followUp.message, followUp.sourceSeq);
26278
+ }
26279
+ }
26280
+ }
25525
26281
  state.closed = true;
25526
26282
  drainClosedWaiters();
25527
26283
  },
25528
26284
  enqueue,
25529
26285
  enqueueSteer,
25530
- completeTurn: () => pendingSourceSeqs.shift(),
26286
+ completeTurn: () => {
26287
+ const completed = pendingSourceSeqs.shift();
26288
+ if (!state.closed && pendingSourceSeqs.length === 0) {
26289
+ const nextFollowUp = deferredFollowUps.shift();
26290
+ if (nextFollowUp !== void 0) {
26291
+ deliverNow(nextFollowUp.message, nextFollowUp.sourceSeq);
26292
+ }
26293
+ }
26294
+ return completed;
26295
+ },
25531
26296
  currentSourceSeq: () => pendingSourceSeqs[0]
25532
26297
  };
25533
26298
  }
@@ -25775,6 +26540,21 @@ async function startClaudeCodeProviderInstance(args) {
25775
26540
  codexVersion: void 0
25776
26541
  };
25777
26542
  const abortController = new AbortController();
26543
+ const reaperRegistration = registerEngineChild({
26544
+ stop: () => {
26545
+ if (!abortController.signal.aborted) {
26546
+ abortController.abort(new Error("engine child reaper teardown"));
26547
+ }
26548
+ },
26549
+ pid: void 0,
26550
+ ownProcessGroup: false,
26551
+ kind: "claude_code"
26552
+ });
26553
+ const acceptedSourceSeqs = new Set(
26554
+ sourceSeq === void 0 ? [] : [sourceSeq]
26555
+ );
26556
+ const sessionWorkHandle = { value: void 0 };
26557
+ let startControlResponded = false;
25778
26558
  let activeSessionId;
25779
26559
  let sessionControls;
25780
26560
  const planMirrorClient = createLinzumiMcpApiClient({
@@ -25909,7 +26689,7 @@ async function startClaudeCodeProviderInstance(args) {
25909
26689
  rootSeq,
25910
26690
  log: args.log
25911
26691
  };
25912
- let lastRateLimitSignature;
26692
+ const lastRateLimitSignatureByWindow = /* @__PURE__ */ new Map();
25913
26693
  const reportClaudeCodeRateLimit = async (event) => {
25914
26694
  const nowMs = Date.now();
25915
26695
  const summary = claudeCodeRateLimitSummaryText(event.rateLimit, nowMs);
@@ -25925,13 +26705,13 @@ async function startClaudeCodeProviderInstance(args) {
25925
26705
  utilization_percent: event.rateLimit.utilizationPercent ?? null,
25926
26706
  resets_at_ms: event.rateLimit.resetsAtMs ?? null
25927
26707
  });
26708
+ const windowKey = event.rateLimit.rateLimitType ?? "account";
25928
26709
  const signature = claudeRateLimitSignature(event.rateLimit);
25929
- if (signature === lastRateLimitSignature) {
26710
+ if (signature === lastRateLimitSignatureByWindow.get(windowKey)) {
25930
26711
  return;
25931
26712
  }
25932
- lastRateLimitSignature = signature;
25933
26713
  try {
25934
- await postClaudeCodeRateLimitMessage({
26714
+ await publishClaudeCodeRateLimitState({
25935
26715
  kandan: args.kandan,
25936
26716
  topic: args.topic,
25937
26717
  workspace,
@@ -25940,6 +26720,7 @@ async function startClaudeCodeProviderInstance(args) {
25940
26720
  claudeSessionId: event.sessionId ?? activeSessionId,
25941
26721
  rateLimit: event.rateLimit
25942
26722
  });
26723
+ lastRateLimitSignatureByWindow.set(windowKey, signature);
25943
26724
  } catch (error) {
25944
26725
  args.log("claude_code.rate_limit_post_failed", {
25945
26726
  thread_id: threadId,
@@ -25947,6 +26728,10 @@ async function startClaudeCodeProviderInstance(args) {
25947
26728
  });
25948
26729
  }
25949
26730
  };
26731
+ let settleFirstTurn = () => void 0;
26732
+ const firstTurnSettled = new Promise((resolve12) => {
26733
+ settleFirstTurn = resolve12;
26734
+ });
25950
26735
  const onTranscriptEvent = async (event) => {
25951
26736
  try {
25952
26737
  await mirrorClaudeSessionStoreAppend(mirrorArgs, event);
@@ -25969,7 +26754,19 @@ async function startClaudeCodeProviderInstance(args) {
25969
26754
  channel,
25970
26755
  threadId,
25971
26756
  currentSourceSeq: inputQueue.currentSourceSeq,
25972
- enqueueInput: inputQueue.enqueue,
26757
+ enqueueInput: (input) => {
26758
+ if (input.sourceSeq !== void 0 && acceptedSourceSeqs.has(input.sourceSeq)) {
26759
+ return "duplicate";
26760
+ }
26761
+ inputQueue.enqueue(input);
26762
+ if (input.sourceSeq !== void 0) {
26763
+ acceptedSourceSeqs.add(input.sourceSeq);
26764
+ }
26765
+ return "queued";
26766
+ },
26767
+ closeInput: inputQueue.close,
26768
+ hasActiveTurn: () => adapter.activeTurnId() !== void 0,
26769
+ sessionWork: () => sessionWorkHandle.value,
25973
26770
  steerTurn: inputQueue.enqueueSteer,
25974
26771
  interruptTurn: async (reason) => {
25975
26772
  const aborted = adapter.interruptActiveTurn(reason);
@@ -26025,6 +26822,10 @@ async function startClaudeCodeProviderInstance(args) {
26025
26822
  claude_session_id: event.sessionId,
26026
26823
  body_preview: claudeConsoleBodyPreview(event.body)
26027
26824
  });
26825
+ settleFirstTurn();
26826
+ }
26827
+ if (event.type === "turn_interrupted") {
26828
+ settleFirstTurn();
26028
26829
  }
26029
26830
  if (event.type === "rate_limit") {
26030
26831
  await reportClaudeCodeRateLimit(event);
@@ -26071,80 +26872,98 @@ async function startClaudeCodeProviderInstance(args) {
26071
26872
  request,
26072
26873
  signal
26073
26874
  });
26074
- let result;
26075
- try {
26875
+ const runSession = async () => {
26876
+ let result2;
26076
26877
  try {
26077
- result = await startClaudeCodeSession({
26078
- cwd: args.cwd,
26079
- prompt: workDescription,
26080
- developerInstructions: commanderDeveloperInstructions({
26878
+ try {
26879
+ result2 = await startClaudeCodeSession({
26081
26880
  cwd: args.cwd,
26082
- agentLabel: "Claude Code",
26083
- planTool: "todo-write",
26084
- developerPrompt,
26085
- linzumiContext
26086
- }),
26087
- model: claudeCodeModelForRuntimeModel(runtimeSettings.model),
26088
- resumeSessionId: args.resumeSessionId,
26089
- abortController,
26090
- streamingInput: inputQueue,
26091
- runner: args.options.claudeCodeRunner,
26092
- canUseTool,
26093
- ...mcpServers === void 0 ? {} : { mcpServers },
26094
- ...liveBashCapture === void 0 ? {} : {
26095
- preToolUseHookMatchers: liveBashCapture.hookMatchers,
26096
- wrapApprovedToolInput: (toolUseId, toolName2, input) => toolName2 === "Bash" ? liveBashCapture.wrapApprovedTool(toolUseId, input) : void 0
26097
- },
26098
- env: sessionEnv,
26099
- // The built-in Linzumi MCP is trusted like the codex side: its tools
26100
- // run without an approval round-trip. SDK allowedTools matches MCP
26101
- // tools as mcp__<server>__<tool>; the __* suffix covers the server.
26102
- allowedTools: ["mcp__linzumi__*"],
26103
- onSessionControls: (controls) => {
26104
- sessionControls = controls;
26105
- },
26106
- onTurnCompleted: () => {
26107
- inputQueue.completeTurn();
26108
- },
26109
- onTranscriptEvent
26110
- });
26111
- } finally {
26112
- inputQueue.close();
26113
- mcpAuthCleanup?.();
26114
- liveBashCapture?.close();
26115
- if (activeSessionId !== void 0) {
26116
- args.activeClaudeCodeSessions.delete(activeSessionId);
26117
- await args.disposeClaudeCodeForwardPortSession?.(activeSessionId);
26881
+ prompt: workDescription,
26882
+ developerInstructions: commanderDeveloperInstructions({
26883
+ cwd: args.cwd,
26884
+ agentLabel: "Claude Code",
26885
+ planTool: "todo-write",
26886
+ developerPrompt,
26887
+ linzumiContext
26888
+ }),
26889
+ model: claudeCodeModelForRuntimeModel(runtimeSettings.model),
26890
+ resumeSessionId: args.resumeSessionId,
26891
+ abortController,
26892
+ streamingInput: inputQueue,
26893
+ runner: args.options.claudeCodeRunner,
26894
+ canUseTool,
26895
+ ...mcpServers === void 0 ? {} : { mcpServers },
26896
+ ...liveBashCapture === void 0 ? {} : {
26897
+ preToolUseHookMatchers: liveBashCapture.hookMatchers,
26898
+ wrapApprovedToolInput: (toolUseId, toolName2, input) => toolName2 === "Bash" ? liveBashCapture.wrapApprovedTool(toolUseId, input) : void 0
26899
+ },
26900
+ env: sessionEnv,
26901
+ // The built-in Linzumi MCP is trusted like the codex side: its tools
26902
+ // run without an approval round-trip. SDK allowedTools matches MCP
26903
+ // tools as mcp__<server>__<tool>; the __* suffix covers the server.
26904
+ allowedTools: ["mcp__linzumi__*"],
26905
+ onSessionControls: (controls) => {
26906
+ sessionControls = controls;
26907
+ },
26908
+ onTurnCompleted: () => {
26909
+ inputQueue.completeTurn();
26910
+ settleFirstTurn();
26911
+ },
26912
+ onTranscriptEvent
26913
+ });
26914
+ } finally {
26915
+ inputQueue.close();
26916
+ mcpAuthCleanup?.();
26917
+ liveBashCapture?.close();
26918
+ if (activeSessionId !== void 0) {
26919
+ args.activeClaudeCodeSessions.delete(activeSessionId);
26920
+ await args.disposeClaudeCodeForwardPortSession?.(activeSessionId);
26921
+ }
26922
+ }
26923
+ } catch (error) {
26924
+ adapter.submitLifecycle(
26925
+ "codexDied",
26926
+ error instanceof Error ? error.message : String(error)
26927
+ );
26928
+ await adapter.close(5e3).catch(() => void 0);
26929
+ await Promise.race([
26930
+ planMirror.settle().catch(() => void 0),
26931
+ new Promise((resolve12) => {
26932
+ setTimeout(resolve12, 5e3).unref?.();
26933
+ })
26934
+ ]);
26935
+ logStartFailureTerminal(
26936
+ error instanceof Error ? error.message : String(error)
26937
+ );
26938
+ if (!startControlResponded) {
26939
+ args.setStartupStage("claude_code_session_failed");
26118
26940
  }
26941
+ throw error;
26119
26942
  }
26120
- } catch (error) {
26121
- adapter.submitLifecycle(
26122
- "codexDied",
26123
- error instanceof Error ? error.message : String(error)
26124
- );
26125
- await adapter.close(5e3).catch(() => void 0);
26943
+ if (!startControlResponded) {
26944
+ args.setStartupStage("posting_claude_code_output");
26945
+ }
26946
+ await adapter.awaitTurnsSettled(3e4);
26947
+ await adapter.close(3e4);
26126
26948
  await Promise.race([
26127
- planMirror.settle().catch(() => void 0),
26949
+ planMirror.settle(),
26128
26950
  new Promise((resolve12) => {
26129
- setTimeout(resolve12, 5e3).unref?.();
26951
+ setTimeout(resolve12, 1e4).unref?.();
26130
26952
  })
26131
26953
  ]);
26132
- logStartFailureTerminal(
26133
- error instanceof Error ? error.message : String(error)
26134
- );
26135
- args.setStartupStage("claude_code_session_failed");
26136
- throw error;
26137
- }
26138
- args.setStartupStage("posting_claude_code_output");
26139
- await adapter.awaitTurnsSettled(3e4);
26140
- await adapter.close(3e4);
26141
- await Promise.race([
26142
- planMirror.settle(),
26143
- new Promise((resolve12) => {
26144
- setTimeout(resolve12, 1e4).unref?.();
26145
- })
26954
+ return result2;
26955
+ };
26956
+ const sessionWork = runSession();
26957
+ void sessionWork.finally(() => reaperRegistration.unregister()).catch(() => void 0);
26958
+ sessionWorkHandle.value = sessionWork;
26959
+ const settled = await Promise.race([
26960
+ sessionWork.then((result2) => ({
26961
+ type: "session_ended",
26962
+ result: result2
26963
+ })),
26964
+ firstTurnSettled.then(() => ({ type: "first_turn_settled" }))
26146
26965
  ]);
26147
- return {
26966
+ const respondWithSessionId = (sessionId) => ({
26148
26967
  controlResponse: {
26149
26968
  instanceId: args.instanceId,
26150
26969
  controlType: args.control.type,
@@ -26152,16 +26971,52 @@ async function startClaudeCodeProviderInstance(args) {
26152
26971
  cwd: args.cwd,
26153
26972
  matchedRoot: args.matchedRoot,
26154
26973
  response: {
26155
- session: { id: result.sessionId }
26974
+ session: { id: sessionId }
26156
26975
  }
26157
26976
  },
26158
26977
  providerSession: {
26159
26978
  provider: "claude-code",
26160
26979
  cwd: args.cwd,
26161
26980
  matchedRoot: args.matchedRoot,
26162
- sessionId: result.sessionId
26981
+ sessionId
26163
26982
  }
26164
- };
26983
+ });
26984
+ if (settled.type === "session_ended") {
26985
+ return respondWithSessionId(settled.result.sessionId);
26986
+ }
26987
+ args.setStartupStage("posting_claude_code_output");
26988
+ startControlResponded = true;
26989
+ await adapter.awaitTurnsSettled(3e4);
26990
+ try {
26991
+ await adapter.flush(3e4);
26992
+ } catch (error) {
26993
+ args.log("claude_code.first_turn_flush_timeout", {
26994
+ linzumi_thread_id: threadId,
26995
+ claude_session_id: activeSessionId ?? null,
26996
+ message: error instanceof Error ? error.message : String(error)
26997
+ });
26998
+ }
26999
+ void sessionWork.then(
27000
+ () => {
27001
+ args.log("claude_code.session_loop_ended", {
27002
+ linzumi_thread_id: threadId,
27003
+ claude_session_id: activeSessionId ?? null
27004
+ });
27005
+ },
27006
+ (error) => {
27007
+ args.log("claude_code.session_loop_failed", {
27008
+ linzumi_thread_id: threadId,
27009
+ claude_session_id: activeSessionId ?? null,
27010
+ message: error instanceof Error ? error.message : String(error)
27011
+ });
27012
+ }
27013
+ );
27014
+ const liveSessionId = activeSessionId ?? args.resumeSessionId;
27015
+ if (liveSessionId !== void 0) {
27016
+ return respondWithSessionId(liveSessionId);
27017
+ }
27018
+ const result = await sessionWork;
27019
+ return respondWithSessionId(result.sessionId);
26165
27020
  }
26166
27021
  function claudePermissionModeForApprovalPolicy(approvalPolicy) {
26167
27022
  switch (approvalPolicy) {
@@ -26403,50 +27258,26 @@ function claudeRateLimitSignature(rateLimit) {
26403
27258
  rateLimit.resetsAtMs ?? "na"
26404
27259
  ].join(":");
26405
27260
  }
26406
- async function postClaudeCodeRateLimitMessage(args) {
26407
- const nowMs = Date.now();
26408
- const summary = claudeCodeRateLimitSummaryText(args.rateLimit, nowMs);
26409
- const headline = `Claude Code rate limit: ${summary}`;
26410
- const resetLabel = claudeCodeRateLimitResetLabel(
26411
- args.rateLimit.resetsAtMs,
26412
- nowMs
26413
- );
26414
- const richUi = args.rateLimit.utilizationPercent === void 0 ? [{ primitive: "label", label: headline }] : [
26415
- {
26416
- primitive: "progress",
26417
- value: Math.min(
26418
- 100,
26419
- Math.max(0, Math.round(args.rateLimit.utilizationPercent))
26420
- ),
26421
- max: 100,
26422
- label: `Claude Code rate limit${resetLabel === void 0 ? "" : ` - ${resetLabel}`}`
26423
- }
26424
- ];
26425
- await args.kandan.push(args.topic, "session:post_thread_message", {
27261
+ async function publishClaudeCodeRateLimitState(args) {
27262
+ await args.kandan.push(args.topic, "claude_rate_limit_state", {
26426
27263
  workspace: args.workspace,
26427
27264
  channel: args.channel,
26428
27265
  thread_id: args.threadId,
26429
- body: headline,
26430
- payload: {
26431
- rich_ui: richUi,
26432
- metadata: {
26433
- local_codex_runner: {
26434
- event_type: "claude_code_rate_limit",
26435
- agent_provider: "claude-code",
26436
- ...args.claudeSessionId === void 0 ? {} : { claude_session_id: args.claudeSessionId }
26437
- }
26438
- }
27266
+ agent_provider: "claude-code",
27267
+ ...args.claudeSessionId === void 0 ? {} : { claude_session_id: args.claudeSessionId },
27268
+ // "account" mirrors the server-side default for SDK frames that omit a
27269
+ // window type; sending it explicitly keeps the wire contract obvious.
27270
+ rate_limit_type: args.rateLimit.rateLimitType ?? "account",
27271
+ status: args.rateLimit.status ?? "allowed",
27272
+ ...args.rateLimit.utilizationPercent === void 0 ? {} : {
27273
+ utilization_percent: Math.min(
27274
+ 100,
27275
+ Math.max(0, Math.round(args.rateLimit.utilizationPercent))
27276
+ )
26439
27277
  },
26440
- client_message_id: claudeRateLimitClientMessageId(
26441
- args.threadId,
26442
- args.rateLimit
26443
- )
27278
+ ...args.rateLimit.resetsAtMs === void 0 ? {} : { resets_at_ms: args.rateLimit.resetsAtMs }
26444
27279
  });
26445
27280
  }
26446
- function claudeRateLimitClientMessageId(threadId, rateLimit) {
26447
- const digest = createHash5("sha256").update(`${threadId}:${claudeRateLimitSignature(rateLimit)}`).digest("hex").slice(0, 32);
26448
- return `claude-rate-limit-${digest}`;
26449
- }
26450
27281
  async function publishStartInstanceMessageState(kandan, topic, control, status, reason, diagnostics = {}) {
26451
27282
  const payload = startInstanceMessageStatePayload(
26452
27283
  control,
@@ -26562,9 +27393,11 @@ function channelSessionPortForwardWatcherOptions(args) {
26562
27393
  ...args.rootPid === void 0 ? [] : [args.rootPid],
26563
27394
  ...args.commanderBoundPids ?? []
26564
27395
  ];
27396
+ const commanderBoundPorts = args.commanderBoundPorts ?? [];
26565
27397
  return {
26566
27398
  ...args.rootPid === void 0 ? {} : { rootPid: args.rootPid },
26567
27399
  ...commanderBoundPids.length === 0 ? {} : { commanderBoundPids },
27400
+ ...commanderBoundPorts.length === 0 ? {} : { commanderBoundPorts },
26568
27401
  ...args.start === void 0 ? {} : { start: args.start }
26569
27402
  };
26570
27403
  }
@@ -28366,6 +29199,7 @@ var init_runner = __esm({
28366
29199
  init_commanderAttachments();
28367
29200
  init_claudeCodePipeline();
28368
29201
  init_claudeCodePlanMirror();
29202
+ init_engineChildReaper();
28369
29203
  init_claudeCodeLiveBashOutput();
28370
29204
  init_claudeCodeSession();
28371
29205
  init_codexAppServer();
@@ -28388,6 +29222,7 @@ var init_runner = __esm({
28388
29222
  init_protocol();
28389
29223
  init_runnerLogger();
28390
29224
  init_runnerLock();
29225
+ init_runnerLockTakeover();
28391
29226
  init_runnerConsoleReporter();
28392
29227
  init_version();
28393
29228
  init_telemetry();
@@ -28428,7 +29263,7 @@ var init_runner = __esm({
28428
29263
  });
28429
29264
 
28430
29265
  // src/kandanTls.ts
28431
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "node:fs";
29266
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "node:fs";
28432
29267
  import { Agent } from "undici";
28433
29268
  import WsWebSocket from "ws";
28434
29269
  function kandanTlsTrustFromEnv() {
@@ -28439,7 +29274,7 @@ function kandanTlsTrustFromCaFile(caFile) {
28439
29274
  return void 0;
28440
29275
  }
28441
29276
  const trimmed = caFile.trim();
28442
- if (!existsSync13(trimmed)) {
29277
+ if (!existsSync14(trimmed)) {
28443
29278
  throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
28444
29279
  }
28445
29280
  const ca = readFileSync17(trimmed, "utf8");
@@ -48717,7 +49552,7 @@ __export(signupFlow_exports, {
48717
49552
  });
48718
49553
  import { spawn as spawn12, spawnSync as spawnSync7 } from "node:child_process";
48719
49554
  import {
48720
- existsSync as existsSync16,
49555
+ existsSync as existsSync17,
48721
49556
  constants as fsConstants,
48722
49557
  mkdirSync as mkdirSync15,
48723
49558
  mkdtempSync as mkdtempSync5,
@@ -48845,7 +49680,7 @@ function defaultSignupDraftStore(serviceUrl) {
48845
49680
  const path2 = defaultSignupDraftPath(serviceUrl);
48846
49681
  return {
48847
49682
  read: () => {
48848
- if (!existsSync16(path2)) {
49683
+ if (!existsSync17(path2)) {
48849
49684
  return void 0;
48850
49685
  }
48851
49686
  let parsed;
@@ -48924,7 +49759,7 @@ function defaultSignupTaskCachePath(serviceUrl) {
48924
49759
  );
48925
49760
  }
48926
49761
  function readSignupTaskCache(path2) {
48927
- if (!existsSync16(path2)) {
49762
+ if (!existsSync17(path2)) {
48928
49763
  return { version: 1, entries: {} };
48929
49764
  }
48930
49765
  let parsed;
@@ -51752,7 +52587,7 @@ async function discoverCodeRoots(homeDir) {
51752
52587
  const candidates = ["src", "code", "projects"].map(
51753
52588
  (name) => join23(homeDir, name)
51754
52589
  );
51755
- return candidates.filter((path2) => existsSync16(path2)).flatMap((path2) => discoveredProjectNames(path2));
52590
+ return candidates.filter((path2) => existsSync17(path2)).flatMap((path2) => discoveredProjectNames(path2));
51756
52591
  }
51757
52592
  function discoveredProjectNames(root) {
51758
52593
  try {
@@ -51856,25 +52691,25 @@ function looksLikeProject(path2) {
51856
52691
  "pnpm-lock.yaml",
51857
52692
  "yarn.lock",
51858
52693
  "package-lock.json"
51859
- ].some((name) => existsSync16(join23(path2, name)));
52694
+ ].some((name) => existsSync17(join23(path2, name)));
51860
52695
  }
51861
52696
  function detectProjectLanguage(path2) {
51862
- if (existsSync16(join23(path2, "pyproject.toml")) || existsSync16(join23(path2, "requirements.txt"))) {
52697
+ if (existsSync17(join23(path2, "pyproject.toml")) || existsSync17(join23(path2, "requirements.txt"))) {
51863
52698
  return "Python";
51864
52699
  }
51865
- if (existsSync16(join23(path2, "Cargo.toml"))) {
52700
+ if (existsSync17(join23(path2, "Cargo.toml"))) {
51866
52701
  return "Rust";
51867
52702
  }
51868
- if (existsSync16(join23(path2, "go.mod"))) {
52703
+ if (existsSync17(join23(path2, "go.mod"))) {
51869
52704
  return "Go";
51870
52705
  }
51871
- if (existsSync16(join23(path2, "mix.exs"))) {
52706
+ if (existsSync17(join23(path2, "mix.exs"))) {
51872
52707
  return "Elixir";
51873
52708
  }
51874
- if (existsSync16(join23(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
52709
+ if (existsSync17(join23(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
51875
52710
  return "TypeScript";
51876
52711
  }
51877
- if (existsSync16(join23(path2, "package.json"))) {
52712
+ if (existsSync17(join23(path2, "package.json"))) {
51878
52713
  return "JavaScript";
51879
52714
  }
51880
52715
  return "Project";
@@ -51890,7 +52725,7 @@ function packageJsonMentionsTypeScript(path2) {
51890
52725
  }
51891
52726
  }
51892
52727
  function hasGitMetadata(path2) {
51893
- return existsSync16(join23(path2, ".git"));
52728
+ return existsSync17(join23(path2, ".git"));
51894
52729
  }
51895
52730
  function childDirectories(root) {
51896
52731
  try {
@@ -52011,9 +52846,10 @@ secure mission control for all your agents on your computers
52011
52846
  // src/index.ts
52012
52847
  init_onboardingDiscoveryChildProcess();
52013
52848
  init_runner();
52849
+ init_runnerLockTakeover();
52014
52850
  init_claudeCodeSession();
52015
52851
  init_authCache();
52016
- import { existsSync as existsSync17, readFileSync as readFileSync22, realpathSync as realpathSync7 } from "node:fs";
52852
+ import { existsSync as existsSync18, readFileSync as readFileSync22, realpathSync as realpathSync7 } from "node:fs";
52017
52853
  import { homedir as homedir17 } from "node:os";
52018
52854
  import { resolve as resolve11 } from "node:path";
52019
52855
  import { fileURLToPath as fileURLToPath4 } from "node:url";
@@ -52109,7 +52945,7 @@ init_kandanTls();
52109
52945
  init_protocol();
52110
52946
  init_json();
52111
52947
  init_defaultUrls();
52112
- import { existsSync as existsSync14, mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "node:fs";
52948
+ import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "node:fs";
52113
52949
  import { dirname as dirname14, join as join21 } from "node:path";
52114
52950
  import { homedir as homedir14 } from "node:os";
52115
52951
  async function runAgentCliCommand(args, deps = {
@@ -52828,7 +53664,7 @@ function authorizationHeaders(token) {
52828
53664
  return { authorization: `Bearer ${token}` };
52829
53665
  }
52830
53666
  function readOptionalTextFile(path2) {
52831
- return existsSync14(path2) ? readFileSync18(path2, "utf8") : void 0;
53667
+ return existsSync15(path2) ? readFileSync18(path2, "utf8") : void 0;
52832
53668
  }
52833
53669
  function writeTextFile(path2, content) {
52834
53670
  mkdirSync13(dirname14(path2), { recursive: true });
@@ -52919,7 +53755,7 @@ init_helloLinzumiProject();
52919
53755
  // src/commanderDaemon.ts
52920
53756
  init_runnerLogger();
52921
53757
  import {
52922
- existsSync as existsSync15,
53758
+ existsSync as existsSync16,
52923
53759
  closeSync as closeSync3,
52924
53760
  mkdirSync as mkdirSync14,
52925
53761
  openSync as openSync4,
@@ -53013,7 +53849,7 @@ function startCommanderDaemon(options) {
53013
53849
  }
53014
53850
  function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), processIdentityReader = readProcessIdentity) {
53015
53851
  const statusFile = commanderStatusFile(runnerId, statusDir);
53016
- if (!existsSync15(statusFile)) {
53852
+ if (!existsSync16(statusFile)) {
53017
53853
  return { status: "missing", runnerId, statusFile };
53018
53854
  }
53019
53855
  const record = parseRecord(readFileSync19(statusFile, "utf8"));
@@ -53021,7 +53857,7 @@ function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), proce
53021
53857
  }
53022
53858
  async function waitForCommanderDaemon(options) {
53023
53859
  const now = options.now ?? (() => Date.now());
53024
- const readTextFile = options.readTextFile ?? ((path2) => existsSync15(path2) ? readFileSync19(path2, "utf8") : void 0);
53860
+ const readTextFile = options.readTextFile ?? ((path2) => existsSync16(path2) ? readFileSync19(path2, "utf8") : void 0);
53025
53861
  const statusImpl = options.statusImpl ?? commanderDaemonStatus;
53026
53862
  const deadline = now() + options.timeoutMs;
53027
53863
  while (now() <= deadline) {
@@ -66236,6 +67072,11 @@ var flagDefinitions = /* @__PURE__ */ new Map([
66236
67072
  ["command", { kind: "value" }],
66237
67073
  ["owner-username", { kind: "value" }],
66238
67074
  ["include-token", { kind: "boolean" }],
67075
+ // Interactive lock-conflict resolution on connect: --take-over stops a live
67076
+ // holder without a prompt (automation); --no-input forces non-interactive so
67077
+ // a TTY never blocks on the takeover prompt (falls back to report + exit 1).
67078
+ ["take-over", { kind: "boolean" }],
67079
+ ["no-input", { kind: "boolean" }],
66239
67080
  ["help", { kind: "boolean" }]
66240
67081
  ]);
66241
67082
  var helloFlagDefinitions = /* @__PURE__ */ new Map([
@@ -66252,8 +67093,11 @@ if (isMainModule()) {
66252
67093
  try {
66253
67094
  await main(process.argv.slice(2));
66254
67095
  } catch (error) {
66255
- process.stderr.write(`${cliErrorMessage(error)}
67096
+ const message = cliErrorMessage(error);
67097
+ if (message !== "") {
67098
+ process.stderr.write(`${message}
66256
67099
  `);
67100
+ }
66257
67101
  process.exit(1);
66258
67102
  }
66259
67103
  }
@@ -66676,6 +67520,9 @@ async function runCommanderDaemonCommand(args) {
66676
67520
  }
66677
67521
  }
66678
67522
  function cliErrorMessage(error) {
67523
+ if (isRunnerLockConflictReportedError(error)) {
67524
+ return "";
67525
+ }
66679
67526
  return signupPromptCancelMessage2(error) ?? userFacingErrorMessage(error, { includeSupportLine: true });
66680
67527
  }
66681
67528
  function signupPromptCancelMessage2(error) {
@@ -66963,7 +67810,7 @@ async function parseAgentRunnerArgs(args, deps = {
66963
67810
  };
66964
67811
  }
66965
67812
  function readAgentTokenTextFile(path2) {
66966
- return existsSync17(path2) ? readFileSync22(path2, "utf8") : void 0;
67813
+ return existsSync18(path2) ? readFileSync22(path2, "utf8") : void 0;
66967
67814
  }
66968
67815
  function rejectAgentRunnerTargetingFlags(values) {
66969
67816
  const unsupportedFlags = [
@@ -67138,7 +67985,11 @@ async function parseRunnerArgs(args, deps = {
67138
67985
  workspaceSlug: workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
67139
67986
  runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
67140
67987
  onboardingDiscovery,
67141
- channelSession: void 0
67988
+ channelSession: void 0,
67989
+ lockTakeover: {
67990
+ takeOverWithoutPrompt: values.get("take-over") === true,
67991
+ forceNonInteractive: values.get("no-input") === true
67992
+ }
67142
67993
  };
67143
67994
  }
67144
67995
  function runnerRuntimeDefaultsFromValues(values) {
@@ -67488,6 +68339,8 @@ Connection:
67488
68339
 
67489
68340
  Workspace:
67490
68341
  --workspace <slug> Workspace slug
68342
+ --take-over If another runner already holds this workspace, stop it and run here (no prompt)
68343
+ --no-input Never prompt; if the workspace is held, print the conflict and exit
67491
68344
 
67492
68345
  Codex:
67493
68346
  --cwd <path> Working directory for Codex, default current directory