@nathapp/nax 0.42.5 → 0.42.6

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.
package/dist/nax.js CHANGED
@@ -18963,6 +18963,8 @@ class SpawnAcpSession {
18963
18963
  timeoutSeconds;
18964
18964
  permissionMode;
18965
18965
  env;
18966
+ pidRegistry;
18967
+ activeProc = null;
18966
18968
  constructor(opts) {
18967
18969
  this.agentName = opts.agentName;
18968
18970
  this.sessionName = opts.sessionName;
@@ -18971,6 +18973,7 @@ class SpawnAcpSession {
18971
18973
  this.timeoutSeconds = opts.timeoutSeconds;
18972
18974
  this.permissionMode = opts.permissionMode;
18973
18975
  this.env = opts.env;
18976
+ this.pidRegistry = opts.pidRegistry;
18974
18977
  }
18975
18978
  async prompt(text) {
18976
18979
  const cmd = [
@@ -18997,35 +19000,50 @@ class SpawnAcpSession {
18997
19000
  stderr: "pipe",
18998
19001
  env: this.env
18999
19002
  });
19000
- proc.stdin.write(text);
19001
- proc.stdin.end();
19002
- const exitCode = await proc.exited;
19003
- const stdout = await new Response(proc.stdout).text();
19004
- const stderr = await new Response(proc.stderr).text();
19005
- if (exitCode !== 0) {
19006
- getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
19007
- stderr: stderr.slice(0, 200)
19008
- });
19009
- return {
19010
- messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
19011
- stopReason: "error"
19012
- };
19013
- }
19003
+ this.activeProc = proc;
19004
+ const processPid = proc.pid;
19005
+ await this.pidRegistry?.register(processPid);
19014
19006
  try {
19015
- const parsed = parseAcpxJsonOutput(stdout);
19016
- return {
19017
- messages: [{ role: "assistant", content: parsed.text || "" }],
19018
- stopReason: "end_turn",
19019
- cumulative_token_usage: parsed.tokenUsage
19020
- };
19021
- } catch (err) {
19022
- getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
19023
- stderr: stderr.slice(0, 200)
19024
- });
19025
- throw err;
19007
+ proc.stdin.write(text);
19008
+ proc.stdin.end();
19009
+ const exitCode = await proc.exited;
19010
+ const stdout = await new Response(proc.stdout).text();
19011
+ const stderr = await new Response(proc.stderr).text();
19012
+ if (exitCode !== 0) {
19013
+ getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
19014
+ stderr: stderr.slice(0, 200)
19015
+ });
19016
+ return {
19017
+ messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
19018
+ stopReason: "error"
19019
+ };
19020
+ }
19021
+ try {
19022
+ const parsed = parseAcpxJsonOutput(stdout);
19023
+ return {
19024
+ messages: [{ role: "assistant", content: parsed.text || "" }],
19025
+ stopReason: "end_turn",
19026
+ cumulative_token_usage: parsed.tokenUsage
19027
+ };
19028
+ } catch (err) {
19029
+ getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
19030
+ stderr: stderr.slice(0, 200)
19031
+ });
19032
+ throw err;
19033
+ }
19034
+ } finally {
19035
+ this.activeProc = null;
19036
+ await this.pidRegistry?.unregister(processPid);
19026
19037
  }
19027
19038
  }
19028
19039
  async close() {
19040
+ if (this.activeProc) {
19041
+ try {
19042
+ this.activeProc.kill(15);
19043
+ getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
19044
+ } catch {}
19045
+ this.activeProc = null;
19046
+ }
19029
19047
  const cmd = ["acpx", this.agentName, "sessions", "close", this.sessionName];
19030
19048
  getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
19031
19049
  const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
@@ -19039,6 +19057,12 @@ class SpawnAcpSession {
19039
19057
  }
19040
19058
  }
19041
19059
  async cancelActivePrompt() {
19060
+ if (this.activeProc) {
19061
+ try {
19062
+ this.activeProc.kill(15);
19063
+ getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
19064
+ } catch {}
19065
+ }
19042
19066
  const cmd = ["acpx", this.agentName, "cancel"];
19043
19067
  getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
19044
19068
  const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
@@ -19052,7 +19076,8 @@ class SpawnAcpClient {
19052
19076
  cwd;
19053
19077
  timeoutSeconds;
19054
19078
  env;
19055
- constructor(cmdStr, cwd, timeoutSeconds) {
19079
+ pidRegistry;
19080
+ constructor(cmdStr, cwd, timeoutSeconds, pidRegistry) {
19056
19081
  const parts = cmdStr.split(/\s+/);
19057
19082
  const modelIdx = parts.indexOf("--model");
19058
19083
  this.model = modelIdx >= 0 && parts[modelIdx + 1] ? parts[modelIdx + 1] : "default";
@@ -19064,6 +19089,7 @@ class SpawnAcpClient {
19064
19089
  this.cwd = cwd || process.cwd();
19065
19090
  this.timeoutSeconds = timeoutSeconds || 1800;
19066
19091
  this.env = buildAllowedEnv2();
19092
+ this.pidRegistry = pidRegistry;
19067
19093
  }
19068
19094
  async start() {}
19069
19095
  async createSession(opts) {
@@ -19083,7 +19109,8 @@ class SpawnAcpClient {
19083
19109
  model: this.model,
19084
19110
  timeoutSeconds: this.timeoutSeconds,
19085
19111
  permissionMode: opts.permissionMode,
19086
- env: this.env
19112
+ env: this.env,
19113
+ pidRegistry: this.pidRegistry
19087
19114
  });
19088
19115
  }
19089
19116
  async loadSession(sessionName, agentName) {
@@ -19100,13 +19127,14 @@ class SpawnAcpClient {
19100
19127
  model: this.model,
19101
19128
  timeoutSeconds: this.timeoutSeconds,
19102
19129
  permissionMode: "approve-all",
19103
- env: this.env
19130
+ env: this.env,
19131
+ pidRegistry: this.pidRegistry
19104
19132
  });
19105
19133
  }
19106
19134
  async close() {}
19107
19135
  }
19108
- function createSpawnAcpClient(cmdStr, cwd, timeoutSeconds) {
19109
- return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds);
19136
+ function createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry) {
19137
+ return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
19110
19138
  }
19111
19139
  var _spawnClientDeps;
19112
19140
  var init_spawn_client = __esm(() => {
@@ -19367,7 +19395,7 @@ class AcpAgentAdapter {
19367
19395
  }
19368
19396
  async _runWithClient(options, startTime) {
19369
19397
  const cmdStr = `acpx --model ${options.modelDef.model} ${this.name}`;
19370
- const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds);
19398
+ const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds, options.pidRegistry);
19371
19399
  await client.start();
19372
19400
  let sessionName = options.acpSessionName;
19373
19401
  if (!sessionName && options.featureName && options.storyId) {
@@ -19539,7 +19567,8 @@ class AcpAgentAdapter {
19539
19567
  maxInteractionTurns: options.maxInteractionTurns,
19540
19568
  featureName: options.featureName,
19541
19569
  storyId: options.storyId,
19542
- sessionRole: options.sessionRole
19570
+ sessionRole: options.sessionRole,
19571
+ pidRegistry: options.pidRegistry
19543
19572
  });
19544
19573
  if (!result.success) {
19545
19574
  throw new Error(`[acp-adapter] plan() failed: ${result.output}`);
@@ -19609,8 +19638,8 @@ var init_adapter = __esm(() => {
19609
19638
  async sleep(ms) {
19610
19639
  await Bun.sleep(ms);
19611
19640
  },
19612
- createClient(cmdStr, cwd, timeoutSeconds) {
19613
- return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds);
19641
+ createClient(cmdStr, cwd, timeoutSeconds, pidRegistry) {
19642
+ return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
19614
19643
  }
19615
19644
  };
19616
19645
  });
@@ -21870,7 +21899,7 @@ var package_default;
21870
21899
  var init_package = __esm(() => {
21871
21900
  package_default = {
21872
21901
  name: "@nathapp/nax",
21873
- version: "0.42.5",
21902
+ version: "0.42.6",
21874
21903
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
21875
21904
  type: "module",
21876
21905
  bin: {
@@ -21943,8 +21972,8 @@ var init_version = __esm(() => {
21943
21972
  NAX_VERSION = package_default.version;
21944
21973
  NAX_COMMIT = (() => {
21945
21974
  try {
21946
- if (/^[0-9a-f]{6,10}$/.test("7b603fa"))
21947
- return "7b603fa";
21975
+ if (/^[0-9a-f]{6,10}$/.test("deb8333"))
21976
+ return "deb8333";
21948
21977
  } catch {}
21949
21978
  try {
21950
21979
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -65602,6 +65631,7 @@ init_registry();
65602
65631
  import { existsSync as existsSync9 } from "fs";
65603
65632
  import { join as join10 } from "path";
65604
65633
  import { createInterface } from "readline";
65634
+ init_pid_registry();
65605
65635
  init_logger2();
65606
65636
 
65607
65637
  // src/prd/schema.ts
@@ -65805,6 +65835,7 @@ async function planCommand(workdir, config2, options) {
65805
65835
  if (!adapter)
65806
65836
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
65807
65837
  const interactionBridge = createCliInteractionBridge();
65838
+ const pidRegistry = new PidRegistry(workdir);
65808
65839
  logger?.info("plan", "Starting interactive planning session...", { agent: agentName });
65809
65840
  try {
65810
65841
  await adapter.plan({
@@ -65817,9 +65848,11 @@ async function planCommand(workdir, config2, options) {
65817
65848
  modelTier: config2?.plan?.model ?? "balanced",
65818
65849
  dangerouslySkipPermissions: config2?.execution?.dangerouslySkipPermissions ?? false,
65819
65850
  maxInteractionTurns: config2?.agent?.maxInteractionTurns,
65820
- featureName: options.feature
65851
+ featureName: options.feature,
65852
+ pidRegistry
65821
65853
  });
65822
65854
  } finally {
65855
+ await pidRegistry.killAll().catch(() => {});
65823
65856
  logger?.info("plan", "Interactive session ended");
65824
65857
  }
65825
65858
  if (!_deps2.existsSync(outputPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.42.5",
3
+ "version": "0.42.6",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -114,8 +114,13 @@ export const _acpAdapterDeps = {
114
114
  * Default: spawn-based client (shells out to acpx CLI).
115
115
  * Override in tests via: _acpAdapterDeps.createClient = mock(...)
116
116
  */
117
- createClient(cmdStr: string, cwd?: string, timeoutSeconds?: number): AcpClient {
118
- return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds);
117
+ createClient(
118
+ cmdStr: string,
119
+ cwd?: string,
120
+ timeoutSeconds?: number,
121
+ pidRegistry?: import("../../execution/pid-registry").PidRegistry,
122
+ ): AcpClient {
123
+ return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
119
124
  },
120
125
  };
121
126
 
@@ -436,7 +441,7 @@ export class AcpAgentAdapter implements AgentAdapter {
436
441
 
437
442
  private async _runWithClient(options: AgentRunOptions, startTime: number): Promise<AgentResult> {
438
443
  const cmdStr = `acpx --model ${options.modelDef.model} ${this.name}`;
439
- const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds);
444
+ const client = _acpAdapterDeps.createClient(cmdStr, options.workdir, options.timeoutSeconds, options.pidRegistry);
440
445
  await client.start();
441
446
 
442
447
  // 1. Resolve session name: explicit > sidecar > derived
@@ -673,6 +678,7 @@ export class AcpAgentAdapter implements AgentAdapter {
673
678
  featureName: options.featureName,
674
679
  storyId: options.storyId,
675
680
  sessionRole: options.sessionRole,
681
+ pidRegistry: options.pidRegistry,
676
682
  });
677
683
 
678
684
  if (!result.success) {
@@ -12,6 +12,7 @@
12
12
  * acpx <agent> cancel → session.cancelActivePrompt()
13
13
  */
14
14
 
15
+ import type { PidRegistry } from "../../execution/pid-registry";
15
16
  import { getSafeLogger } from "../../logger";
16
17
  import type { AcpClient, AcpSession, AcpSessionResponse } from "./adapter";
17
18
  import { parseAcpxJsonOutput } from "./parser";
@@ -96,6 +97,8 @@ class SpawnAcpSession implements AcpSession {
96
97
  private readonly timeoutSeconds: number;
97
98
  private readonly permissionMode: string;
98
99
  private readonly env: Record<string, string | undefined>;
100
+ private readonly pidRegistry?: PidRegistry;
101
+ private activeProc: { pid: number; kill(signal?: number): void } | null = null;
99
102
 
100
103
  constructor(opts: {
101
104
  agentName: string;
@@ -105,6 +108,7 @@ class SpawnAcpSession implements AcpSession {
105
108
  timeoutSeconds: number;
106
109
  permissionMode: string;
107
110
  env: Record<string, string | undefined>;
111
+ pidRegistry?: PidRegistry;
108
112
  }) {
109
113
  this.agentName = opts.agentName;
110
114
  this.sessionName = opts.sessionName;
@@ -113,6 +117,7 @@ class SpawnAcpSession implements AcpSession {
113
117
  this.timeoutSeconds = opts.timeoutSeconds;
114
118
  this.permissionMode = opts.permissionMode;
115
119
  this.env = opts.env;
120
+ this.pidRegistry = opts.pidRegistry;
116
121
  }
117
122
 
118
123
  async prompt(text: string): Promise<AcpSessionResponse> {
@@ -143,40 +148,60 @@ class SpawnAcpSession implements AcpSession {
143
148
  env: this.env,
144
149
  });
145
150
 
146
- proc.stdin.write(text);
147
- proc.stdin.end();
151
+ this.activeProc = proc;
152
+ const processPid = proc.pid;
153
+ await this.pidRegistry?.register(processPid);
148
154
 
149
- const exitCode = await proc.exited;
150
- const stdout = await new Response(proc.stdout).text();
151
- const stderr = await new Response(proc.stderr).text();
155
+ try {
156
+ proc.stdin.write(text);
157
+ proc.stdin.end();
152
158
 
153
- if (exitCode !== 0) {
154
- getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
155
- stderr: stderr.slice(0, 200),
156
- });
157
- // Return error response so the adapter can handle it
158
- return {
159
- messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
160
- stopReason: "error",
161
- };
162
- }
159
+ const exitCode = await proc.exited;
160
+ const stdout = await new Response(proc.stdout).text();
161
+ const stderr = await new Response(proc.stderr).text();
163
162
 
164
- try {
165
- const parsed = parseAcpxJsonOutput(stdout);
166
- return {
167
- messages: [{ role: "assistant", content: parsed.text || "" }],
168
- stopReason: "end_turn",
169
- cumulative_token_usage: parsed.tokenUsage,
170
- };
171
- } catch (err) {
172
- getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
173
- stderr: stderr.slice(0, 200),
174
- });
175
- throw err;
163
+ if (exitCode !== 0) {
164
+ getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
165
+ stderr: stderr.slice(0, 200),
166
+ });
167
+ // Return error response so the adapter can handle it
168
+ return {
169
+ messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
170
+ stopReason: "error",
171
+ };
172
+ }
173
+
174
+ try {
175
+ const parsed = parseAcpxJsonOutput(stdout);
176
+ return {
177
+ messages: [{ role: "assistant", content: parsed.text || "" }],
178
+ stopReason: "end_turn",
179
+ cumulative_token_usage: parsed.tokenUsage,
180
+ };
181
+ } catch (err) {
182
+ getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
183
+ stderr: stderr.slice(0, 200),
184
+ });
185
+ throw err;
186
+ }
187
+ } finally {
188
+ this.activeProc = null;
189
+ await this.pidRegistry?.unregister(processPid);
176
190
  }
177
191
  }
178
192
 
179
193
  async close(): Promise<void> {
194
+ // Kill in-flight prompt process first (if any)
195
+ if (this.activeProc) {
196
+ try {
197
+ this.activeProc.kill(15); // SIGTERM
198
+ getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
199
+ } catch {
200
+ // Process may have already exited
201
+ }
202
+ this.activeProc = null;
203
+ }
204
+
180
205
  const cmd = ["acpx", this.agentName, "sessions", "close", this.sessionName];
181
206
  getSafeLogger()?.debug("acp-adapter", `Closing session: ${this.sessionName}`);
182
207
 
@@ -193,6 +218,16 @@ class SpawnAcpSession implements AcpSession {
193
218
  }
194
219
 
195
220
  async cancelActivePrompt(): Promise<void> {
221
+ // Kill in-flight prompt process directly (faster than acpx cancel)
222
+ if (this.activeProc) {
223
+ try {
224
+ this.activeProc.kill(15); // SIGTERM
225
+ getSafeLogger()?.debug("acp-adapter", `Killed active prompt process PID ${this.activeProc.pid}`);
226
+ } catch {
227
+ // Process may have already exited
228
+ }
229
+ }
230
+
196
231
  const cmd = ["acpx", this.agentName, "cancel"];
197
232
  getSafeLogger()?.debug("acp-adapter", `Cancelling active prompt: ${this.sessionName}`);
198
233
 
@@ -220,8 +255,9 @@ export class SpawnAcpClient implements AcpClient {
220
255
  private readonly cwd: string;
221
256
  private readonly timeoutSeconds: number;
222
257
  private readonly env: Record<string, string | undefined>;
258
+ private readonly pidRegistry?: PidRegistry;
223
259
 
224
- constructor(cmdStr: string, cwd?: string, timeoutSeconds?: number) {
260
+ constructor(cmdStr: string, cwd?: string, timeoutSeconds?: number, pidRegistry?: PidRegistry) {
225
261
  // Parse: "acpx --model <model> <agentName>"
226
262
  const parts = cmdStr.split(/\s+/);
227
263
  const modelIdx = parts.indexOf("--model");
@@ -235,6 +271,7 @@ export class SpawnAcpClient implements AcpClient {
235
271
  this.cwd = cwd || process.cwd();
236
272
  this.timeoutSeconds = timeoutSeconds || 1800;
237
273
  this.env = buildAllowedEnv();
274
+ this.pidRegistry = pidRegistry;
238
275
  }
239
276
 
240
277
  async start(): Promise<void> {
@@ -268,6 +305,7 @@ export class SpawnAcpClient implements AcpClient {
268
305
  timeoutSeconds: this.timeoutSeconds,
269
306
  permissionMode: opts.permissionMode,
270
307
  env: this.env,
308
+ pidRegistry: this.pidRegistry,
271
309
  });
272
310
  }
273
311
 
@@ -290,6 +328,7 @@ export class SpawnAcpClient implements AcpClient {
290
328
  timeoutSeconds: this.timeoutSeconds,
291
329
  permissionMode: "approve-all", // Default for resumed sessions
292
330
  env: this.env,
331
+ pidRegistry: this.pidRegistry,
293
332
  });
294
333
  }
295
334
 
@@ -306,6 +345,11 @@ export class SpawnAcpClient implements AcpClient {
306
345
  * Create a spawn-based ACP client. This is the default production factory.
307
346
  * The cmdStr format is: "acpx --model <model> <agentName>"
308
347
  */
309
- export function createSpawnAcpClient(cmdStr: string, cwd?: string, timeoutSeconds?: number): AcpClient {
310
- return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds);
348
+ export function createSpawnAcpClient(
349
+ cmdStr: string,
350
+ cwd?: string,
351
+ timeoutSeconds?: number,
352
+ pidRegistry?: PidRegistry,
353
+ ): AcpClient {
354
+ return new SpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
311
355
  }
@@ -55,6 +55,8 @@ export interface PlanOptions {
55
55
  * Used to persist the name to status.json for plan→run session continuity.
56
56
  */
57
57
  onAcpSessionCreated?: (sessionName: string) => Promise<void> | void;
58
+ /** PID registry for tracking spawned agent processes — cleanup on crash/SIGTERM */
59
+ pidRegistry?: import("../execution/pid-registry").PidRegistry;
58
60
  }
59
61
 
60
62
  /**
package/src/cli/plan.ts CHANGED
@@ -15,6 +15,7 @@ import type { AgentAdapter } from "../agents/types";
15
15
  import { scanCodebase } from "../analyze/scanner";
16
16
  import type { CodebaseScan } from "../analyze/types";
17
17
  import type { NaxConfig } from "../config";
18
+ import { PidRegistry } from "../execution/pid-registry";
18
19
  import { getLogger } from "../logger";
19
20
  import { validatePlanOutput } from "../prd/schema";
20
21
 
@@ -123,6 +124,7 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
123
124
  const adapter = _deps.getAgent(agentName, config);
124
125
  if (!adapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
125
126
  const interactionBridge = createCliInteractionBridge();
127
+ const pidRegistry = new PidRegistry(workdir);
126
128
  logger?.info("plan", "Starting interactive planning session...", { agent: agentName });
127
129
  try {
128
130
  await adapter.plan({
@@ -136,8 +138,10 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
136
138
  dangerouslySkipPermissions: config?.execution?.dangerouslySkipPermissions ?? false,
137
139
  maxInteractionTurns: config?.agent?.maxInteractionTurns,
138
140
  featureName: options.feature,
141
+ pidRegistry,
139
142
  });
140
143
  } finally {
144
+ await pidRegistry.killAll().catch(() => {});
141
145
  logger?.info("plan", "Interactive session ended");
142
146
  }
143
147
  // Read back from file written by agent