@linzumi/cli 0.0.85-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 +1013 -296
  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
  }
@@ -11964,14 +12058,336 @@ var init_claudeCodePlanMirror = __esm({
11964
12058
  }
11965
12059
  });
11966
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
+
11967
12383
  // src/claudeCodeLiveBashOutput.ts
11968
12384
  import {
11969
- mkdirSync as mkdirSync2,
12385
+ mkdirSync as mkdirSync3,
11970
12386
  rmSync,
11971
12387
  writeFileSync,
11972
12388
  promises as fsPromises
11973
12389
  } from "node:fs";
11974
- import { join as join6 } from "node:path";
12390
+ import { join as join7 } from "node:path";
11975
12391
  import { StringDecoder } from "node:string_decoder";
11976
12392
  function shellSingleQuoted(value) {
11977
12393
  return `'${value.replaceAll("'", `'\\''`)}'`;
@@ -12096,12 +12512,12 @@ function createClaudeCodeLiveBashCapture(host) {
12096
12512
  isClaudeLiveBashWrappedCommand(command)) {
12097
12513
  return void 0;
12098
12514
  }
12099
- const file = join6(
12515
+ const file = join7(
12100
12516
  host.captureDir,
12101
12517
  `${toolUseId.replaceAll(/[^\w-]/g, "_")}.out`
12102
12518
  );
12103
12519
  try {
12104
- mkdirSync2(host.captureDir, { recursive: true });
12520
+ mkdirSync3(host.captureDir, { recursive: true });
12105
12521
  writeFileSync(file, "");
12106
12522
  } catch (error) {
12107
12523
  host.log?.("claude_live_bash.capture_file_failed", {
@@ -12183,8 +12599,8 @@ var init_claudeCodeLiveBashOutput = __esm({
12183
12599
 
12184
12600
  // src/claudeCodeSession.ts
12185
12601
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
12186
- import { homedir as homedir5 } from "node:os";
12187
- import { join as join7 } from "node:path";
12602
+ import { homedir as homedir6 } from "node:os";
12603
+ import { join as join8 } from "node:path";
12188
12604
  function claudeCodeSettingSources() {
12189
12605
  return ["user", "project", "local"];
12190
12606
  }
@@ -12328,7 +12744,7 @@ function claudeCodeRateLimitSummaryText(rateLimit, nowMs) {
12328
12744
  async function probeClaudeCodeAvailability(args) {
12329
12745
  if (!hasClaudeCodeAuthHint(process.env, {
12330
12746
  cwd: args.cwd,
12331
- homeDir: homedir5(),
12747
+ homeDir: homedir6(),
12332
12748
  platform: process.platform,
12333
12749
  fileExists: existsSync4,
12334
12750
  readTextFile: readTextFileIfPresent
@@ -12361,7 +12777,7 @@ async function probeClaudeCodeAvailability(args) {
12361
12777
  }
12362
12778
  }
12363
12779
  function hasClaudeCodeAuthHint(env, deps) {
12364
- const configDir = env.CLAUDE_CONFIG_DIR ?? join7(deps.homeDir, ".claude");
12780
+ const configDir = env.CLAUDE_CONFIG_DIR ?? join8(deps.homeDir, ".claude");
12365
12781
  return hasAnthropicCredentialEnv(env) || hasClaudeCloudProviderEnv(env) || hasClaudeCodeFileCredential(configDir, deps) || hasClaudeCodeApiKeyHelper(configDir, deps) || hasMacClaudeCodeKeychainAnchor(deps);
12366
12782
  }
12367
12783
  function claudeCodePolicyDeferHooks() {
@@ -12396,14 +12812,14 @@ function hasClaudeCloudProviderEnv(env) {
12396
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);
12397
12813
  }
12398
12814
  function hasClaudeCodeFileCredential(configDir, deps) {
12399
- 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"));
12400
12816
  }
12401
12817
  function hasClaudeCodeApiKeyHelper(configDir, deps) {
12402
12818
  return [
12403
- join7(configDir, "settings.json"),
12404
- join7(configDir, "settings.local.json"),
12405
- join7(deps.cwd, ".claude", "settings.json"),
12406
- 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")
12407
12823
  ].some((path2) => settingsFileHasApiKeyHelper(path2, deps));
12408
12824
  }
12409
12825
  function settingsFileHasApiKeyHelper(path2, deps) {
@@ -12423,9 +12839,9 @@ function hasMacClaudeCodeKeychainAnchor(deps) {
12423
12839
  return false;
12424
12840
  }
12425
12841
  return [
12426
- join7(deps.homeDir, "Library", "Application Support", "claude-cli-nodejs"),
12427
- join7(deps.homeDir, "Library", "Application Support", "Claude"),
12428
- 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")
12429
12845
  ].some((path2) => deps.fileExists(path2));
12430
12846
  }
12431
12847
  function readTextFileIfPresent(path2) {
@@ -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.85-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);
@@ -19512,20 +20026,21 @@ function handleDashboardKey(state, key, exitProcess = () => process.kill(process
19512
20026
  function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill(process.pid, "SIGINT")) {
19513
20027
  let rendering = false;
19514
20028
  let rawScrollAnchor;
19515
- const screen = blessed.screen({
20029
+ installBlessedSetulcShim();
20030
+ const screen = blessed2.screen({
19516
20031
  title: "Linzumi Commander",
19517
20032
  smartCSR: true,
19518
20033
  fullUnicode: true,
19519
20034
  mouse: true
19520
20035
  });
19521
- const header = blessed.box({
20036
+ const header = blessed2.box({
19522
20037
  top: 0,
19523
20038
  left: 0,
19524
20039
  width: "100%",
19525
20040
  height: 1,
19526
20041
  tags: false
19527
20042
  });
19528
- const tableElement = blessed.listtable({
20043
+ const tableElement = blessed2.listtable({
19529
20044
  top: 1,
19530
20045
  left: 0,
19531
20046
  width: "100%",
@@ -19554,7 +20069,7 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
19554
20069
  }
19555
20070
  }
19556
20071
  });
19557
- const rawTitle = blessed.box({
20072
+ const rawTitle = blessed2.box({
19558
20073
  top: 0,
19559
20074
  left: 12,
19560
20075
  right: 0,
@@ -19562,7 +20077,7 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
19562
20077
  tags: false,
19563
20078
  hidden: true
19564
20079
  });
19565
- const backButton = blessed.button({
20080
+ const backButton = blessed2.button({
19566
20081
  top: 0,
19567
20082
  left: 0,
19568
20083
  width: 10,
@@ -19577,7 +20092,7 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
19577
20092
  hover: { inverse: true }
19578
20093
  }
19579
20094
  });
19580
- const rawBox = blessed.box({
20095
+ const rawBox = blessed2.box({
19581
20096
  top: 3,
19582
20097
  left: 0,
19583
20098
  width: "100%",
@@ -20056,6 +20571,7 @@ var dashboardState, maxRawLines, escapeKey, ctrlCKey, enterKey, upKey, downKey,
20056
20571
  var init_runnerConsoleReporter = __esm({
20057
20572
  "src/runnerConsoleReporter.ts"() {
20058
20573
  "use strict";
20574
+ init_blessedTputSetulcShim();
20059
20575
  dashboardState = {
20060
20576
  jobs: /* @__PURE__ */ new Map(),
20061
20577
  discovery: /* @__PURE__ */ new Map(),
@@ -21381,6 +21897,7 @@ import { spawn as spawn9, spawnSync as spawnSync5 } from "node:child_process";
21381
21897
  import { createHash as createHash5, randomUUID as randomUUID4 } from "node:crypto";
21382
21898
  import {
21383
21899
  chmodSync as chmodSync2,
21900
+ existsSync as existsSync13,
21384
21901
  lstatSync,
21385
21902
  mkdirSync as mkdirSync12,
21386
21903
  mkdtempSync as mkdtempSync4,
@@ -21395,6 +21912,7 @@ import {
21395
21912
  import { readFile as readFile2 } from "node:fs/promises";
21396
21913
  import { createServer as createServer3 } from "node:http";
21397
21914
  import { homedir as homedir13, hostname as hostname2, tmpdir as tmpdir3 } from "node:os";
21915
+ import { createInterface } from "node:readline";
21398
21916
  import {
21399
21917
  basename as basename8,
21400
21918
  dirname as dirname13,
@@ -21419,42 +21937,35 @@ async function runLocalCodexRunner(options) {
21419
21937
  });
21420
21938
  try {
21421
21939
  if (options.machineId !== void 0) {
21422
- let runnerLock;
21423
- try {
21424
- runnerLock = acquireRunnerLock({
21425
- machineId: options.machineId,
21426
- runnerId: options.runnerId,
21427
- cwd: options.cwd,
21428
- workspace: runnerWorkspaceSlug(options) ?? null,
21429
- linzumiUrl: options.kandanUrl,
21430
- launchSource: options.launchSource,
21431
- configPath: options.runnerLockConfigPath,
21432
- // Wedged-runner takeover: a lock holder whose pid is alive but
21433
- // whose lock heartbeat went stale (>3 min) gets SIGKILLed and its
21434
- // lock replaced, instead of blocking recovery until someone runs
21435
- // kill -9 by hand. Log it so the runner log explains where the old
21436
- // pid went.
21437
- onTakeover: (takeover) => {
21438
- log2("runner.lock_takeover", {
21439
- runnerId: options.runnerId,
21440
- holderRunnerId: takeover.holderRunnerId,
21441
- holderPid: takeover.holderPid,
21442
- reason: takeover.reason,
21443
- lockPath: takeover.lockPath
21444
- });
21445
- }
21446
- });
21447
- } catch (error) {
21448
- if (isRunnerLockHeldError(error)) {
21449
- 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", {
21450
21956
  runnerId: options.runnerId,
21451
- heldByRunnerId: error.heldBy.runnerId,
21452
- heldByPid: error.heldBy.pid,
21453
- lockPath: error.lockPath
21957
+ holderRunnerId: takeover.holderRunnerId,
21958
+ holderPid: takeover.holderPid,
21959
+ reason: takeover.reason,
21960
+ lockPath: takeover.lockPath
21454
21961
  });
21455
21962
  }
21456
- throw error;
21457
- }
21963
+ });
21964
+ const runnerLock = await acquireRunnerLockWithConflictResolution(
21965
+ acquire,
21966
+ options,
21967
+ log2
21968
+ );
21458
21969
  cleanup.actions.push(() => runnerLock.release());
21459
21970
  log2("runner.lock_acquired", {
21460
21971
  path: runnerLock.path,
@@ -22474,6 +22985,11 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22474
22985
  if (options.channelSession !== void 0 && codex === void 0) {
22475
22986
  throw new Error("channel session requires a Codex app-server connection");
22476
22987
  }
22988
+ const ownsRespawnableAppServer = runnerOwnsRespawnableAppServer({
22989
+ codexUrl: options.codexUrl,
22990
+ threadProcessRole: options.threadProcess?.role,
22991
+ ownsStartedAppServer: started !== void 0
22992
+ });
22477
22993
  const seq = { value: 0 };
22478
22994
  const discoveredCodexThreads = { value: [] };
22479
22995
  const runtimeDefaults = runnerRuntimeDefaults(options);
@@ -22565,6 +23081,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22565
23081
  const dynamicChannelSessions = /* @__PURE__ */ new Map();
22566
23082
  const dynamicChannelSessionCodexClients = /* @__PURE__ */ new Map();
22567
23083
  const codexTurnFailureGuards = /* @__PURE__ */ new Map();
23084
+ const lossReportPushes = [];
22568
23085
  const codexTurnFailureGuardFor = (client) => {
22569
23086
  const existing = codexTurnFailureGuards.get(client);
22570
23087
  if (existing !== void 0) {
@@ -22577,7 +23094,36 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22577
23094
  ([codexThreadId]) => dynamicChannelSessionCodexClients.get(codexThreadId) === client
22578
23095
  ).map(([, session]) => session)
22579
23096
  ],
22580
- 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
22581
23127
  });
22582
23128
  codexTurnFailureGuards.set(client, guard);
22583
23129
  return guard;
@@ -22681,7 +23227,8 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22681
23227
  portForwardWatcher: channelSessionPortForwardWatcherOptions({
22682
23228
  rootPid: portForwardWatcherRootPid,
22683
23229
  start: options.portForwardWatcher,
22684
- commanderBoundPids: args.commanderBoundPids
23230
+ commanderBoundPids: args.commanderBoundPids,
23231
+ commanderBoundPorts: args.commanderBoundPorts
22685
23232
  }),
22686
23233
  suppressedForwardPorts,
22687
23234
  onForwardPortApproved: (port, attribution) => {
@@ -22876,6 +23423,12 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22876
23423
  portForwardWatcherRootPid: handle.processPid,
22877
23424
  commanderBoundPids: (handle.commanderManagedPorts ?? []).flatMap(
22878
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
22879
23432
  )
22880
23433
  });
22881
23434
  switch (control.type) {
@@ -23025,6 +23578,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
23025
23578
  }
23026
23579
  });
23027
23580
  if (sharedCodexTurnFailureGuard !== void 0) {
23581
+ const appServerLost = { handled: false };
23028
23582
  const failCodexSessionTurns = (cause) => {
23029
23583
  if (cleanup.closePromise !== void 0) {
23030
23584
  return;
@@ -23032,6 +23586,27 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
23032
23586
  log2("codex.app_server_connection_lost", { cause });
23033
23587
  sharedCodexTurnFailureGuard.failActiveTurns(cause);
23034
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
+ }
23035
23610
  };
23036
23611
  codex?.onClose?.((error) => failCodexSessionTurns(error.message));
23037
23612
  started?.process.once(
@@ -23787,6 +24362,19 @@ async function resolveSessionControl(channelSession, dynamicChannelSessions, con
23787
24362
  }
23788
24363
  return void 0;
23789
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
+ }
23790
24378
  function createCodexTurnFailureGuard(args) {
23791
24379
  const activeTurnsByThread = /* @__PURE__ */ new Map();
23792
24380
  const state = { failed: false };
@@ -23822,6 +24410,7 @@ function createCodexTurnFailureGuard(args) {
23822
24410
  if (activeTurns.length === 0) {
23823
24411
  return;
23824
24412
  }
24413
+ args.onActiveThreadsFailed?.(activeTurns.map(([threadId]) => threadId));
23825
24414
  const sessions = Array.from(args.sessions());
23826
24415
  for (const [threadId, turnId] of activeTurns) {
23827
24416
  args.log("codex.active_turn_failed_on_connection_loss", {
@@ -23979,6 +24568,101 @@ function installCleanupHandlers(close) {
23979
24568
  process.off("exit", closeOnExit);
23980
24569
  };
23981
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
+ }
23982
24666
  function launchCodexTui(codexBin, codexUrl, cwd, codexThreadId, session, fast) {
23983
24667
  const args = codexTuiArgs(codexUrl, codexThreadId, session, fast);
23984
24668
  writeCliAuditEvent("process.spawn", {
@@ -25856,6 +26540,16 @@ async function startClaudeCodeProviderInstance(args) {
25856
26540
  codexVersion: void 0
25857
26541
  };
25858
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
+ });
25859
26553
  const acceptedSourceSeqs = new Set(
25860
26554
  sourceSeq === void 0 ? [] : [sourceSeq]
25861
26555
  );
@@ -26260,6 +26954,7 @@ async function startClaudeCodeProviderInstance(args) {
26260
26954
  return result2;
26261
26955
  };
26262
26956
  const sessionWork = runSession();
26957
+ void sessionWork.finally(() => reaperRegistration.unregister()).catch(() => void 0);
26263
26958
  sessionWorkHandle.value = sessionWork;
26264
26959
  const settled = await Promise.race([
26265
26960
  sessionWork.then((result2) => ({
@@ -26698,9 +27393,11 @@ function channelSessionPortForwardWatcherOptions(args) {
26698
27393
  ...args.rootPid === void 0 ? [] : [args.rootPid],
26699
27394
  ...args.commanderBoundPids ?? []
26700
27395
  ];
27396
+ const commanderBoundPorts = args.commanderBoundPorts ?? [];
26701
27397
  return {
26702
27398
  ...args.rootPid === void 0 ? {} : { rootPid: args.rootPid },
26703
27399
  ...commanderBoundPids.length === 0 ? {} : { commanderBoundPids },
27400
+ ...commanderBoundPorts.length === 0 ? {} : { commanderBoundPorts },
26704
27401
  ...args.start === void 0 ? {} : { start: args.start }
26705
27402
  };
26706
27403
  }
@@ -28502,6 +29199,7 @@ var init_runner = __esm({
28502
29199
  init_commanderAttachments();
28503
29200
  init_claudeCodePipeline();
28504
29201
  init_claudeCodePlanMirror();
29202
+ init_engineChildReaper();
28505
29203
  init_claudeCodeLiveBashOutput();
28506
29204
  init_claudeCodeSession();
28507
29205
  init_codexAppServer();
@@ -28524,6 +29222,7 @@ var init_runner = __esm({
28524
29222
  init_protocol();
28525
29223
  init_runnerLogger();
28526
29224
  init_runnerLock();
29225
+ init_runnerLockTakeover();
28527
29226
  init_runnerConsoleReporter();
28528
29227
  init_version();
28529
29228
  init_telemetry();
@@ -28564,7 +29263,7 @@ var init_runner = __esm({
28564
29263
  });
28565
29264
 
28566
29265
  // src/kandanTls.ts
28567
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "node:fs";
29266
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "node:fs";
28568
29267
  import { Agent } from "undici";
28569
29268
  import WsWebSocket from "ws";
28570
29269
  function kandanTlsTrustFromEnv() {
@@ -28575,7 +29274,7 @@ function kandanTlsTrustFromCaFile(caFile) {
28575
29274
  return void 0;
28576
29275
  }
28577
29276
  const trimmed = caFile.trim();
28578
- if (!existsSync13(trimmed)) {
29277
+ if (!existsSync14(trimmed)) {
28579
29278
  throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
28580
29279
  }
28581
29280
  const ca = readFileSync17(trimmed, "utf8");
@@ -48853,7 +49552,7 @@ __export(signupFlow_exports, {
48853
49552
  });
48854
49553
  import { spawn as spawn12, spawnSync as spawnSync7 } from "node:child_process";
48855
49554
  import {
48856
- existsSync as existsSync16,
49555
+ existsSync as existsSync17,
48857
49556
  constants as fsConstants,
48858
49557
  mkdirSync as mkdirSync15,
48859
49558
  mkdtempSync as mkdtempSync5,
@@ -48981,7 +49680,7 @@ function defaultSignupDraftStore(serviceUrl) {
48981
49680
  const path2 = defaultSignupDraftPath(serviceUrl);
48982
49681
  return {
48983
49682
  read: () => {
48984
- if (!existsSync16(path2)) {
49683
+ if (!existsSync17(path2)) {
48985
49684
  return void 0;
48986
49685
  }
48987
49686
  let parsed;
@@ -49060,7 +49759,7 @@ function defaultSignupTaskCachePath(serviceUrl) {
49060
49759
  );
49061
49760
  }
49062
49761
  function readSignupTaskCache(path2) {
49063
- if (!existsSync16(path2)) {
49762
+ if (!existsSync17(path2)) {
49064
49763
  return { version: 1, entries: {} };
49065
49764
  }
49066
49765
  let parsed;
@@ -51888,7 +52587,7 @@ async function discoverCodeRoots(homeDir) {
51888
52587
  const candidates = ["src", "code", "projects"].map(
51889
52588
  (name) => join23(homeDir, name)
51890
52589
  );
51891
- return candidates.filter((path2) => existsSync16(path2)).flatMap((path2) => discoveredProjectNames(path2));
52590
+ return candidates.filter((path2) => existsSync17(path2)).flatMap((path2) => discoveredProjectNames(path2));
51892
52591
  }
51893
52592
  function discoveredProjectNames(root) {
51894
52593
  try {
@@ -51992,25 +52691,25 @@ function looksLikeProject(path2) {
51992
52691
  "pnpm-lock.yaml",
51993
52692
  "yarn.lock",
51994
52693
  "package-lock.json"
51995
- ].some((name) => existsSync16(join23(path2, name)));
52694
+ ].some((name) => existsSync17(join23(path2, name)));
51996
52695
  }
51997
52696
  function detectProjectLanguage(path2) {
51998
- if (existsSync16(join23(path2, "pyproject.toml")) || existsSync16(join23(path2, "requirements.txt"))) {
52697
+ if (existsSync17(join23(path2, "pyproject.toml")) || existsSync17(join23(path2, "requirements.txt"))) {
51999
52698
  return "Python";
52000
52699
  }
52001
- if (existsSync16(join23(path2, "Cargo.toml"))) {
52700
+ if (existsSync17(join23(path2, "Cargo.toml"))) {
52002
52701
  return "Rust";
52003
52702
  }
52004
- if (existsSync16(join23(path2, "go.mod"))) {
52703
+ if (existsSync17(join23(path2, "go.mod"))) {
52005
52704
  return "Go";
52006
52705
  }
52007
- if (existsSync16(join23(path2, "mix.exs"))) {
52706
+ if (existsSync17(join23(path2, "mix.exs"))) {
52008
52707
  return "Elixir";
52009
52708
  }
52010
- if (existsSync16(join23(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
52709
+ if (existsSync17(join23(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
52011
52710
  return "TypeScript";
52012
52711
  }
52013
- if (existsSync16(join23(path2, "package.json"))) {
52712
+ if (existsSync17(join23(path2, "package.json"))) {
52014
52713
  return "JavaScript";
52015
52714
  }
52016
52715
  return "Project";
@@ -52026,7 +52725,7 @@ function packageJsonMentionsTypeScript(path2) {
52026
52725
  }
52027
52726
  }
52028
52727
  function hasGitMetadata(path2) {
52029
- return existsSync16(join23(path2, ".git"));
52728
+ return existsSync17(join23(path2, ".git"));
52030
52729
  }
52031
52730
  function childDirectories(root) {
52032
52731
  try {
@@ -52147,9 +52846,10 @@ secure mission control for all your agents on your computers
52147
52846
  // src/index.ts
52148
52847
  init_onboardingDiscoveryChildProcess();
52149
52848
  init_runner();
52849
+ init_runnerLockTakeover();
52150
52850
  init_claudeCodeSession();
52151
52851
  init_authCache();
52152
- 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";
52153
52853
  import { homedir as homedir17 } from "node:os";
52154
52854
  import { resolve as resolve11 } from "node:path";
52155
52855
  import { fileURLToPath as fileURLToPath4 } from "node:url";
@@ -52245,7 +52945,7 @@ init_kandanTls();
52245
52945
  init_protocol();
52246
52946
  init_json();
52247
52947
  init_defaultUrls();
52248
- 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";
52249
52949
  import { dirname as dirname14, join as join21 } from "node:path";
52250
52950
  import { homedir as homedir14 } from "node:os";
52251
52951
  async function runAgentCliCommand(args, deps = {
@@ -52964,7 +53664,7 @@ function authorizationHeaders(token) {
52964
53664
  return { authorization: `Bearer ${token}` };
52965
53665
  }
52966
53666
  function readOptionalTextFile(path2) {
52967
- return existsSync14(path2) ? readFileSync18(path2, "utf8") : void 0;
53667
+ return existsSync15(path2) ? readFileSync18(path2, "utf8") : void 0;
52968
53668
  }
52969
53669
  function writeTextFile(path2, content) {
52970
53670
  mkdirSync13(dirname14(path2), { recursive: true });
@@ -53055,7 +53755,7 @@ init_helloLinzumiProject();
53055
53755
  // src/commanderDaemon.ts
53056
53756
  init_runnerLogger();
53057
53757
  import {
53058
- existsSync as existsSync15,
53758
+ existsSync as existsSync16,
53059
53759
  closeSync as closeSync3,
53060
53760
  mkdirSync as mkdirSync14,
53061
53761
  openSync as openSync4,
@@ -53149,7 +53849,7 @@ function startCommanderDaemon(options) {
53149
53849
  }
53150
53850
  function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), processIdentityReader = readProcessIdentity) {
53151
53851
  const statusFile = commanderStatusFile(runnerId, statusDir);
53152
- if (!existsSync15(statusFile)) {
53852
+ if (!existsSync16(statusFile)) {
53153
53853
  return { status: "missing", runnerId, statusFile };
53154
53854
  }
53155
53855
  const record = parseRecord(readFileSync19(statusFile, "utf8"));
@@ -53157,7 +53857,7 @@ function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), proce
53157
53857
  }
53158
53858
  async function waitForCommanderDaemon(options) {
53159
53859
  const now = options.now ?? (() => Date.now());
53160
- 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);
53161
53861
  const statusImpl = options.statusImpl ?? commanderDaemonStatus;
53162
53862
  const deadline = now() + options.timeoutMs;
53163
53863
  while (now() <= deadline) {
@@ -66372,6 +67072,11 @@ var flagDefinitions = /* @__PURE__ */ new Map([
66372
67072
  ["command", { kind: "value" }],
66373
67073
  ["owner-username", { kind: "value" }],
66374
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" }],
66375
67080
  ["help", { kind: "boolean" }]
66376
67081
  ]);
66377
67082
  var helloFlagDefinitions = /* @__PURE__ */ new Map([
@@ -66388,8 +67093,11 @@ if (isMainModule()) {
66388
67093
  try {
66389
67094
  await main(process.argv.slice(2));
66390
67095
  } catch (error) {
66391
- process.stderr.write(`${cliErrorMessage(error)}
67096
+ const message = cliErrorMessage(error);
67097
+ if (message !== "") {
67098
+ process.stderr.write(`${message}
66392
67099
  `);
67100
+ }
66393
67101
  process.exit(1);
66394
67102
  }
66395
67103
  }
@@ -66812,6 +67520,9 @@ async function runCommanderDaemonCommand(args) {
66812
67520
  }
66813
67521
  }
66814
67522
  function cliErrorMessage(error) {
67523
+ if (isRunnerLockConflictReportedError(error)) {
67524
+ return "";
67525
+ }
66815
67526
  return signupPromptCancelMessage2(error) ?? userFacingErrorMessage(error, { includeSupportLine: true });
66816
67527
  }
66817
67528
  function signupPromptCancelMessage2(error) {
@@ -67099,7 +67810,7 @@ async function parseAgentRunnerArgs(args, deps = {
67099
67810
  };
67100
67811
  }
67101
67812
  function readAgentTokenTextFile(path2) {
67102
- return existsSync17(path2) ? readFileSync22(path2, "utf8") : void 0;
67813
+ return existsSync18(path2) ? readFileSync22(path2, "utf8") : void 0;
67103
67814
  }
67104
67815
  function rejectAgentRunnerTargetingFlags(values) {
67105
67816
  const unsupportedFlags = [
@@ -67274,7 +67985,11 @@ async function parseRunnerArgs(args, deps = {
67274
67985
  workspaceSlug: workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
67275
67986
  runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
67276
67987
  onboardingDiscovery,
67277
- channelSession: void 0
67988
+ channelSession: void 0,
67989
+ lockTakeover: {
67990
+ takeOverWithoutPrompt: values.get("take-over") === true,
67991
+ forceNonInteractive: values.get("no-input") === true
67992
+ }
67278
67993
  };
67279
67994
  }
67280
67995
  function runnerRuntimeDefaultsFromValues(values) {
@@ -67624,6 +68339,8 @@ Connection:
67624
68339
 
67625
68340
  Workspace:
67626
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
67627
68344
 
67628
68345
  Codex:
67629
68346
  --cwd <path> Working directory for Codex, default current directory