@linzumi/cli 0.0.85-beta → 0.0.87-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 +1190 -332
  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) {
@@ -12657,7 +13073,16 @@ async function* defaultClaudeCodeRunner(options) {
12657
13073
  options.wrapApprovedToolInput
12658
13074
  )
12659
13075
  },
12660
- ...options.model === void 0 ? {} : { model: options.model }
13076
+ ...options.model === void 0 ? {} : { model: options.model },
13077
+ ...options.permissionMode === void 0 ? {} : {
13078
+ permissionMode: options.permissionMode,
13079
+ // The SDK requires this safety acknowledgement when a session is
13080
+ // constructed in 'bypassPermissions' (sdk.d.ts Options:
13081
+ // "Must be set to true when using permissionMode:
13082
+ // 'bypassPermissions'"). It is the intentional-bypass flag, only
13083
+ // meaningful for that mode; harmless for the others.
13084
+ ...options.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}
13085
+ }
12661
13086
  // exactOptionalPropertyTypes: the conditional spreads build a union the
12662
13087
  // SDK's Options cannot absorb verbatim; the shape is correct field-wise.
12663
13088
  }
@@ -13076,207 +13501,6 @@ var init_claudeCodeSession = __esm({
13076
13501
  }
13077
13502
  });
13078
13503
 
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
13504
  // src/mcpConfig.ts
13281
13505
  function linzumiMcpServerConfig(options) {
13282
13506
  return {
@@ -13403,6 +13627,76 @@ var init_mcpConfig = __esm({
13403
13627
  }
13404
13628
  });
13405
13629
 
13630
+ // src/engineParentDeathWatchdog.ts
13631
+ function encodeParentDeathWatchdogConfig(config) {
13632
+ return JSON.stringify(config);
13633
+ }
13634
+ function parentDeathWatchdogProgram() {
13635
+ return `
13636
+ const { spawn } = require('node:child_process');
13637
+ const config = JSON.parse(process.argv[process.argv.length - 1]);
13638
+ const originalParentPid = process.ppid;
13639
+ const child = spawn(config.command, config.args, { stdio: 'inherit' });
13640
+ let done = false;
13641
+ function shutdown(signal) {
13642
+ if (done) return;
13643
+ done = true;
13644
+ try { process.kill(-process.pid, signal); }
13645
+ catch (_e) { try { child.kill(signal); } catch (_e2) {} }
13646
+ }
13647
+ process.on('SIGINT', () => shutdown('SIGINT'));
13648
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
13649
+ process.on('SIGHUP', () => shutdown('SIGTERM'));
13650
+ child.on('exit', (code, signal) => {
13651
+ clearInterval(watchdog);
13652
+ // The child is gone; the wrapper must follow it down rather than linger as an
13653
+ // orphan. On the forwarded-signal path (shutdown() above caught SIGINT/SIGTERM
13654
+ // /SIGHUP and propagated it to the child), the wrapper still has its own
13655
+ // handler installed for that signal, so a bare process.kill(self, signal)
13656
+ // would just re-enter shutdown() - a no-op once done is set - and the wrapper
13657
+ // would never exit. Remove the handlers first so the signal's DEFAULT
13658
+ // disposition (terminate) applies, re-raise to mirror the child's death
13659
+ // signal, then exit() as a belt-and-suspenders fallback in case re-raising
13660
+ // does not terminate (e.g. a signal with no default-terminate disposition).
13661
+ process.removeAllListeners('SIGINT');
13662
+ process.removeAllListeners('SIGTERM');
13663
+ process.removeAllListeners('SIGHUP');
13664
+ if (signal) { try { process.kill(process.pid, signal); } catch (_e) {} }
13665
+ process.exit(signal ? 1 : code == null ? 0 : code);
13666
+ });
13667
+ child.on('error', (err) => {
13668
+ process.stderr.write('engine watchdog spawn failed: ' + (err && err.message) + '\\n');
13669
+ process.exit(1);
13670
+ });
13671
+ const watchdog = setInterval(() => {
13672
+ // Inlined parentDeathWatchdogShouldReap (keep in sync): reap PURELY on a ppid
13673
+ // change from boot - covers reparent-to-subreaper AND reparent-to-init. NO
13674
+ // \`=== 1\` arm: a runner that is PID 1 captures originalParentPid === 1, so
13675
+ // such an arm would false-fire on the first poll and kill a healthy codex.
13676
+ if (process.ppid !== originalParentPid) {
13677
+ try { child.kill('SIGKILL'); } catch (_e) {}
13678
+ try { process.kill(-process.pid, 'SIGKILL'); } catch (_e) {}
13679
+ process.exit(0);
13680
+ }
13681
+ }, config.pollIntervalMs);
13682
+ `.trim();
13683
+ }
13684
+ function parentDeathWatchdogSpawn(nodeExecPath, config) {
13685
+ return {
13686
+ command: nodeExecPath,
13687
+ args: [
13688
+ "-e",
13689
+ parentDeathWatchdogProgram(),
13690
+ encodeParentDeathWatchdogConfig(config)
13691
+ ]
13692
+ };
13693
+ }
13694
+ var init_engineParentDeathWatchdog = __esm({
13695
+ "src/engineParentDeathWatchdog.ts"() {
13696
+ "use strict";
13697
+ }
13698
+ });
13699
+
13406
13700
  // src/codexAppServer.ts
13407
13701
  import {
13408
13702
  spawn as spawn2
@@ -13444,13 +13738,18 @@ async function startCodexAppServerAttempt(codexBin, cwd, options, attempt) {
13444
13738
  let stderrText = "";
13445
13739
  const configuredStdio = codexAppServerStdio(process.stdout.isTTY === true);
13446
13740
  const stdio = [configuredStdio[0], configuredStdio[1], "pipe"];
13741
+ const watchdogSpawn = parentDeathWatchdogSpawn(process.execPath, {
13742
+ command: codexBin,
13743
+ args,
13744
+ pollIntervalMs: codexAppServerWatchdogPollMs
13745
+ });
13447
13746
  writeCliAuditEvent("process.spawn", {
13448
13747
  command: codexBin,
13449
13748
  args,
13450
13749
  cwd,
13451
13750
  purpose: "codex.app_server"
13452
13751
  });
13453
- const child = spawn2(codexBin, args, {
13752
+ const child = spawn2(watchdogSpawn.command, [...watchdogSpawn.args], {
13454
13753
  cwd,
13455
13754
  env: codexAppServerEnv(options.env, options.inheritEnv ?? true),
13456
13755
  stdio,
@@ -13467,7 +13766,17 @@ async function startCodexAppServerAttempt(codexBin, cwd, options, attempt) {
13467
13766
  }
13468
13767
  });
13469
13768
  }
13470
- const stop = () => stopCodexAppServerProcess(child);
13769
+ const registered = registerEngineChild({
13770
+ stop: () => stopCodexAppServerProcess(child),
13771
+ pid: child.pid,
13772
+ ownProcessGroup: true,
13773
+ kind: "codex.app_server"
13774
+ });
13775
+ child.once("exit", () => registered.unregister());
13776
+ const stop = () => {
13777
+ registered.unregister();
13778
+ stopCodexAppServerProcess(child);
13779
+ };
13471
13780
  writeCliAuditEvent("process.spawned", {
13472
13781
  command: codexBin,
13473
13782
  args,
@@ -13571,7 +13880,7 @@ function stopCodexAppServerProcess(child, killProcess = process.kill) {
13571
13880
  child.kill("SIGINT");
13572
13881
  }
13573
13882
  function logProcessGroupSignalFailure(pid, error) {
13574
- const code = processSignalErrorCode(error);
13883
+ const code = processSignalErrorCode2(error);
13575
13884
  const message = error instanceof Error ? error.message : String(error);
13576
13885
  const event = code === "EPERM" ? "process.group_signal_denied" : code === "ESRCH" ? "process.group_signal_missing" : "process.group_signal_failed";
13577
13886
  writeCliAuditEvent(event, {
@@ -13589,7 +13898,7 @@ function logProcessGroupSignalFailure(pid, error) {
13589
13898
  );
13590
13899
  }
13591
13900
  }
13592
- function processSignalErrorCode(error) {
13901
+ function processSignalErrorCode2(error) {
13593
13902
  if (error !== null && typeof error === "object" && "code" in error) {
13594
13903
  const code = error.code;
13595
13904
  return typeof code === "string" ? code : void 0;
@@ -13973,13 +14282,16 @@ function readyzUrlForWebsocket(websocketUrl) {
13973
14282
  parsed.hash = "";
13974
14283
  return parsed.toString();
13975
14284
  }
13976
- var blockedCodexAppServerEnvKeys;
14285
+ var codexAppServerWatchdogPollMs, blockedCodexAppServerEnvKeys;
13977
14286
  var init_codexAppServer = __esm({
13978
14287
  "src/codexAppServer.ts"() {
13979
14288
  "use strict";
13980
14289
  init_protocol();
13981
14290
  init_runnerLogger();
13982
14291
  init_mcpConfig();
14292
+ init_engineChildReaper();
14293
+ init_engineParentDeathWatchdog();
14294
+ codexAppServerWatchdogPollMs = 2e3;
13983
14295
  blockedCodexAppServerEnvKeys = [
13984
14296
  "LINZUMI_MCP_ACCESS_TOKEN",
13985
14297
  "LINZUMI_MCP_OWNER_USERNAME"
@@ -14689,8 +15001,8 @@ function startCallbackServer(args) {
14689
15001
  }
14690
15002
  resolveCallback?.({ code, state });
14691
15003
  writeOauthResult(response, {
14692
- title: "Connected",
14693
- body: "You can close this tab. Your terminal will finish the setup.",
15004
+ title: "Computer authorized",
15005
+ body: "This computer has been authorized for agentic use in Linzumi. Feel free to close this tab and let the terminal finish setup.",
14694
15006
  status: 200
14695
15007
  });
14696
15008
  } catch (error) {
@@ -14733,29 +15045,108 @@ function writeOauthResult(response, args) {
14733
15045
  response.end(oauthResultHtml(args));
14734
15046
  }
14735
15047
  function oauthResultHtml(args) {
15048
+ const success = args.status >= 200 && args.status < 300;
15049
+ const badgeClass = success ? "badge badge--success" : "badge badge--notice";
15050
+ const badgeMark = success ? '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" class="mark"><path fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d="M5 12.5l4.2 4.2L19 7"/></svg>' : '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" class="mark"><path fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d="M12 7v6M12 17h.01"/></svg>';
14736
15051
  return `<!doctype html>
14737
- <html>
15052
+ <html lang="en">
14738
15053
  <head>
14739
15054
  <meta charset="utf-8" />
14740
15055
  <meta name="viewport" content="width=device-width, initial-scale=1" />
14741
15056
  <title>${escapeHtml(args.title)}</title>
14742
15057
  <style>
14743
- :root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
14744
- body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f6f7f9; color: #13161c; }
14745
- main { width: min(520px, calc(100vw - 32px)); border: 1px solid #d9dde5; border-radius: 12px; background: #fff; padding: 30px; box-shadow: 0 24px 70px rgba(18, 25, 38, 0.12); }
14746
- h1 { margin: 0 0 10px; font-size: 24px; line-height: 1.2; }
14747
- p { margin: 0; color: #465160; line-height: 1.55; }
15058
+ :root {
15059
+ color-scheme: light dark;
15060
+ /* Linzumi brand tokens (light) \u2014 kandan/server_v2/web/src/ui/theme/tokens.css */
15061
+ --page-bg: #f5f2ee;
15062
+ --surface: #fcfaf7;
15063
+ --foreground: #1e1b18;
15064
+ --muted: #5c5550;
15065
+ --divider: rgba(138, 148, 144, 0.16);
15066
+ --accent: #c4897a;
15067
+ --success: #5a9a6a;
15068
+ --success-soft: rgba(90, 154, 106, 0.1);
15069
+ --notice-soft: rgba(196, 137, 122, 0.1);
15070
+ --shadow: 0 12px 40px rgba(106, 138, 122, 0.12), 0 4px 12px rgba(106, 138, 122, 0.06);
15071
+ }
14748
15072
  @media (prefers-color-scheme: dark) {
14749
- body { background: #0f131a; color: #f3f5f8; }
14750
- main { background: #171c25; border-color: #2b3442; box-shadow: 0 24px 70px rgba(0, 0, 0, 0.4); }
14751
- p { color: #aab4c2; }
15073
+ :root {
15074
+ /* Linzumi brand tokens (dark) */
15075
+ --page-bg: #171b19;
15076
+ --surface: #202522;
15077
+ --foreground: rgba(245, 241, 232, 0.95);
15078
+ --muted: rgba(245, 241, 232, 0.68);
15079
+ --divider: rgba(245, 241, 232, 0.1);
15080
+ --accent: #c8a0b0;
15081
+ --success: #72c488;
15082
+ --success-soft: rgba(114, 196, 136, 0.14);
15083
+ --notice-soft: rgba(200, 160, 176, 0.14);
15084
+ --shadow: 0 18px 50px rgba(0, 0, 0, 0.5), 0 4px 12px rgba(0, 0, 0, 0.35);
15085
+ }
15086
+ }
15087
+ * { box-sizing: border-box; }
15088
+ body {
15089
+ margin: 0;
15090
+ min-height: 100vh;
15091
+ display: grid;
15092
+ place-items: center;
15093
+ padding: 24px;
15094
+ background:
15095
+ radial-gradient(1200px 600px at 50% -10%, var(--notice-soft), transparent 60%),
15096
+ var(--page-bg);
15097
+ color: var(--foreground);
15098
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
15099
+ -webkit-font-smoothing: antialiased;
15100
+ }
15101
+ main {
15102
+ width: min(520px, 100%);
15103
+ border: 1px solid var(--divider);
15104
+ border-radius: 16px;
15105
+ background: var(--surface);
15106
+ padding: 40px 36px;
15107
+ box-shadow: var(--shadow);
15108
+ text-align: center;
15109
+ }
15110
+ .logo {
15111
+ width: 44px;
15112
+ height: 44px;
15113
+ margin: 28px auto 0;
15114
+ color: var(--accent);
15115
+ }
15116
+ .logo svg { display: block; width: 100%; height: 100%; }
15117
+ .badge {
15118
+ width: 64px;
15119
+ height: 64px;
15120
+ margin: 0 auto 22px;
15121
+ display: grid;
15122
+ place-items: center;
15123
+ border-radius: 999px;
15124
+ }
15125
+ .badge--success { background: var(--success-soft); color: var(--success); }
15126
+ .badge--notice { background: var(--notice-soft); color: var(--accent); }
15127
+ .mark { width: 32px; height: 32px; }
15128
+ h1 {
15129
+ margin: 0 0 12px;
15130
+ font-size: 23px;
15131
+ font-weight: 600;
15132
+ line-height: 1.3;
15133
+ letter-spacing: -0.01em;
15134
+ color: var(--foreground);
15135
+ }
15136
+ p {
15137
+ margin: 0;
15138
+ color: var(--muted);
15139
+ font-size: 15px;
15140
+ line-height: 1.6;
14752
15141
  }
14753
15142
  </style>
14754
15143
  </head>
14755
15144
  <body>
14756
15145
  <main>
15146
+ <div class="${badgeClass}">${badgeMark}</div>
14757
15147
  <h1>${escapeHtml(args.title)}</h1>
14758
15148
  <p>${escapeHtml(args.body)}</p>
15149
+ <div class="logo">${LINZUMI_LOGO_SVG}</div>
14759
15150
  </main>
14760
15151
  </body>
14761
15152
  </html>`;
@@ -14786,10 +15177,12 @@ function openBrowser(url) {
14786
15177
  });
14787
15178
  });
14788
15179
  }
15180
+ var LINZUMI_LOGO_SVG;
14789
15181
  var init_oauth = __esm({
14790
15182
  "src/oauth.ts"() {
14791
15183
  "use strict";
14792
15184
  init_runnerLogger();
15185
+ LINZUMI_LOGO_SVG = '<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><path fill="currentColor" d="M206.7,82.37l2.55-6.45,2.01-3.63c.67-1.21,1.07-1.88,2.55-3.63,1.34-1.61,3.36-4.43,5.64-6.58,2.42-2.24,5.06-4.3,7.93-6.18,3.92-2.53,8.17-4.52,12.63-5.91,1.79-.63,4.03-1.16,6.72-1.61,6.1-.88,12.29-.92,18.4-.13,2.69.45,5.06,1.03,7.12,1.75,2.15.67,3.49,1.07,5.64,2.15,4.55,2.16,8.87,4.77,12.9,7.79,1.61,1.34,2.42,2.15,4.16,4.16l5.91,7.66c1.34,2.01,1.34,2.28,2.15,4.03l2.55,6.58c.67,1.34-.13,1.34,1.07.94,1.21-.27,4.3-2.01,6.45-2.82,4.7-1.83,9.62-3,14.64-3.49,2.42-.27,3.76-.54,7.12-.27s9.27,1.21,12.76,2.15c3.63.94,6.58,2.55,8.73,3.49,2.15,1.07,2.55,1.48,4.16,2.55,3.49,2.51,6.77,5.29,9.81,8.33,1.52,1.7,3.04,3.58,4.57,5.64,2.74,4.49,4.86,9.32,6.31,14.37.67,2.28,1.07,3.49,1.34,6.72.27,3.22,0,10.48.4,12.76.27,2.28.27.67,1.61.94,1.48.27,4.3,0,6.72.4,2.42.27,4.97.85,7.66,1.75,6.76,2.32,13.01,5.92,18.4,10.61,2.28,1.75,2.96,2.69,4.3,4.3,3.51,3.93,6.32,8.43,8.33,13.3,1.07,2.15,1.21,2.28,1.88,5.1.54,2.96,1.61,8.19,1.61,12.36s-.81,9.67-1.34,12.76c-.67,3.22-1.07,4.03-2.15,6.18-2.16,4.54-4.81,8.82-7.93,12.76-1.25,1.61-2.6,3-4.03,4.16-1.48,1.34-2.69,2.42-4.7,3.76-3.79,2.34-7.74,4.4-11.82,6.18l-6.58,2.15c-2.06.45-4.12.72-6.18.81l-7.25.13c-2.28,0-2.82.27-6.58-.4-3.76-.81-11.69-2.82-15.99-4.3-4.16-1.34-6.31-2.69-9.67-4.43-3.49-1.75-10.48-6.18-12.9-7.39-8.26-4.54-16.78-8.58-25.52-12.09-4.3-1.75-9.27-3.22-11.82-4.03-2.55-.81-5.24-1.61-7.66-2.15-8.39-2.25-16.99-3.6-25.66-4.03-3.9-.27-4.84-.27-8.19-.13-3.67-.09-7.79.04-12.36.4-7.9.79-15.75,2.04-23.51,3.76-8.37,2.26-16.62,4.95-24.72,8.06-3.22,1.34-2.82.81-9.13,4.16-6.31,3.36-22.03,12.22-28.75,15.58-6.72,3.36-8.73,3.49-11.82,4.43-3.09,1.07-4.3,1.21-6.58,1.61-7.3,1.09-14.71,1.18-22.03.27-2.55-.54-5.78-1.61-7.79-2.28-2.01-.54-2.15-.67-4.03-1.75-2.02-1.07-5.64-3.22-7.79-4.7-3.24-2.15-6.14-4.77-8.6-7.79-1.34-1.52-2.69-3.36-4.03-5.51-1.52-2.51-2.96-5.24-4.3-8.19-2.03-5.46-3.03-11.24-2.96-17.06,0-2.96.13-6.72.67-9.67.54-2.87,1.34-5.6,2.42-8.19,4.24-10.52,12.04-19.22,22.03-24.58,5.19-2.98,10.88-4.98,16.79-5.91,2.55-.4,5.37-.13,6.72-.4,1.34-.27,1.34,1.34,1.61-.94.4-2.28,0-9.4.4-12.76.27-3.36.81-4.97,1.48-7.25,1.87-6.19,4.88-11.97,8.87-17.06,3.47-3.98,7.39-7.54,11.69-10.61,4.43-2.61,9.11-4.77,13.97-6.45,2.55-.67,6.99-1.34,9.13-1.75,2.15-.27,1.88-.13,3.63-.13,1.79-.09,4.03,0,6.72.27,2.69.18,5.73.72,9.13,1.61,3.49,1.07,9.27,4.03,11.28,4.7s.4.27,1.07-.94ZM512.27,255.7c0,141.39-114.62,256-256,256S.27,397.09.27,255.7,114.89-.3,256.27-.3s256,114.62,256,256ZM449.33,352.66c14.67-29.16,22.94-62.09,22.94-96.95,0-119.29-96.71-216-216-216S40.27,136.41,40.27,255.7c0,41.89,11.94,80.98,32.57,114.09,1.7,1.79,2.94.65,3.05-.57.27-1.61,2.75-20.85,4.43-27.27,1.87-7.6,4.52-14.98,7.93-22.03,1.75-3.49,3.76-7.93,6.18-11.82,8.77-13.95,19.75-26.38,32.51-36.81,7.24-5.32,14.87-10.08,22.84-14.24,9.74-4.78,20.09-8.22,30.76-10.21,4.03-.99,7.61-1.66,10.75-2.02,3.36-.54,5.78-.54,9.27-.94,3.49-.54,9.32-1.16,11.82-2.01,6.65-2.27,5.64-6.72,3.09-7.52-2.55-.81-9.67-1.88-13.84-2.96-4.16-1.21-8.73-2.82-11.28-3.76-2.55-.94-2.28-.94-3.9-2.02l-5.91-4.03c-.94-.94-1.34-.94.54-2.02l10.21-5.51c2.01-1.07,1.75-.67,3.09-.13,1.34.67,4.3,2.42,6.72,3.36,7.2,2.69,14.96,3.52,22.57,2.42,1.34-.27,1.34-.54,1.61-1.21.27-.67.81-.94-.13-2.69-.94-1.88-4.43-6.58-5.64-8.46l-1.21-2.28c-.27-.45-1.07-1.07,1.21-1.75,2.42-.54,10.48-2.58,12.9-2.82.86-.09.67,0,1.75,1.07,1.07,1.07,3.09,3.63,4.84,5.24,1.88,1.61,4.43,3.22,6.18,4.3,2.8,1.71,5.99,2.68,9.27,2.82,1.88,0,4.97-.13,7.12-.67,2.15-.4,3.63-.94,5.64-2.02,2.01-1.07,4.7-2.69,6.72-4.43,2.01-1.75,3.49-5.1,5.64-6.04,2.15-.81,4.97.54,7.12.94,2.28.4,4.97,1.21,6.18,1.61,1.21.4,2.01.4.67,2.28l-8.06,10.34c-1.34,1.88.13,5.51,1.21,5.64l5.64.27c2.28-.13,4.43.13,7.66-.67,3.36-.81,9.27-2.96,11.82-3.9,2.69-.94,3.22-1.34,4.16-2.01.81-.54,1.39-1.03,1.75-1.48.45-.27.67-1.07,2.82,0s9.4,4.97,11.28,6.18c2.01,1.21,1.07.94,0,1.75l-5.51,3.9c-1.48.94-1.07.94-3.22,1.88-2.15,1.07-6.58,2.96-9.67,4.03s-4.03,1.75-8.73,2.69c-4.7.81-14.51,1.75-19.48,2.55-4.97.81-7.93,1.34-10.34,2.01-2.28.67-2.15.94-3.49,1.88-1.48,1.07-2.69,1.61-4.97,3.9-2.28,2.28-5.91,6.85-8.73,9.81-2.82,2.96-6.45,6.04-8.33,7.79-2.02,1.61-1.61,1.34-3.36,2.42l-6.99,4.03-4.03,2.02-6.72,2.15c-2.42.67-3.9,1.21-7.66,2.01l-14.91,2.82c-3.63.81-4.3,1.07-7.12,2.01-2.96,1.07-6.22,2.46-9.81,4.16-3.22,1.43-6.81,3.45-10.75,6.04-13.6,9.14-24.61,21.63-31.97,36.27-2.01,3.76-2.96,6.58-3.9,9.27-1.07,2.69-1.61,4.03-2.28,6.58-.67,2.69-1.48,5.24-2.15,9.27-.54,4.16-1.48,11.15-1.75,15.45-.27,4.16-.27,6.04,0,10.21l1.88,14.91c.67,3.76,1.48,5.91,2.15,8.19l1.88,5.64c.99,2.6,2.42,5.78,4.3,9.54,2.15,3.76,5.37,9.13,8.06,13.03,10.08,14.51,23.81,26.36,40.17,33.05,22.55,9.22,43.32,10.46,61.89,10.46,3.25,0,6.49-.09,9.71-.23,3.07-.42,2.74-6.09,10.48-9.42,5.26-2.26,13.7-2.42,14.91-2.55,1.07-.13-5.24-4.57-7.79-6.45-2.55-1.88-4.97-4.3-6.72-5.91-1.75-1.75-2.42-2.28-3.9-4.16-1.61-1.88-3.9-5.51-5.24-7.12-1.34-1.61-1.48-2.28-2.55-2.55-.98-.27-2.37-.09-4.16.54-1.88.4-5.37,1.21-7.12,1.88-1.75.67-2.02.81-3.09,2.01-1.07,1.07-2.69,3.76-3.63,4.7-.81.94-.67.67-1.48.94-.81.27-2.42.67-3.63.54-1.34-.18-2.55-.58-3.63-1.21-.81-.81-1.48-2.42-1.88-3.22-.4-.94-.54-1.21-.54-2.15l.4-3.49c.18-1.52.72-3.27,1.61-5.24.94-1.75,1.88-3.49,4.16-5.64,2.15-2.02,7.52-5.51,9.13-6.85,1.61-1.34,1.21-2.15.54-3.63-2.69-5.16-6.33-9.77-10.75-13.57-2.42-1.88-5.51-3.22-7.79-4.3-2.15-1.21-2.01-1.21-5.78-2.42-3.76-1.07-13.16-3.63-16.79-4.84-3.63-1.07-2.82-.94-5.1-2.01l-8.19-4.3c-1.75-.94-1.34-.54-2.69-1.61l-5.51-4.57c-2.18-1.93-3.77-4.45-4.57-7.25-.45-1.52-.45-2.87,0-4.03.4-1.34,0-1.21,2.01-3.63,2.15-2.42,7.25-7.93,10.21-10.75,3.09-2.82,4.97-4.03,7.79-6.04,5.43-3.82,11.09-7.32,16.93-10.48,2.42-1.21,5.37-2.02,7.12-2.69,1.61-.81,2.64-1.43,3.09-1.88.54-.4.67.54.4-.94-.4-1.48-1.88-4.97-2.28-7.66-.4-2.82-.4-6.18,0-8.73.36-2.42,1.03-4.66,2.01-6.72,1.16-2.24,2.46-4.3,3.9-6.18,1.34-1.7,2.91-3.04,4.7-4.03,1.75-1.07,4.3-2.01,6.18-2.42,1.61-.45,3.13-.58,4.57-.4l4.57.27c1.34.18,2.55.49,3.63.94,1.07.54,2.55,1.48,3.09,2.01.4.67.67.13-.27,1.61-1.07,1.48-4.16,4.57-5.78,7.12-1.61,2.69-2.96,5.6-4.03,8.73-.94,3.22-1.88,8.73-2.01,10.75-.27,2.15,4.7,1.75,5.1.54.54-1.07.27-2.55,1.07-5.1.81-2.42,2.42-7.25,3.9-9.81,1.34-2.55,2.82-3.76,4.57-5.51,1.79-1.88,3.67-3.45,5.64-4.7,1.88-1.21,3.22-1.75,5.64-2.28,2.42-.4,6.18-.67,8.73-.67,2.42,0,3.9.13,6.18.94,2.15.81,5.24,2.69,7.12,4.03,1.79,1.52,3.22,3.09,4.3,4.7,1.21,1.61,1.88,2.96,2.55,5.1.67,2.15,1.34,5.1,1.34,7.66,0,2.69-.54,6.04-1.21,8.19-.63,2.24-1.43,4.03-2.42,5.37-.67,1.34-.94,1.61-2.15,2.82-1.21,1.34-3.22,3.22-4.97,4.57-1.75,1.21-4.43,2.28-5.51,3.22-.94.81-.4,2.51.4,2.42.94.13,1.75.4,4.57-.13,2.82-.4,8.33-2.15,12.22-2.82,11.99-2.63,24.41-2.63,36.4,0,3.22.54,4.16,1.07,6.31,1.75l6.04,2.28c2.42,1.07,5.51,2.42,8.19,4.16,8.15,5.36,15.14,12.3,20.55,20.42,1.61,2.42,2.87,4.66,3.76,6.72,1.85,4.51,3.42,9.13,4.7,13.84.67,2.82,1.21,4.43,1.61,8.19.4,3.76.4,11.82.54,14.37.27,2.55-.13,2.28,1.75,1.34,1.75-.94,20.09-7.74,30.63-31.57,9.16-20.71,1.9-47.26-2.28-55.34-1.75-3.22-4.16-6.99-6.31-9.81-4.44-5.14-9.6-9.62-15.31-13.3-2.96-1.7-5.55-3.09-7.79-4.16-4.33-1.6-8.77-2.86-13.3-3.76l-6.18-.4h-14.37c-2.69-.27-3.1-3.18-.54-4.3,3.64-1.58,9.88-2.63,18.54-2.42,8.78.21,14.91,1.88,17.33,2.55,13.98,3.94,30.19,16.65,38.55,30.76,6.61,11.16,9.81,29.96,9.81,29.96.37,1.75,2.18,1.66,3.08.35ZM232.78,345.3c-3.91,0-7.08,3.17-7.08,7.08s3.17,7.08,7.08,7.08,7.08-3.17,7.08-7.08-3.17-7.08-7.08-7.08Z"/></svg>';
14793
15186
  }
14794
15187
  });
14795
15188
 
@@ -15502,7 +15895,7 @@ var init_remoteCodexExecutionContext = __esm({
15502
15895
  "SHELL",
15503
15896
  "TMPDIR"
15504
15897
  ]);
15505
- ignoredEnvOverrideKeys = /* @__PURE__ */ new Set(["PATH"]);
15898
+ ignoredEnvOverrideKeys = /* @__PURE__ */ new Set(["PATH", "GIT_PAGER"]);
15506
15899
  deniedEnvKeys = /* @__PURE__ */ new Set([
15507
15900
  "BASH_ENV",
15508
15901
  "ENV",
@@ -18127,7 +18520,7 @@ var linzumiCliVersion, linzumiCliVersionText;
18127
18520
  var init_version = __esm({
18128
18521
  "src/version.ts"() {
18129
18522
  "use strict";
18130
- linzumiCliVersion = "0.0.85-beta";
18523
+ linzumiCliVersion = "0.0.87-beta";
18131
18524
  linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
18132
18525
  }
18133
18526
  });
@@ -18406,6 +18799,9 @@ function releaseRunnerLock(path2, record) {
18406
18799
  }
18407
18800
  }
18408
18801
  function readRunnerLockForRelease(path2) {
18802
+ return readRunnerLockOwner(path2);
18803
+ }
18804
+ function readRunnerLockOwner(path2) {
18409
18805
  try {
18410
18806
  return readRunnerLockIfPresent(path2);
18411
18807
  } catch (_error) {
@@ -18526,8 +18922,216 @@ var init_runnerLock = __esm({
18526
18922
  }
18527
18923
  });
18528
18924
 
18529
- // src/runnerConsoleReporter.ts
18925
+ // src/runnerLockTakeover.ts
18926
+ function runnerLockTakeoverSleep(ms) {
18927
+ return new Promise((resolve12) => {
18928
+ setTimeout(resolve12, ms);
18929
+ });
18930
+ }
18931
+ function runnerLockConflictReport(baseMessage) {
18932
+ const bar = "=".repeat(64);
18933
+ return [
18934
+ "",
18935
+ bar,
18936
+ "ERROR: linzumi connect could not start - workspace already in use",
18937
+ bar,
18938
+ baseMessage,
18939
+ bar,
18940
+ ""
18941
+ ].join("\n");
18942
+ }
18943
+ function isRunnerLockConflictReportedError(error) {
18944
+ return error instanceof RunnerLockConflictReportedError || error instanceof Error && error.name === runnerLockConflictReportedErrorName;
18945
+ }
18946
+ function shouldResolveRunnerLockConflict(lockTakeover) {
18947
+ return lockTakeover !== void 0;
18948
+ }
18949
+ async function resolveRunnerLockConflict(args) {
18950
+ const report = runnerLockConflictReport(args.baseMessage);
18951
+ if (args.takeOverWithoutPrompt) {
18952
+ args.prompt.writeReport(report);
18953
+ await stopHolderAndAwaitRelease(args.holder, args.lockPath, args.takeover);
18954
+ return { outcome: "take_over" };
18955
+ }
18956
+ if (!args.prompt.isInteractive()) {
18957
+ args.prompt.writeReport(report);
18958
+ return { outcome: "declined", report };
18959
+ }
18960
+ args.prompt.writeReport(report);
18961
+ const yes = await args.prompt.promptYesNo(
18962
+ `Another runner (pid ${args.holder.pid}) is already connected to this workspace. Stop it and run here instead? [y/N] `
18963
+ );
18964
+ if (!yes) {
18965
+ return { outcome: "declined", report };
18966
+ }
18967
+ await stopHolderAndAwaitRelease(args.holder, args.lockPath, args.takeover);
18968
+ return { outcome: "take_over" };
18969
+ }
18970
+ async function stopHolderAndAwaitRelease(holder, lockPath, deps) {
18971
+ deps.log("runner.lock_takeover_requested", {
18972
+ holderPid: holder.pid,
18973
+ holderRunnerId: holder.runnerId,
18974
+ lockPath
18975
+ });
18976
+ if (!deps.isPidAlive(holder.pid)) {
18977
+ deps.log("runner.lock_takeover_holder_already_gone", {
18978
+ holderPid: holder.pid,
18979
+ lockPath
18980
+ });
18981
+ return;
18982
+ }
18983
+ if (!stillHeldBy(deps, holder)) {
18984
+ deps.log("runner.lock_takeover_owner_changed", {
18985
+ holderPid: holder.pid,
18986
+ lockPath
18987
+ });
18988
+ return;
18989
+ }
18990
+ sendSignalTolerant(deps, holder.pid, "SIGTERM");
18991
+ if (await waitForHolderRelease(holder, deps, runnerLockTakeoverGracefulStopMs)) {
18992
+ deps.log("runner.lock_takeover_graceful", {
18993
+ holderPid: holder.pid,
18994
+ lockPath
18995
+ });
18996
+ return;
18997
+ }
18998
+ if (!stillHeldBy(deps, holder)) {
18999
+ deps.log("runner.lock_takeover_owner_changed", {
19000
+ holderPid: holder.pid,
19001
+ lockPath
19002
+ });
19003
+ return;
19004
+ }
19005
+ deps.log("runner.lock_takeover_escalating_sigkill", {
19006
+ holderPid: holder.pid,
19007
+ lockPath
19008
+ });
19009
+ sendSignalTolerant(deps, holder.pid, "SIGKILL");
19010
+ if (await waitForHolderRelease(holder, deps, runnerLockTakeoverForcefulStopMs)) {
19011
+ deps.log("runner.lock_takeover_forceful", {
19012
+ holderPid: holder.pid,
19013
+ lockPath
19014
+ });
19015
+ return;
19016
+ }
19017
+ throw new Error(
19018
+ `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.`
19019
+ );
19020
+ }
19021
+ async function waitForHolderRelease(holder, deps, timeoutMs) {
19022
+ const deadline = deps.now() + timeoutMs;
19023
+ while (deps.now() < deadline) {
19024
+ if (!deps.isPidAlive(holder.pid) || deps.lockReleased()) {
19025
+ return true;
19026
+ }
19027
+ await deps.sleep(runnerLockTakeoverPollMs);
19028
+ }
19029
+ return !deps.isPidAlive(holder.pid) || deps.lockReleased();
19030
+ }
19031
+ function stillHeldBy(deps, holder) {
19032
+ const current = deps.currentLockOwner();
19033
+ return current !== void 0 && current.machineId === holder.machineId && current.runnerId === holder.runnerId && current.pid === holder.pid && current.startedAt === holder.startedAt;
19034
+ }
19035
+ function sendSignalTolerant(deps, pid, signal) {
19036
+ try {
19037
+ deps.signalPid(pid, signal);
19038
+ } catch (error) {
19039
+ const code = error !== null && typeof error === "object" && "code" in error ? error.code : void 0;
19040
+ if (code !== "ESRCH") {
19041
+ deps.log("runner.lock_takeover_signal_failed", {
19042
+ holderPid: pid,
19043
+ signal,
19044
+ code: typeof code === "string" ? code : null,
19045
+ message: error instanceof Error ? error.message : String(error)
19046
+ });
19047
+ }
19048
+ }
19049
+ }
19050
+ var runnerLockTakeoverGracefulStopMs, runnerLockTakeoverForcefulStopMs, runnerLockTakeoverPollMs, runnerLockConflictReportedErrorName, RunnerLockConflictReportedError;
19051
+ var init_runnerLockTakeover = __esm({
19052
+ "src/runnerLockTakeover.ts"() {
19053
+ "use strict";
19054
+ runnerLockTakeoverGracefulStopMs = 1e4;
19055
+ runnerLockTakeoverForcefulStopMs = 5e3;
19056
+ runnerLockTakeoverPollMs = 200;
19057
+ runnerLockConflictReportedErrorName = "RunnerLockConflictReportedError";
19058
+ RunnerLockConflictReportedError = class extends Error {
19059
+ constructor() {
19060
+ super("");
19061
+ this.name = runnerLockConflictReportedErrorName;
19062
+ }
19063
+ };
19064
+ }
19065
+ });
19066
+
19067
+ // src/blessedTputSetulcShim.ts
18530
19068
  import blessed from "blessed";
19069
+ function balanceTerminfoConditionals(cap) {
19070
+ if (cap.indexOf("%;") === -1) {
19071
+ return cap;
19072
+ }
19073
+ let depth = 0;
19074
+ let repaired = "";
19075
+ let index = 0;
19076
+ while (index < cap.length) {
19077
+ const here = cap[index];
19078
+ const next = cap[index + 1];
19079
+ if (here === "%" && next === "%") {
19080
+ repaired += "%%";
19081
+ index += 2;
19082
+ continue;
19083
+ }
19084
+ if (here === "%" && next === "?") {
19085
+ depth += 1;
19086
+ repaired += "%?";
19087
+ index += 2;
19088
+ continue;
19089
+ }
19090
+ if (here === "%" && next === ";") {
19091
+ if (depth > 0) {
19092
+ depth -= 1;
19093
+ repaired += "%;";
19094
+ }
19095
+ index += 2;
19096
+ continue;
19097
+ }
19098
+ repaired += here;
19099
+ index += 1;
19100
+ }
19101
+ return repaired;
19102
+ }
19103
+ function installBlessedSetulcShim(blessedModule = blessed) {
19104
+ const tput = blessedModule.Tput;
19105
+ const proto = tput?.prototype;
19106
+ const original = proto?._compile;
19107
+ if (proto === void 0 || typeof original !== "function") {
19108
+ return;
19109
+ }
19110
+ if (proto[SHIM_FLAG] === true) {
19111
+ return;
19112
+ }
19113
+ const patched = function patchedCompile(info, key, str) {
19114
+ const safeStr = typeof str === "string" ? balanceTerminfoConditionals(str) : str;
19115
+ return original.call(this, info, key, safeStr);
19116
+ };
19117
+ proto._compile = patched;
19118
+ Object.defineProperty(proto, SHIM_FLAG, {
19119
+ value: true,
19120
+ enumerable: false,
19121
+ configurable: true,
19122
+ writable: true
19123
+ });
19124
+ }
19125
+ var SHIM_FLAG;
19126
+ var init_blessedTputSetulcShim = __esm({
19127
+ "src/blessedTputSetulcShim.ts"() {
19128
+ "use strict";
19129
+ SHIM_FLAG = "__linzumiSetulcShimInstalled";
19130
+ }
19131
+ });
19132
+
19133
+ // src/runnerConsoleReporter.ts
19134
+ import blessed2 from "blessed";
18531
19135
  function reportRunnerConsoleEvent(event, payload) {
18532
19136
  if (shouldRenderDashboard()) {
18533
19137
  const tui = initializeDashboardTui(dashboardState);
@@ -19512,20 +20116,21 @@ function handleDashboardKey(state, key, exitProcess = () => process.kill(process
19512
20116
  function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill(process.pid, "SIGINT")) {
19513
20117
  let rendering = false;
19514
20118
  let rawScrollAnchor;
19515
- const screen = blessed.screen({
20119
+ installBlessedSetulcShim();
20120
+ const screen = blessed2.screen({
19516
20121
  title: "Linzumi Commander",
19517
20122
  smartCSR: true,
19518
20123
  fullUnicode: true,
19519
20124
  mouse: true
19520
20125
  });
19521
- const header = blessed.box({
20126
+ const header = blessed2.box({
19522
20127
  top: 0,
19523
20128
  left: 0,
19524
20129
  width: "100%",
19525
20130
  height: 1,
19526
20131
  tags: false
19527
20132
  });
19528
- const tableElement = blessed.listtable({
20133
+ const tableElement = blessed2.listtable({
19529
20134
  top: 1,
19530
20135
  left: 0,
19531
20136
  width: "100%",
@@ -19554,7 +20159,7 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
19554
20159
  }
19555
20160
  }
19556
20161
  });
19557
- const rawTitle = blessed.box({
20162
+ const rawTitle = blessed2.box({
19558
20163
  top: 0,
19559
20164
  left: 12,
19560
20165
  right: 0,
@@ -19562,7 +20167,7 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
19562
20167
  tags: false,
19563
20168
  hidden: true
19564
20169
  });
19565
- const backButton = blessed.button({
20170
+ const backButton = blessed2.button({
19566
20171
  top: 0,
19567
20172
  left: 0,
19568
20173
  width: 10,
@@ -19577,7 +20182,7 @@ function createRunnerConsoleDashboardTui(state, exitProcess = () => process.kill
19577
20182
  hover: { inverse: true }
19578
20183
  }
19579
20184
  });
19580
- const rawBox = blessed.box({
20185
+ const rawBox = blessed2.box({
19581
20186
  top: 3,
19582
20187
  left: 0,
19583
20188
  width: "100%",
@@ -20056,6 +20661,7 @@ var dashboardState, maxRawLines, escapeKey, ctrlCKey, enterKey, upKey, downKey,
20056
20661
  var init_runnerConsoleReporter = __esm({
20057
20662
  "src/runnerConsoleReporter.ts"() {
20058
20663
  "use strict";
20664
+ init_blessedTputSetulcShim();
20059
20665
  dashboardState = {
20060
20666
  jobs: /* @__PURE__ */ new Map(),
20061
20667
  discovery: /* @__PURE__ */ new Map(),
@@ -21381,6 +21987,7 @@ import { spawn as spawn9, spawnSync as spawnSync5 } from "node:child_process";
21381
21987
  import { createHash as createHash5, randomUUID as randomUUID4 } from "node:crypto";
21382
21988
  import {
21383
21989
  chmodSync as chmodSync2,
21990
+ existsSync as existsSync13,
21384
21991
  lstatSync,
21385
21992
  mkdirSync as mkdirSync12,
21386
21993
  mkdtempSync as mkdtempSync4,
@@ -21395,6 +22002,7 @@ import {
21395
22002
  import { readFile as readFile2 } from "node:fs/promises";
21396
22003
  import { createServer as createServer3 } from "node:http";
21397
22004
  import { homedir as homedir13, hostname as hostname2, tmpdir as tmpdir3 } from "node:os";
22005
+ import { createInterface } from "node:readline";
21398
22006
  import {
21399
22007
  basename as basename8,
21400
22008
  dirname as dirname13,
@@ -21419,42 +22027,35 @@ async function runLocalCodexRunner(options) {
21419
22027
  });
21420
22028
  try {
21421
22029
  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", {
22030
+ const machineId = options.machineId;
22031
+ const acquire = () => acquireRunnerLock({
22032
+ machineId,
22033
+ runnerId: options.runnerId,
22034
+ cwd: options.cwd,
22035
+ workspace: runnerWorkspaceSlug(options) ?? null,
22036
+ linzumiUrl: options.kandanUrl,
22037
+ launchSource: options.launchSource,
22038
+ configPath: options.runnerLockConfigPath,
22039
+ // Wedged-runner takeover: a lock holder whose pid is alive but
22040
+ // whose lock heartbeat went stale (>3 min) gets SIGKILLed and its
22041
+ // lock replaced, instead of blocking recovery until someone runs
22042
+ // kill -9 by hand. Log it so the runner log explains where the old
22043
+ // pid went.
22044
+ onTakeover: (takeover) => {
22045
+ log2("runner.lock_takeover", {
21450
22046
  runnerId: options.runnerId,
21451
- heldByRunnerId: error.heldBy.runnerId,
21452
- heldByPid: error.heldBy.pid,
21453
- lockPath: error.lockPath
22047
+ holderRunnerId: takeover.holderRunnerId,
22048
+ holderPid: takeover.holderPid,
22049
+ reason: takeover.reason,
22050
+ lockPath: takeover.lockPath
21454
22051
  });
21455
22052
  }
21456
- throw error;
21457
- }
22053
+ });
22054
+ const runnerLock = await acquireRunnerLockWithConflictResolution(
22055
+ acquire,
22056
+ options,
22057
+ log2
22058
+ );
21458
22059
  cleanup.actions.push(() => runnerLock.release());
21459
22060
  log2("runner.lock_acquired", {
21460
22061
  path: runnerLock.path,
@@ -22474,6 +23075,11 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22474
23075
  if (options.channelSession !== void 0 && codex === void 0) {
22475
23076
  throw new Error("channel session requires a Codex app-server connection");
22476
23077
  }
23078
+ const ownsRespawnableAppServer = runnerOwnsRespawnableAppServer({
23079
+ codexUrl: options.codexUrl,
23080
+ threadProcessRole: options.threadProcess?.role,
23081
+ ownsStartedAppServer: started !== void 0
23082
+ });
22477
23083
  const seq = { value: 0 };
22478
23084
  const discoveredCodexThreads = { value: [] };
22479
23085
  const runtimeDefaults = runnerRuntimeDefaults(options);
@@ -22565,6 +23171,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22565
23171
  const dynamicChannelSessions = /* @__PURE__ */ new Map();
22566
23172
  const dynamicChannelSessionCodexClients = /* @__PURE__ */ new Map();
22567
23173
  const codexTurnFailureGuards = /* @__PURE__ */ new Map();
23174
+ const lossReportPushes = [];
22568
23175
  const codexTurnFailureGuardFor = (client) => {
22569
23176
  const existing = codexTurnFailureGuards.get(client);
22570
23177
  if (existing !== void 0) {
@@ -22577,7 +23184,36 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22577
23184
  ([codexThreadId]) => dynamicChannelSessionCodexClients.get(codexThreadId) === client
22578
23185
  ).map(([, session]) => session)
22579
23186
  ],
22580
- log: log2
23187
+ log: log2,
23188
+ // Spec: plans/2026-06-13-orphaned-mid-turn-reconnect-recovery-spec.md
23189
+ // The runner process and its channel survive an app-server death, so the
23190
+ // server otherwise keeps these thread bindings "connected" and never
23191
+ // offers reconnect. Report the loss per affected thread (best-effort)
23192
+ // so the bindings flip to disconnected/reconnectable upstream.
23193
+ //
23194
+ // Only do this for a runner that owns a respawnable app-server. For an
23195
+ // external/remote `codexUrl` backend (or a thread-process worker) this
23196
+ // runner cannot relaunch the dead app-server, so a reconnect could never
23197
+ // recover the thread - marking it reconnectable would be misleading. In
23198
+ // that case we still fail the in-flight turns terminally, but leave the
23199
+ // binding as-is rather than offering a reconnect that cannot succeed.
23200
+ onActiveThreadsFailed: ownsRespawnableAppServer ? (threadIds) => {
23201
+ for (const codexThreadId of threadIds) {
23202
+ lossReportPushes.push(
23203
+ kandan.push(topic, "session:app_server_lost", {
23204
+ codex_thread_id: codexThreadId
23205
+ }).then(
23206
+ () => void 0,
23207
+ (error) => {
23208
+ log2("kandan.session_app_server_lost_push_failed", {
23209
+ codex_thread_id: codexThreadId,
23210
+ message: error instanceof Error ? error.message : String(error)
23211
+ });
23212
+ }
23213
+ )
23214
+ );
23215
+ }
23216
+ } : void 0
22581
23217
  });
22582
23218
  codexTurnFailureGuards.set(client, guard);
22583
23219
  return guard;
@@ -22681,7 +23317,8 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22681
23317
  portForwardWatcher: channelSessionPortForwardWatcherOptions({
22682
23318
  rootPid: portForwardWatcherRootPid,
22683
23319
  start: options.portForwardWatcher,
22684
- commanderBoundPids: args.commanderBoundPids
23320
+ commanderBoundPids: args.commanderBoundPids,
23321
+ commanderBoundPorts: args.commanderBoundPorts
22685
23322
  }),
22686
23323
  suppressedForwardPorts,
22687
23324
  onForwardPortApproved: (port, attribution) => {
@@ -22876,6 +23513,12 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
22876
23513
  portForwardWatcherRootPid: handle.processPid,
22877
23514
  commanderBoundPids: (handle.commanderManagedPorts ?? []).flatMap(
22878
23515
  (port) => port.pid === void 0 ? [] : [port.pid]
23516
+ ),
23517
+ // The exact codex app-server ports are commander-managed even though
23518
+ // their listening pid is the wrapped grandchild, not the recorded
23519
+ // wrapper pid (Codex P1).
23520
+ commanderBoundPorts: (handle.commanderManagedPorts ?? []).map(
23521
+ (port) => port.port
22879
23522
  )
22880
23523
  });
22881
23524
  switch (control.type) {
@@ -23025,6 +23668,7 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
23025
23668
  }
23026
23669
  });
23027
23670
  if (sharedCodexTurnFailureGuard !== void 0) {
23671
+ const appServerLost = { handled: false };
23028
23672
  const failCodexSessionTurns = (cause) => {
23029
23673
  if (cleanup.closePromise !== void 0) {
23030
23674
  return;
@@ -23032,6 +23676,27 @@ async function openLocalCodexRunner(options, log2, cleanup, close) {
23032
23676
  log2("codex.app_server_connection_lost", { cause });
23033
23677
  sharedCodexTurnFailureGuard.failActiveTurns(cause);
23034
23678
  started?.stop();
23679
+ if (ownsRespawnableAppServer && !appServerLost.handled) {
23680
+ appServerLost.handled = true;
23681
+ console.error(
23682
+ `Codex app-server connection was lost (${cause}); exiting so a fresh runner can relaunch it. Run \`linzumi connect\` to reconnect.`
23683
+ );
23684
+ const sessionDrains = [
23685
+ ...channelSession !== void 0 ? [channelSession] : [],
23686
+ ...dynamicChannelSessions.values()
23687
+ ].map(
23688
+ (session) => session.close().catch((error) => {
23689
+ log2("codex.app_server_lost_session_drain_failed", {
23690
+ message: error instanceof Error ? error.message : String(error)
23691
+ });
23692
+ })
23693
+ );
23694
+ void awaitLossReportsThenExit({
23695
+ pushes: [...lossReportPushes, ...sessionDrains],
23696
+ exit: () => process.exit(75),
23697
+ safetyCapMs: 5e3
23698
+ });
23699
+ }
23035
23700
  };
23036
23701
  codex?.onClose?.((error) => failCodexSessionTurns(error.message));
23037
23702
  started?.process.once(
@@ -23787,6 +24452,19 @@ async function resolveSessionControl(channelSession, dynamicChannelSessions, con
23787
24452
  }
23788
24453
  return void 0;
23789
24454
  }
24455
+ function runnerOwnsRespawnableAppServer(args) {
24456
+ return args.codexUrl === void 0 && args.threadProcessRole !== "thread" && args.ownsStartedAppServer;
24457
+ }
24458
+ async function awaitLossReportsThenExit(args) {
24459
+ const setTimeoutImpl = args.setTimeoutFn ?? setTimeout;
24460
+ const drained = Promise.allSettled(args.pushes).then(() => void 0);
24461
+ const safetyCap = new Promise((resolve12) => {
24462
+ const handle = setTimeoutImpl(() => resolve12(), args.safetyCapMs);
24463
+ handle?.unref?.();
24464
+ });
24465
+ await Promise.race([drained, safetyCap]);
24466
+ args.exit();
24467
+ }
23790
24468
  function createCodexTurnFailureGuard(args) {
23791
24469
  const activeTurnsByThread = /* @__PURE__ */ new Map();
23792
24470
  const state = { failed: false };
@@ -23822,6 +24500,7 @@ function createCodexTurnFailureGuard(args) {
23822
24500
  if (activeTurns.length === 0) {
23823
24501
  return;
23824
24502
  }
24503
+ args.onActiveThreadsFailed?.(activeTurns.map(([threadId]) => threadId));
23825
24504
  const sessions = Array.from(args.sessions());
23826
24505
  for (const [threadId, turnId] of activeTurns) {
23827
24506
  args.log("codex.active_turn_failed_on_connection_loss", {
@@ -23979,6 +24658,101 @@ function installCleanupHandlers(close) {
23979
24658
  process.off("exit", closeOnExit);
23980
24659
  };
23981
24660
  }
24661
+ async function acquireRunnerLockWithConflictResolution(acquire, options, log2) {
24662
+ try {
24663
+ return acquire();
24664
+ } catch (error) {
24665
+ if (!isRunnerLockHeldError(error)) {
24666
+ throw error;
24667
+ }
24668
+ log2("runner.lock_held_by_live_process", {
24669
+ runnerId: options.runnerId,
24670
+ heldByRunnerId: error.heldBy.runnerId,
24671
+ heldByPid: error.heldBy.pid,
24672
+ lockPath: error.lockPath
24673
+ });
24674
+ const lockTakeover = options.lockTakeover;
24675
+ if (!shouldResolveRunnerLockConflict(lockTakeover)) {
24676
+ throw error;
24677
+ }
24678
+ const promptDeps = lockTakeover.prompt ?? defaultRunnerLockTakeoverPromptDeps(lockTakeover);
24679
+ const takeoverDeps = resolveRunnerLockTakeoverDeps(
24680
+ error.lockPath,
24681
+ lockTakeover.takeover,
24682
+ log2
24683
+ );
24684
+ const resolution = await resolveRunnerLockConflict({
24685
+ holder: error.heldBy,
24686
+ lockPath: error.lockPath,
24687
+ baseMessage: error.message,
24688
+ takeOverWithoutPrompt: lockTakeover.takeOverWithoutPrompt === true,
24689
+ prompt: promptDeps,
24690
+ takeover: takeoverDeps
24691
+ });
24692
+ if (resolution.outcome === "declined") {
24693
+ throw new RunnerLockConflictReportedError();
24694
+ }
24695
+ log2("runner.lock_takeover_interactive", {
24696
+ runnerId: options.runnerId,
24697
+ heldByRunnerId: error.heldBy.runnerId,
24698
+ heldByPid: error.heldBy.pid,
24699
+ lockPath: error.lockPath
24700
+ });
24701
+ return acquire();
24702
+ }
24703
+ }
24704
+ function defaultRunnerLockTakeoverPromptDeps(takeover) {
24705
+ return {
24706
+ isInteractive: () => takeover?.forceNonInteractive !== true && process.stdin.isTTY === true && process.stderr.isTTY === true,
24707
+ writeReport: (text2) => {
24708
+ process.stderr.write(`${text2}
24709
+ `);
24710
+ },
24711
+ promptYesNo: (question) => promptYesNoOnStdin(question)
24712
+ };
24713
+ }
24714
+ function promptYesNoOnStdin(question) {
24715
+ return new Promise((resolve12) => {
24716
+ const rl = createInterface({
24717
+ input: process.stdin,
24718
+ output: process.stderr
24719
+ });
24720
+ rl.question(question, (answer) => {
24721
+ rl.close();
24722
+ resolve12(/^y(es)?$/i.test(answer.trim()));
24723
+ });
24724
+ });
24725
+ }
24726
+ function resolveRunnerLockTakeoverDeps(lockPath, overrides, log2) {
24727
+ return {
24728
+ isPidAlive: overrides?.isPidAlive ?? runnerLockTakeoverPidIsAlive,
24729
+ signalPid: overrides?.signalPid ?? ((pid, signal) => {
24730
+ process.kill(pid, signal);
24731
+ }),
24732
+ lockReleased: overrides?.lockReleased ?? (() => !existsSync13(lockPath)),
24733
+ // Re-reads the on-disk owner so the takeover flow can revalidate identity
24734
+ // before signaling (never throws on a torn/partial lock file).
24735
+ currentLockOwner: overrides?.currentLockOwner ?? (() => readRunnerLockOwner(lockPath)),
24736
+ // runnerLockTakeoverSleep is deliberately NOT unref'ed - see its definition;
24737
+ // an unref'ed timer here would let `linzumi connect` exit before retrying
24738
+ // the lock acquisition during a takeover.
24739
+ sleep: overrides?.sleep ?? runnerLockTakeoverSleep,
24740
+ now: overrides?.now ?? (() => Date.now()),
24741
+ log: overrides?.log ?? ((event, payload) => {
24742
+ log2(event, payload);
24743
+ writeCliAuditEvent(event, payload);
24744
+ })
24745
+ };
24746
+ }
24747
+ function runnerLockTakeoverPidIsAlive(pid) {
24748
+ try {
24749
+ process.kill(pid, 0);
24750
+ return true;
24751
+ } catch (error) {
24752
+ const code = error !== null && typeof error === "object" && "code" in error ? error.code : void 0;
24753
+ return code !== "ESRCH";
24754
+ }
24755
+ }
23982
24756
  function launchCodexTui(codexBin, codexUrl, cwd, codexThreadId, session, fast) {
23983
24757
  const args = codexTuiArgs(codexUrl, codexThreadId, session, fast);
23984
24758
  writeCliAuditEvent("process.spawn", {
@@ -24965,29 +25739,18 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
24965
25739
  approvalPolicy: stringValue(control.approvalPolicy) ?? void 0
24966
25740
  });
24967
25741
  if (applied.approval_policy === "never") {
24968
- const pendingForSession = [
24969
- ...pendingClaudeCodeApprovals.entries()
24970
- ].filter(
24971
- ([, pending]) => pending.sessionId === void 0 || pending.sessionId === control.threadId
24972
- );
24973
- for (const [requestId, pending] of pendingForSession) {
24974
- pending.resolve({ type: "allow" });
24975
- await publishClaudeCodeMessageState(kandan, topic, {
24976
- workspace: pending.workspace,
24977
- channel: pending.channel,
24978
- threadId: pending.threadId,
24979
- sourceSeq: pending.sourceSeq,
24980
- status: "processing",
24981
- reason: "Claude Code tool approved",
24982
- sessionId: pending.sessionId ?? control.threadId
24983
- });
24984
- log2("claude_code.approval_request_auto_approved", {
24985
- request_id: requestId,
24986
- source_seq: pending.sourceSeq,
24987
- session_id: pending.sessionId ?? control.threadId,
24988
- reason: "full_access_enabled"
24989
- });
24990
- }
25742
+ await settleClaudeCodeApprovalsForSession({
25743
+ kandan,
25744
+ topic,
25745
+ pendingClaudeCodeApprovals,
25746
+ // activeClaudeCodeSessions is keyed by the SDK session id, so
25747
+ // control.threadId here is the SDK session id, NOT the linzumi thread
25748
+ // id. The drain matches pending approvals by their linzumi threadId,
25749
+ // so pass the session's stored linzumi threadId (Codex PR #1846 P1).
25750
+ sessionThreadId: claudeSession.threadId,
25751
+ log: log2,
25752
+ reason: "full_access_enabled"
25753
+ });
24991
25754
  }
24992
25755
  return {
24993
25756
  instanceId,
@@ -25837,6 +26600,7 @@ async function startClaudeCodeProviderInstance(args) {
25837
26600
  args.options,
25838
26601
  args.control
25839
26602
  );
26603
+ const initialPermissionMode = runtimeSettings.approvalPolicy === void 0 ? void 0 : claudePermissionModeForApprovalPolicy(runtimeSettings.approvalPolicy);
25840
26604
  if (args.resumeSessionId !== void 0) {
25841
26605
  try {
25842
26606
  await validateClaudeCodeResumeSession({
@@ -25856,6 +26620,16 @@ async function startClaudeCodeProviderInstance(args) {
25856
26620
  codexVersion: void 0
25857
26621
  };
25858
26622
  const abortController = new AbortController();
26623
+ const reaperRegistration = registerEngineChild({
26624
+ stop: () => {
26625
+ if (!abortController.signal.aborted) {
26626
+ abortController.abort(new Error("engine child reaper teardown"));
26627
+ }
26628
+ },
26629
+ pid: void 0,
26630
+ ownProcessGroup: false,
26631
+ kind: "claude_code"
26632
+ });
25859
26633
  const acceptedSourceSeqs = new Set(
25860
26634
  sourceSeq === void 0 ? [] : [sourceSeq]
25861
26635
  );
@@ -25863,6 +26637,7 @@ async function startClaudeCodeProviderInstance(args) {
25863
26637
  let startControlResponded = false;
25864
26638
  let activeSessionId;
25865
26639
  let sessionControls;
26640
+ let startupApprovalDrain;
25866
26641
  const planMirrorClient = createLinzumiMcpApiClient({
25867
26642
  kandanUrl: args.options.kandanUrl,
25868
26643
  accessToken: args.options.token,
@@ -26193,6 +26968,18 @@ async function startClaudeCodeProviderInstance(args) {
26193
26968
  linzumiContext
26194
26969
  }),
26195
26970
  model: claudeCodeModelForRuntimeModel(runtimeSettings.model),
26971
+ // Apply the user's initial access level at session START, mirroring
26972
+ // codex (which seeds approvalPolicy / sandbox via
26973
+ // codexThreadRuntimeOverrides at start). Without this every Claude
26974
+ // Code session started in the SDK's 'default' mode and the no-prompt
26975
+ // ("never" -> bypassPermissions) level only took effect once an
26976
+ // update_session_settings control arrived post-start - so WebFetch
26977
+ // (no built-in allowlist) still raised an approval card on turn 1.
26978
+ // The mode is derived from runtimeSettings.approvalPolicy, which the
26979
+ // server already gates so only "never" (paired with a full-access
26980
+ // sandbox) survives to map to bypassPermissions; lesser levels keep
26981
+ // their 'default' / 'acceptEdits' modes.
26982
+ ...initialPermissionMode === void 0 ? {} : { permissionMode: initialPermissionMode },
26196
26983
  resumeSessionId: args.resumeSessionId,
26197
26984
  abortController,
26198
26985
  streamingInput: inputQueue,
@@ -26210,6 +26997,22 @@ async function startClaudeCodeProviderInstance(args) {
26210
26997
  allowedTools: ["mcp__linzumi__*"],
26211
26998
  onSessionControls: (controls) => {
26212
26999
  sessionControls = controls;
27000
+ if (initialPermissionMode === "bypassPermissions") {
27001
+ startupApprovalDrain = settleClaudeCodeApprovalsForSession({
27002
+ kandan: args.kandan,
27003
+ topic: args.topic,
27004
+ pendingClaudeCodeApprovals: args.pendingClaudeCodeApprovals,
27005
+ sessionThreadId: threadId,
27006
+ log: args.log,
27007
+ reason: "full_access_started"
27008
+ }).catch((error) => {
27009
+ args.log("claude_code.startup_approval_drain_failed", {
27010
+ linzumi_thread_id: threadId,
27011
+ claude_session_id: activeSessionId ?? null,
27012
+ message: error instanceof Error ? error.message : String(error)
27013
+ });
27014
+ });
27015
+ }
26213
27016
  },
26214
27017
  onTurnCompleted: () => {
26215
27018
  inputQueue.completeTurn();
@@ -26260,6 +27063,7 @@ async function startClaudeCodeProviderInstance(args) {
26260
27063
  return result2;
26261
27064
  };
26262
27065
  const sessionWork = runSession();
27066
+ void sessionWork.finally(() => reaperRegistration.unregister()).catch(() => void 0);
26263
27067
  sessionWorkHandle.value = sessionWork;
26264
27068
  const settled = await Promise.race([
26265
27069
  sessionWork.then((result2) => ({
@@ -26301,6 +27105,9 @@ async function startClaudeCodeProviderInstance(args) {
26301
27105
  message: error instanceof Error ? error.message : String(error)
26302
27106
  });
26303
27107
  }
27108
+ if (startupApprovalDrain !== void 0) {
27109
+ await startupApprovalDrain;
27110
+ }
26304
27111
  void sessionWork.then(
26305
27112
  () => {
26306
27113
  args.log("claude_code.session_loop_ended", {
@@ -26336,6 +27143,35 @@ function claudePermissionModeForApprovalPolicy(approvalPolicy) {
26336
27143
  return void 0;
26337
27144
  }
26338
27145
  }
27146
+ function claudeCodeApprovalsForSessionThread(pendingClaudeCodeApprovals, sessionThreadId) {
27147
+ return [...pendingClaudeCodeApprovals.entries()].filter(
27148
+ ([, pending]) => pending.threadId === sessionThreadId
27149
+ );
27150
+ }
27151
+ async function settleClaudeCodeApprovalsForSession(args) {
27152
+ const pendingForSession = claudeCodeApprovalsForSessionThread(
27153
+ args.pendingClaudeCodeApprovals,
27154
+ args.sessionThreadId
27155
+ );
27156
+ for (const [requestId, pending] of pendingForSession) {
27157
+ pending.resolve({ type: "allow" });
27158
+ await publishClaudeCodeMessageState(args.kandan, args.topic, {
27159
+ workspace: pending.workspace,
27160
+ channel: pending.channel,
27161
+ threadId: pending.threadId,
27162
+ sourceSeq: pending.sourceSeq,
27163
+ status: "processing",
27164
+ reason: "Claude Code tool approved",
27165
+ sessionId: pending.sessionId ?? args.sessionThreadId
27166
+ });
27167
+ args.log("claude_code.approval_request_auto_approved", {
27168
+ request_id: requestId,
27169
+ source_seq: pending.sourceSeq,
27170
+ session_id: pending.sessionId ?? args.sessionThreadId,
27171
+ reason: args.reason
27172
+ });
27173
+ }
27174
+ }
26339
27175
  function claudeSteerInputText(input) {
26340
27176
  const items = arrayValue(input) ?? [];
26341
27177
  const text2 = items.flatMap((item) => {
@@ -26698,9 +27534,11 @@ function channelSessionPortForwardWatcherOptions(args) {
26698
27534
  ...args.rootPid === void 0 ? [] : [args.rootPid],
26699
27535
  ...args.commanderBoundPids ?? []
26700
27536
  ];
27537
+ const commanderBoundPorts = args.commanderBoundPorts ?? [];
26701
27538
  return {
26702
27539
  ...args.rootPid === void 0 ? {} : { rootPid: args.rootPid },
26703
27540
  ...commanderBoundPids.length === 0 ? {} : { commanderBoundPids },
27541
+ ...commanderBoundPorts.length === 0 ? {} : { commanderBoundPorts },
26704
27542
  ...args.start === void 0 ? {} : { start: args.start }
26705
27543
  };
26706
27544
  }
@@ -28502,6 +29340,7 @@ var init_runner = __esm({
28502
29340
  init_commanderAttachments();
28503
29341
  init_claudeCodePipeline();
28504
29342
  init_claudeCodePlanMirror();
29343
+ init_engineChildReaper();
28505
29344
  init_claudeCodeLiveBashOutput();
28506
29345
  init_claudeCodeSession();
28507
29346
  init_codexAppServer();
@@ -28524,6 +29363,7 @@ var init_runner = __esm({
28524
29363
  init_protocol();
28525
29364
  init_runnerLogger();
28526
29365
  init_runnerLock();
29366
+ init_runnerLockTakeover();
28527
29367
  init_runnerConsoleReporter();
28528
29368
  init_version();
28529
29369
  init_telemetry();
@@ -28564,7 +29404,7 @@ var init_runner = __esm({
28564
29404
  });
28565
29405
 
28566
29406
  // src/kandanTls.ts
28567
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "node:fs";
29407
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "node:fs";
28568
29408
  import { Agent } from "undici";
28569
29409
  import WsWebSocket from "ws";
28570
29410
  function kandanTlsTrustFromEnv() {
@@ -28575,7 +29415,7 @@ function kandanTlsTrustFromCaFile(caFile) {
28575
29415
  return void 0;
28576
29416
  }
28577
29417
  const trimmed = caFile.trim();
28578
- if (!existsSync13(trimmed)) {
29418
+ if (!existsSync14(trimmed)) {
28579
29419
  throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
28580
29420
  }
28581
29421
  const ca = readFileSync17(trimmed, "utf8");
@@ -48853,7 +49693,7 @@ __export(signupFlow_exports, {
48853
49693
  });
48854
49694
  import { spawn as spawn12, spawnSync as spawnSync7 } from "node:child_process";
48855
49695
  import {
48856
- existsSync as existsSync16,
49696
+ existsSync as existsSync17,
48857
49697
  constants as fsConstants,
48858
49698
  mkdirSync as mkdirSync15,
48859
49699
  mkdtempSync as mkdtempSync5,
@@ -48981,7 +49821,7 @@ function defaultSignupDraftStore(serviceUrl) {
48981
49821
  const path2 = defaultSignupDraftPath(serviceUrl);
48982
49822
  return {
48983
49823
  read: () => {
48984
- if (!existsSync16(path2)) {
49824
+ if (!existsSync17(path2)) {
48985
49825
  return void 0;
48986
49826
  }
48987
49827
  let parsed;
@@ -49060,7 +49900,7 @@ function defaultSignupTaskCachePath(serviceUrl) {
49060
49900
  );
49061
49901
  }
49062
49902
  function readSignupTaskCache(path2) {
49063
- if (!existsSync16(path2)) {
49903
+ if (!existsSync17(path2)) {
49064
49904
  return { version: 1, entries: {} };
49065
49905
  }
49066
49906
  let parsed;
@@ -51888,7 +52728,7 @@ async function discoverCodeRoots(homeDir) {
51888
52728
  const candidates = ["src", "code", "projects"].map(
51889
52729
  (name) => join23(homeDir, name)
51890
52730
  );
51891
- return candidates.filter((path2) => existsSync16(path2)).flatMap((path2) => discoveredProjectNames(path2));
52731
+ return candidates.filter((path2) => existsSync17(path2)).flatMap((path2) => discoveredProjectNames(path2));
51892
52732
  }
51893
52733
  function discoveredProjectNames(root) {
51894
52734
  try {
@@ -51992,25 +52832,25 @@ function looksLikeProject(path2) {
51992
52832
  "pnpm-lock.yaml",
51993
52833
  "yarn.lock",
51994
52834
  "package-lock.json"
51995
- ].some((name) => existsSync16(join23(path2, name)));
52835
+ ].some((name) => existsSync17(join23(path2, name)));
51996
52836
  }
51997
52837
  function detectProjectLanguage(path2) {
51998
- if (existsSync16(join23(path2, "pyproject.toml")) || existsSync16(join23(path2, "requirements.txt"))) {
52838
+ if (existsSync17(join23(path2, "pyproject.toml")) || existsSync17(join23(path2, "requirements.txt"))) {
51999
52839
  return "Python";
52000
52840
  }
52001
- if (existsSync16(join23(path2, "Cargo.toml"))) {
52841
+ if (existsSync17(join23(path2, "Cargo.toml"))) {
52002
52842
  return "Rust";
52003
52843
  }
52004
- if (existsSync16(join23(path2, "go.mod"))) {
52844
+ if (existsSync17(join23(path2, "go.mod"))) {
52005
52845
  return "Go";
52006
52846
  }
52007
- if (existsSync16(join23(path2, "mix.exs"))) {
52847
+ if (existsSync17(join23(path2, "mix.exs"))) {
52008
52848
  return "Elixir";
52009
52849
  }
52010
- if (existsSync16(join23(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
52850
+ if (existsSync17(join23(path2, "tsconfig.json")) || packageJsonMentionsTypeScript(path2)) {
52011
52851
  return "TypeScript";
52012
52852
  }
52013
- if (existsSync16(join23(path2, "package.json"))) {
52853
+ if (existsSync17(join23(path2, "package.json"))) {
52014
52854
  return "JavaScript";
52015
52855
  }
52016
52856
  return "Project";
@@ -52026,7 +52866,7 @@ function packageJsonMentionsTypeScript(path2) {
52026
52866
  }
52027
52867
  }
52028
52868
  function hasGitMetadata(path2) {
52029
- return existsSync16(join23(path2, ".git"));
52869
+ return existsSync17(join23(path2, ".git"));
52030
52870
  }
52031
52871
  function childDirectories(root) {
52032
52872
  try {
@@ -52147,9 +52987,10 @@ secure mission control for all your agents on your computers
52147
52987
  // src/index.ts
52148
52988
  init_onboardingDiscoveryChildProcess();
52149
52989
  init_runner();
52990
+ init_runnerLockTakeover();
52150
52991
  init_claudeCodeSession();
52151
52992
  init_authCache();
52152
- import { existsSync as existsSync17, readFileSync as readFileSync22, realpathSync as realpathSync7 } from "node:fs";
52993
+ import { existsSync as existsSync18, readFileSync as readFileSync22, realpathSync as realpathSync7 } from "node:fs";
52153
52994
  import { homedir as homedir17 } from "node:os";
52154
52995
  import { resolve as resolve11 } from "node:path";
52155
52996
  import { fileURLToPath as fileURLToPath4 } from "node:url";
@@ -52245,7 +53086,7 @@ init_kandanTls();
52245
53086
  init_protocol();
52246
53087
  init_json();
52247
53088
  init_defaultUrls();
52248
- import { existsSync as existsSync14, mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "node:fs";
53089
+ import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "node:fs";
52249
53090
  import { dirname as dirname14, join as join21 } from "node:path";
52250
53091
  import { homedir as homedir14 } from "node:os";
52251
53092
  async function runAgentCliCommand(args, deps = {
@@ -52964,7 +53805,7 @@ function authorizationHeaders(token) {
52964
53805
  return { authorization: `Bearer ${token}` };
52965
53806
  }
52966
53807
  function readOptionalTextFile(path2) {
52967
- return existsSync14(path2) ? readFileSync18(path2, "utf8") : void 0;
53808
+ return existsSync15(path2) ? readFileSync18(path2, "utf8") : void 0;
52968
53809
  }
52969
53810
  function writeTextFile(path2, content) {
52970
53811
  mkdirSync13(dirname14(path2), { recursive: true });
@@ -53055,7 +53896,7 @@ init_helloLinzumiProject();
53055
53896
  // src/commanderDaemon.ts
53056
53897
  init_runnerLogger();
53057
53898
  import {
53058
- existsSync as existsSync15,
53899
+ existsSync as existsSync16,
53059
53900
  closeSync as closeSync3,
53060
53901
  mkdirSync as mkdirSync14,
53061
53902
  openSync as openSync4,
@@ -53149,7 +53990,7 @@ function startCommanderDaemon(options) {
53149
53990
  }
53150
53991
  function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), processIdentityReader = readProcessIdentity) {
53151
53992
  const statusFile = commanderStatusFile(runnerId, statusDir);
53152
- if (!existsSync15(statusFile)) {
53993
+ if (!existsSync16(statusFile)) {
53153
53994
  return { status: "missing", runnerId, statusFile };
53154
53995
  }
53155
53996
  const record = parseRecord(readFileSync19(statusFile, "utf8"));
@@ -53157,7 +53998,7 @@ function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), proce
53157
53998
  }
53158
53999
  async function waitForCommanderDaemon(options) {
53159
54000
  const now = options.now ?? (() => Date.now());
53160
- const readTextFile = options.readTextFile ?? ((path2) => existsSync15(path2) ? readFileSync19(path2, "utf8") : void 0);
54001
+ const readTextFile = options.readTextFile ?? ((path2) => existsSync16(path2) ? readFileSync19(path2, "utf8") : void 0);
53161
54002
  const statusImpl = options.statusImpl ?? commanderDaemonStatus;
53162
54003
  const deadline = now() + options.timeoutMs;
53163
54004
  while (now() <= deadline) {
@@ -66372,6 +67213,11 @@ var flagDefinitions = /* @__PURE__ */ new Map([
66372
67213
  ["command", { kind: "value" }],
66373
67214
  ["owner-username", { kind: "value" }],
66374
67215
  ["include-token", { kind: "boolean" }],
67216
+ // Interactive lock-conflict resolution on connect: --take-over stops a live
67217
+ // holder without a prompt (automation); --no-input forces non-interactive so
67218
+ // a TTY never blocks on the takeover prompt (falls back to report + exit 1).
67219
+ ["take-over", { kind: "boolean" }],
67220
+ ["no-input", { kind: "boolean" }],
66375
67221
  ["help", { kind: "boolean" }]
66376
67222
  ]);
66377
67223
  var helloFlagDefinitions = /* @__PURE__ */ new Map([
@@ -66388,8 +67234,11 @@ if (isMainModule()) {
66388
67234
  try {
66389
67235
  await main(process.argv.slice(2));
66390
67236
  } catch (error) {
66391
- process.stderr.write(`${cliErrorMessage(error)}
67237
+ const message = cliErrorMessage(error);
67238
+ if (message !== "") {
67239
+ process.stderr.write(`${message}
66392
67240
  `);
67241
+ }
66393
67242
  process.exit(1);
66394
67243
  }
66395
67244
  }
@@ -66812,6 +67661,9 @@ async function runCommanderDaemonCommand(args) {
66812
67661
  }
66813
67662
  }
66814
67663
  function cliErrorMessage(error) {
67664
+ if (isRunnerLockConflictReportedError(error)) {
67665
+ return "";
67666
+ }
66815
67667
  return signupPromptCancelMessage2(error) ?? userFacingErrorMessage(error, { includeSupportLine: true });
66816
67668
  }
66817
67669
  function signupPromptCancelMessage2(error) {
@@ -67099,7 +67951,7 @@ async function parseAgentRunnerArgs(args, deps = {
67099
67951
  };
67100
67952
  }
67101
67953
  function readAgentTokenTextFile(path2) {
67102
- return existsSync17(path2) ? readFileSync22(path2, "utf8") : void 0;
67954
+ return existsSync18(path2) ? readFileSync22(path2, "utf8") : void 0;
67103
67955
  }
67104
67956
  function rejectAgentRunnerTargetingFlags(values) {
67105
67957
  const unsupportedFlags = [
@@ -67274,7 +68126,11 @@ async function parseRunnerArgs(args, deps = {
67274
68126
  workspaceSlug: workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
67275
68127
  runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
67276
68128
  onboardingDiscovery,
67277
- channelSession: void 0
68129
+ channelSession: void 0,
68130
+ lockTakeover: {
68131
+ takeOverWithoutPrompt: values.get("take-over") === true,
68132
+ forceNonInteractive: values.get("no-input") === true
68133
+ }
67278
68134
  };
67279
68135
  }
67280
68136
  function runnerRuntimeDefaultsFromValues(values) {
@@ -67624,6 +68480,8 @@ Connection:
67624
68480
 
67625
68481
  Workspace:
67626
68482
  --workspace <slug> Workspace slug
68483
+ --take-over If another runner already holds this workspace, stop it and run here (no prompt)
68484
+ --no-input Never prompt; if the workspace is held, print the conflict and exit
67627
68485
 
67628
68486
  Codex:
67629
68487
  --cwd <path> Working directory for Codex, default current directory