@ferris1225/pi-subagents 4.1.8 → 4.1.9

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.
@@ -40,6 +40,14 @@ export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime
40
40
  pruneResultArtifacts();
41
41
  await announceRecoveryRecords(runtime.configPath, ctx);
42
42
  await migrateUnavailableAgentModels(ctx, runtime);
43
+ if (!runtime.restoredNotified && runtime.restoredRunIds.length > 0) {
44
+ runtime.restoredNotified = true;
45
+ const ids = runtime.restoredRunIds.map((id) => `#${id}`).join(", ");
46
+ ctx.ui.notify(
47
+ `pi-subagents: restored ${runtime.restoredRunIds.length} resumable thread${runtime.restoredRunIds.length === 1 ? "" : "s"} from the previous session (${ids}). subagent_status lists them; subagent_control resume continues one.`,
48
+ "info",
49
+ );
50
+ }
43
51
  if (ctx.mode !== "tui") return;
44
52
  installActiveRunsWidget(ctx);
45
53
  });
package/src/background.ts CHANGED
@@ -29,13 +29,19 @@ interface PendingTask {
29
29
 
30
30
  /** How many sub-agent processes may run at once, and how many tasks one
31
31
  * parallel `subagent` call may contain. Fixed by design: the queue sheds load
32
- * by waiting, so the knob bought nothing worth its maintenance. */
32
+ * by waiting, so the knob bought nothing worth its maintenance. Only manually
33
+ * dispatched top-level generations hold slots; runtime-initiated managed
34
+ * continuations (gate reviews, auto-fix rounds, documentation sync) suspend
35
+ * their task's slot so they never starve manual dispatches. */
33
36
  export const MAX_CONCURRENT_SUBAGENTS = 4;
34
37
 
35
38
  export class BackgroundTaskQueue {
36
39
  private concurrency: number;
37
40
  private readonly pending: PendingTask[] = [];
38
41
  private readonly active = new Set<AbortController>();
42
+ /** Active tasks that no longer count toward the concurrency limit. They keep
43
+ * every other guarantee: abortable, awaited by waitForTask/waitForIdle. */
44
+ private readonly suspended = new Set<AbortController>();
39
45
  private readonly completions = new WeakMap<AbortController, Promise<void>>();
40
46
  private readonly idleWaiters = new Set<() => void>();
41
47
  private stopped = false;
@@ -71,6 +77,16 @@ export class BackgroundTaskQueue {
71
77
  return this.completions.get(controller) ?? Promise.resolve();
72
78
  }
73
79
 
80
+ /** Stop counting a running task toward the concurrency limit. Its body keeps
81
+ * running under the same abort signal; completion still releases everything
82
+ * waitForTask/waitForIdle promise. Frees a slot for queued work immediately. */
83
+ suspend(controller: AbortController | undefined): void {
84
+ if (!controller || this.stopped) return;
85
+ if (!this.active.delete(controller)) return;
86
+ this.suspended.add(controller);
87
+ this.drain();
88
+ }
89
+
74
90
  /** Cancel one queued/running task. Queued entries are removed immediately;
75
91
  * active entries resolve waitForTask only after their body and error handler
76
92
  * have quiesced and the concurrency slot has been released. */
@@ -89,7 +105,7 @@ export class BackgroundTaskQueue {
89
105
 
90
106
  /** Resolve once no queued or running task remains. */
91
107
  waitForIdle(): Promise<void> {
92
- if (this.pending.length === 0 && this.active.size === 0) return Promise.resolve();
108
+ if (this.pending.length === 0 && this.active.size === 0 && this.suspended.size === 0) return Promise.resolve();
93
109
  return new Promise<void>((resolve) => this.idleWaiters.add(resolve));
94
110
  }
95
111
 
@@ -104,6 +120,7 @@ export class BackgroundTaskQueue {
104
120
  entry.complete();
105
121
  }
106
122
  for (const controller of this.active) controller.abort();
123
+ for (const controller of this.suspended) controller.abort();
107
124
  this.resolveIdleWaiters();
108
125
  }
109
126
 
@@ -145,6 +162,7 @@ export class BackgroundTaskQueue {
145
162
  })
146
163
  .finally(() => {
147
164
  this.active.delete(entry.controller);
165
+ this.suspended.delete(entry.controller);
148
166
  entry.complete();
149
167
  this.drain();
150
168
  this.resolveIdleWaiters();
@@ -153,7 +171,7 @@ export class BackgroundTaskQueue {
153
171
  }
154
172
 
155
173
  private resolveIdleWaiters(): void {
156
- if (this.pending.length > 0 || this.active.size > 0) return;
174
+ if (this.pending.length > 0 || this.active.size > 0 || this.suspended.size > 0) return;
157
175
  for (const resolve of this.idleWaiters) resolve();
158
176
  this.idleWaiters.clear();
159
177
  }