@oh-my-pi/pi-agent-core 18.2.10 → 18.2.11

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.11] - 2026-09-23
6
+
7
+ ### Fixed
8
+
9
+ - Fixed background job completions interrupting foreground Bash and eval calls, which could cause those calls to be repeatedly moved into the background.
10
+
5
11
  ## [18.2.9] - 2026-09-22
6
12
 
7
13
  ### Fixed
@@ -243,7 +243,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
243
243
  * Same rules as {@link hasIrcInterrupts}: non-consuming, only cuts
244
244
  * *interruptible* waits short, ignored when interruptMode is "wait". Without
245
245
  * it a completion notice sits behind an hour-long `hub wait` that the agent
246
- * would have abandoned had it seen the notice.
246
+ * would have abandoned had it seen the notice. Unlike a peer IRC it never
247
+ * raises {@link ToolCallContext.steeringSignal}: a queued completion must
248
+ * not push ordinary foreground work (auto-background bash/eval) into the
249
+ * background.
247
250
  */
248
251
  hasBackgroundCompletions?: () => boolean | Promise<boolean>;
249
252
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-agent-core",
4
- "version": "18.2.10",
4
+ "version": "18.2.11",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -35,16 +35,16 @@
35
35
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "18.2.10",
39
- "@oh-my-pi/pi-catalog": "18.2.10",
40
- "@oh-my-pi/pi-natives": "18.2.10",
41
- "@oh-my-pi/pi-utils": "18.2.10",
42
- "@oh-my-pi/pi-wire": "18.2.10",
43
- "@oh-my-pi/snapcompact": "18.2.10",
38
+ "@oh-my-pi/pi-ai": "18.2.11",
39
+ "@oh-my-pi/pi-catalog": "18.2.11",
40
+ "@oh-my-pi/pi-natives": "18.2.11",
41
+ "@oh-my-pi/pi-utils": "18.2.11",
42
+ "@oh-my-pi/pi-wire": "18.2.11",
43
+ "@oh-my-pi/snapcompact": "18.2.11",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@oh-my-pi/omptype": "18.2.10",
47
+ "@oh-my-pi/omptype": "18.2.11",
48
48
  "@opentelemetry/context-async-hooks": "^2.9.0",
49
49
  "@opentelemetry/sdk-trace-base": "^2.9.0",
50
50
  "@types/bun": "^1.3.14"
package/src/agent-loop.ts CHANGED
@@ -2938,19 +2938,32 @@ async function executeToolCalls(
2938
2938
  const checkAsideInterrupts = async (): Promise<void> => {
2939
2939
  // Asides only fire once: an interrupt already recorded on interruptState
2940
2940
  // must not re-abort, and (unlike steering) never re-consumes a queue.
2941
- if (!shouldInterruptImmediately || signal?.aborted || interruptState.triggered) return;
2941
+ // A completion-triggered record is the exception — it leaves the
2942
+ // cooperative signal down, so keep polling until a peer IRC escalates.
2943
+ if (!shouldInterruptImmediately || signal?.aborted) return;
2944
+ if (interruptState.triggered && steeringSoftController.signal.aborted) return;
2942
2945
  // Peer IRC and background completions (finished jobs, exited supervised
2943
2946
  // processes) hard-abort interruptible waits only; foreground tools keep
2944
- // running (no partial side effects) but get the cooperative soft signal
2945
- // so backgroundable work can step aside for the queued notice.
2947
+ // running (no partial side effects).
2946
2948
  let source: AsideInterruptSource | undefined;
2947
2949
  if (hasIrcInterrupts && (await hasIrcInterrupts())) source = "irc";
2948
- else if (hasBackgroundCompletions && (await hasBackgroundCompletions())) source = "background";
2949
- if (!source || interruptState.triggered) return;
2950
- interruptState.triggered = true;
2951
- interruptState.source = source;
2952
- ircAbortController.abort();
2953
- steeringSoftController.abort();
2950
+ else if (!interruptState.triggered && hasBackgroundCompletions && (await hasBackgroundCompletions()))
2951
+ source = "background";
2952
+ if (!source) return;
2953
+ if (!interruptState.triggered) {
2954
+ interruptState.triggered = true;
2955
+ interruptState.source = source;
2956
+ ircAbortController.abort();
2957
+ }
2958
+ // Only an urgent aside raises the cooperative signal that makes
2959
+ // backgroundable foreground work (auto-background bash/eval) detach
2960
+ // itself. A peer waiting on an IRC is blocked on this batch; a finished
2961
+ // background job is not — its notice is an aside that injects at the
2962
+ // batch boundary either way. Detaching ordinary foreground work for it
2963
+ // also cascades: the freshly detached job's own completion re-triggers
2964
+ // this check for the next command, so millisecond-long commands chain
2965
+ // into separate background deliveries (#12869).
2966
+ if (source !== "background") steeringSoftController.abort();
2954
2967
  };
2955
2968
 
2956
2969
  const checkSteering = async (): Promise<void> => {
@@ -3296,10 +3309,11 @@ async function executeToolCalls(
3296
3309
 
3297
3310
  // While tool calls are in flight, queued steering or interrupting IRC would
3298
3311
  // otherwise wait out the tools' own window. Poll only non-consuming queues:
3299
- // detection hard-aborts interruptible waits (running or not yet started)
3300
- // and soft-signals cooperative tools (auto-background bash), so the boundary
3301
- // dequeue below injects the message promptly. Gated on immediate-interrupt
3302
- // mode; checkSteering is idempotent (no-op once triggered).
3312
+ // detection hard-aborts interruptible waits (running or not yet started),
3313
+ // and steering/IRC additionally soft-signal cooperative tools
3314
+ // (auto-background bash), so the boundary dequeue below injects the message
3315
+ // promptly. Gated on immediate-interrupt mode; checkSteering is idempotent
3316
+ // (no-op once triggered).
3303
3317
  const hasAsidePeek = hasIrcInterrupts !== undefined || hasBackgroundCompletions !== undefined;
3304
3318
  const watchSteeringWhileRunning = shouldInterruptImmediately && (hasSteeringMessages !== undefined || hasAsidePeek);
3305
3319
  const eventDrivenSteeringWatch =
@@ -3335,7 +3349,11 @@ async function executeToolCalls(
3335
3349
  () => false,
3336
3350
  );
3337
3351
  if (!(await Promise.race([steeringChecked, watchAbortedFalse]))) return;
3338
- if (steeringWatchSignal.aborted || interruptState.triggered) return;
3352
+ // Stop once the cooperative signal is up — steering and IRC
3353
+ // have nothing left to escalate. A completion-only trigger
3354
+ // leaves it down, so keep watching: a genuine steer arriving
3355
+ // afterwards must still reach foreground tools.
3356
+ if (steeringWatchSignal.aborted || steeringSoftController.signal.aborted) return;
3339
3357
  if (!(await Promise.race([steeringQueued, watchAbortedFalse]))) return;
3340
3358
  }
3341
3359
  })()
package/src/types.ts CHANGED
@@ -300,7 +300,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
300
300
  * Same rules as {@link hasIrcInterrupts}: non-consuming, only cuts
301
301
  * *interruptible* waits short, ignored when interruptMode is "wait". Without
302
302
  * it a completion notice sits behind an hour-long `hub wait` that the agent
303
- * would have abandoned had it seen the notice.
303
+ * would have abandoned had it seen the notice. Unlike a peer IRC it never
304
+ * raises {@link ToolCallContext.steeringSignal}: a queued completion must
305
+ * not push ordinary foreground work (auto-background bash/eval) into the
306
+ * background.
304
307
  */
305
308
  hasBackgroundCompletions?: () => boolean | Promise<boolean>;
306
309