@nklisch/pi-enhanced 0.2.6 → 0.2.7

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
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.2.7
4
+
5
+ ### Fixed
6
+
7
+ - Rebundle `@nklisch/pi-plugins` 0.6.2 and `@nklisch/pi-subagents` 18.1.0-nklisch.4 so a blocking subagent result request cannot duplicate its completion notification in context.
8
+
3
9
  ## v0.2.6
4
10
 
5
11
  ### Fixed
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [18.1.0-nklisch.4] - 2026-08-27
9
+
10
+ ### Fixed
11
+
12
+ - Prevent a blocking `get_subagent_result` call from also receiving the same background-agent completion as a delayed follow-up notification.
13
+
8
14
  ## [18.1.0-nklisch.3] - 2026-08-24
9
15
 
10
16
  ### Fixed
@@ -243,7 +243,7 @@ Resume admission is a record-owned lease. A genuinely running or already-reserve
243
243
 
244
244
  Terminal outcomes also carry orthogonal consumption state. Foreground delivery, `get_subagent_result`, and a queued completion notification mark the outcome consumed. Records remain available for the parent session; only their heavy live child sessions are released after the configured consumed or unconsumed retention window. Released records keep their result and persisted transcript pointer but cannot resume. Release detaches the session synchronously before awaiting teardown, so it cannot race a new resume.
245
245
 
246
- Completion notifications are held while the parent agent run is active and flushed on `agent_settled`, rechecking consumption before enqueueing a follow-up. Child session creation shares the parent model runtime and leaves extension-tool registration open; a denylist removes disallowed built-ins and recursive orchestration tools across registry refreshes. Child teardown mirrors Pi's managed root lifecycle: it emits and awaits `session_shutdown` before `AgentSession.dispose()` revokes extension contexts, then publishes the child `disposed` event.
246
+ Completion notifications are held while the parent agent run is active and flushed on `agent_settled`, rechecking consumption before enqueueing a follow-up. A blocking `get_subagent_result` request claims direct delivery before awaiting, so the same terminal outcome cannot also enqueue a completion follow-up. Child session creation shares the parent model runtime and leaves extension-tool registration open; a denylist removes disallowed built-ins and recursive orchestration tools across registry refreshes. Child teardown mirrors Pi's managed root lifecycle: it emits and awaits `session_shutdown` before `AgentSession.dispose()` revokes extension contexts, then publishes the child `disposed` event.
247
247
 
248
248
  ## Execution flow
249
249
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nklisch/pi-subagents",
3
- "version": "18.1.0-nklisch.3",
3
+ "version": "18.1.0-nklisch.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -66,6 +66,12 @@ export class SubagentState {
66
66
  get consumedAt(): number | undefined { return this._consumedAt; }
67
67
  get consumed(): boolean { return this._consumedAt != null; }
68
68
 
69
+ // A blocking result request has claimed the next terminal outcome for direct
70
+ // delivery. Completion notification observes this so it cannot enqueue the
71
+ // same result through Pi's asynchronous follow-up channel.
72
+ private _pendingResultWaits = 0;
73
+ get hasPendingResultWait(): boolean { return this._pendingResultWaits > 0; }
74
+
69
75
  // Stats — accumulated via mutation methods, readable via getters
70
76
  private _toolUses = 0;
71
77
  get toolUses(): number { return this._toolUses; }
@@ -208,6 +214,16 @@ export class SubagentState {
208
214
  this._consumedAt ??= at ?? Date.now();
209
215
  }
210
216
 
217
+ /** Claim the next terminal outcome for a blocking direct-result request. */
218
+ beginResultWait(): void {
219
+ this._pendingResultWaits++;
220
+ }
221
+
222
+ /** Release one direct-result claim after its wait returns or is interrupted. */
223
+ endResultWait(): void {
224
+ this._pendingResultWaits = Math.max(0, this._pendingResultWaits - 1);
225
+ }
226
+
211
227
  /** Transition to stopped state. Always valid — no guard. */
212
228
  markStopped(completedAt?: number): void {
213
229
  this._status = "stopped";
@@ -127,6 +127,8 @@ export class Subagent {
127
127
  get stoppedWhileQueued(): boolean { return this.state.stoppedWhileQueued; }
128
128
  get consumedAt(): number | undefined { return this.state.consumedAt; }
129
129
  get consumed(): boolean { return this.state.consumed; }
130
+ /** A blocking get-result call will deliver the next terminal outcome directly. */
131
+ get hasPendingResultWait(): boolean { return this.state.hasPendingResultWait; }
130
132
  get toolUses(): number { return this.state.toolUses; }
131
133
  get lifetimeUsage(): Readonly<LifetimeUsage> { return this.state.lifetimeUsage; }
132
134
  get compactionCount(): number { return this.state.compactionCount; }
@@ -496,6 +498,21 @@ export class Subagent {
496
498
  await settleOrAbort(run, signal);
497
499
  }
498
500
 
501
+ /**
502
+ * Wait for a terminal result that the caller will return directly to the
503
+ * parent. The claim begins synchronously, before the first await, so a child
504
+ * that settles while the tool is blocked cannot also queue a follow-up nudge.
505
+ */
506
+ async waitForResult(signal: AbortSignal): Promise<void> {
507
+ if (!this.isActive()) return;
508
+ this.state.beginResultWait();
509
+ try {
510
+ await this.waitUntilSettled(signal);
511
+ } finally {
512
+ this.state.endResultWait();
513
+ }
514
+ }
515
+
499
516
  /** Build a callback-only lifecycle bridge after the immutable child session exists. */
500
517
  private createTurnLifecycle(
501
518
  phase: "initial" | "resume",
@@ -157,7 +157,10 @@ export class NotificationManager implements NotificationSystem {
157
157
  ) {}
158
158
 
159
159
  sendCompletion(record: Subagent): void {
160
- if (this.disposed || record.consumed) return;
160
+ // A blocking get-result request has already selected direct delivery for
161
+ // this terminal outcome. Never enqueue the same payload as a follow-up,
162
+ // even if Pi's parent lifecycle currently appears idle.
163
+ if (this.disposed || record.consumed || record.hasPendingResultWait) return;
161
164
  if (this.parentRunActive) {
162
165
  this.pendingNudges.set(record.id, record);
163
166
  return;
@@ -33,8 +33,10 @@ export class GetResultTool {
33
33
  }
34
34
 
35
35
  // A queued record is awaitable from spawn, and a resumed record republishes
36
- // its live promise. Interrupting this tool stops only the wait.
37
- if (params.wait) await record.waitUntilSettled(signal);
36
+ // its live promise. This claim is synchronous so completion cannot race the
37
+ // direct result with an asynchronous follow-up notification. Interrupting
38
+ // this tool stops only the wait.
39
+ if (params.wait) await record.waitForResult(signal);
38
40
 
39
41
  // Pull delivery: only a terminal result was actually collected.
40
42
  if (!record.isActive()) record.markConsumed();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nklisch/pi-plugins",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "private": false,
5
5
  "description": "A small filesystem-first marketplace and plugin host for Pi.",
6
6
  "license": "MIT",
@@ -63,7 +63,7 @@
63
63
  ],
64
64
  "dependencies": {
65
65
  "@nklisch/pi-mcp-adapter": "2.21.0-nklisch.2",
66
- "@nklisch/pi-subagents": "18.1.0-nklisch.3",
66
+ "@nklisch/pi-subagents": "18.1.0-nklisch.4",
67
67
  "jiti": "2.7.0"
68
68
  },
69
69
  "devDependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nklisch/pi-enhanced",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Pi, enhanced — one install for nklisch's full harness: policy-gated command review, plugin marketplace, subagents, background tasks, research tools, search, model modes, and a curated UX set.",
5
5
  "author": {
6
6
  "name": "nklisch"