@mjasnikovs/pi-task 0.21.8 → 0.21.10

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.
@@ -340,10 +340,29 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
340
340
  streamWatch?.note();
341
341
  stderr += d.toString();
342
342
  });
343
- proc.on('close', (code) => {
343
+ // One idempotent settle path for close/error/abort. Detaching the abort
344
+ // listener here is the point: `{once: true}` only fires-and-removes on an
345
+ // ACTUAL abort, so a child that finishes normally used to leave its
346
+ // listener on the signal forever. A TaskRunner shares ONE AbortController
347
+ // across every child of a run, so those listeners accumulated linearly and
348
+ // each one retained, via `killProc`'s closure, the finished child process,
349
+ // this invocation (including its full prompt), and `opts` — together with
350
+ // everything the caller's callbacks close over. See GitHub issue #9.
351
+ let settled = false;
352
+ const cleanup = () => {
344
353
  if (stallTimer)
345
354
  clearInterval(stallTimer);
346
355
  streamWatch?.stop();
356
+ signal?.removeEventListener('abort', killProc);
357
+ };
358
+ const settle = (result) => {
359
+ cleanup();
360
+ if (settled)
361
+ return;
362
+ settled = true;
363
+ resolve(result);
364
+ };
365
+ proc.once('close', (code) => {
347
366
  // The child has exited, but anything it backgrounded (a dev server) may
348
367
  // still hold its process group and a port — reap the group so the next
349
368
  // gate's boot check does not collide with our own orphan. Best-effort:
@@ -355,7 +374,7 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
355
374
  if (sink)
356
375
  sink.flush();
357
376
  const text = sink ? sink.text : undefined;
358
- resolve({
377
+ settle({
359
378
  stdout,
360
379
  stderr,
361
380
  exitCode: code ?? 0,
@@ -368,11 +387,8 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
368
387
  : {})
369
388
  });
370
389
  });
371
- proc.on('error', () => {
372
- if (stallTimer)
373
- clearInterval(stallTimer);
374
- streamWatch?.stop();
375
- resolve({ stdout, stderr, exitCode: 1, aborted });
390
+ proc.once('error', () => {
391
+ settle({ stdout, stderr, exitCode: 1, aborted });
376
392
  });
377
393
  if (signal) {
378
394
  if (signal.aborted)
@@ -37,12 +37,31 @@ function runGitLsFiles(cwd, signal) {
37
37
  proc.stdout?.on('data', (d) => {
38
38
  stdout += d.toString();
39
39
  });
40
- proc.on('error', () => resolve(''));
41
- proc.on('close', code => resolve(code === 0 ? stdout : ''));
42
- signal?.addEventListener('abort', () => {
40
+ // Same detach discipline as runChild (GitHub issue #9): `signal` is the
41
+ // run-long orchestrator signal, so a listener left behind by a normally
42
+ // finished `git ls-files` would retain that child for the whole run.
43
+ const onAbort = () => {
43
44
  if (!proc.killed)
44
45
  proc.kill('SIGTERM');
45
- });
46
+ };
47
+ let settled = false;
48
+ const settle = (value) => {
49
+ signal?.removeEventListener('abort', onAbort);
50
+ if (settled)
51
+ return;
52
+ settled = true;
53
+ resolve(value);
54
+ };
55
+ proc.once('error', () => settle(''));
56
+ proc.once('close', code => settle(code === 0 ? stdout : ''));
57
+ if (signal) {
58
+ // An already-aborted signal never emits 'abort', so without this check
59
+ // a run cancelled before this point would let the child run to term.
60
+ if (signal.aborted)
61
+ onAbort();
62
+ else
63
+ signal.addEventListener('abort', onAbort, { once: true });
64
+ }
46
65
  });
47
66
  }
48
67
  /** Cap output to maxLines real (non-blank) paths; tag truncation when cut. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.21.8",
3
+ "version": "0.21.10",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",