@ferris1225/pi-subagents 4.1.18 → 4.1.21

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/src/runtime.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared per-session runtime state for pi-subagents.
3
3
  *
4
- * The extension registers several tools (subagent, subagent_wait/status/stop)
4
+ * The extension registers several tools (subagent, subagent_control/stop)
5
5
  * that share the background queue, completion batcher, abort controllers per
6
6
  * run, and settled-results store.
7
7
  * `createRuntime` builds those once per extension load and hands the same object
@@ -25,6 +25,7 @@ import { isRunActiveStatus, monitor } from "./monitor.ts";
25
25
  import type { RpcRunControl } from "./rpc-run.ts";
26
26
  import type { StartBackgroundInternal } from "./thread-lifecycle.ts";
27
27
  import { isFailedResult, type SingleResult } from "./spawn.ts";
28
+ import type { ReviewMode } from "./workflow.ts";
28
29
  import type { IsolationMode, WorktreeFinalization, WorktreeIsolation } from "./worktree.ts";
29
30
 
30
31
  export type ThreadState =
@@ -50,6 +51,9 @@ export interface SubagentThread {
50
51
  executionCwd: string;
51
52
  thinkingLevel?: ThinkingLevel;
52
53
  isolation: IsolationMode;
54
+ /** Dispatch-time gate intensity; "none" skips the automatic post-writer
55
+ * reviewer for this thread (kept across resumes and reloads). */
56
+ review?: ReviewMode;
53
57
  worktree?: WorktreeIsolation;
54
58
  state: ThreadState;
55
59
  control: RpcRunControl;
@@ -90,6 +94,13 @@ export interface SubagentRuntime {
90
94
  /** The process-wide background dispatcher. Set at tool registration so
91
95
  * threads restored from the durable manifest can resume before any dispatch. */
92
96
  dispatcher?: StartBackgroundInternal;
97
+ /** Resolves when the load-time durable restore pass has finished. Everything
98
+ * that answers "which threads exist" awaits it — the lookup tools, a fresh
99
+ * dispatch before it allocates a run id, and the restored-thread notice — so
100
+ * a reload can never report parked work as missing, or hand a new run an id a
101
+ * record still owns, while the manifest is being read. Resolved by default;
102
+ * `bootstrapDurableState` publishes the real pass. */
103
+ durableRestore: Promise<void>;
93
104
  /** Run ids restored from the durable manifest at load; consumed by the
94
105
  * one-time session-start notice. */
95
106
  restoredRunIds: number[];
@@ -100,8 +111,9 @@ export interface SubagentRuntime {
100
111
  completionBatcher: CompletionBatcher<CompletionMessageItem>;
101
112
  /** Abort controllers per active run, so subagent_stop can cancel a run in-turn. */
102
113
  runControllers: Map<number, AbortController>;
103
- /** Final results keyed by run id, so subagent_wait can hand the model the
104
- * actual result in-turn instead of it sleeping/polling for a wake-up message. */
114
+ /** Final results keyed by run id, so a dispatch with wait: true can hand the
115
+ * model the actual result in-turn instead of it sleeping/polling for a
116
+ * wake-up message. */
105
117
  settledRuns: Map<number, SingleResult>;
106
118
  settledListeners: Map<number, Set<(result: SingleResult) => void>>;
107
119
  registerRunResult: (runId: number, result: SingleResult) => void;
@@ -127,6 +139,7 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
127
139
  backgroundQueue,
128
140
  getActiveTools: () => pi.getActiveTools(),
129
141
  sessionActive: true,
142
+ durableRestore: Promise.resolve(),
130
143
  restoredRunIds: [],
131
144
  restoredNotified: false,
132
145
  sendCompletionGroup: (items) => {
@@ -140,7 +153,12 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
140
153
  const active = monitor
141
154
  .getRuns()
142
155
  .filter((run) => isRunActiveStatus(run.status))
143
- .map((run) => ({ id: run.id, agent: run.agent, label: run.label, queued: run.status === "queued" }));
156
+ .map((run) => ({
157
+ id: run.id,
158
+ agent: run.agent,
159
+ label: run.label,
160
+ ...(run.status === "queued" && run.waitReason ? { wait: run.waitReason } : {}),
161
+ }));
144
162
  const message = {
145
163
  customType: "subagent-result",
146
164
  content: formatCompletionMessage(items) + formatActiveRunsFooter(active),
@@ -4,6 +4,7 @@ import { SessionManager } from "@earendil-works/pi-coding-agent";
4
4
  import { existsSync } from "node:fs";
5
5
  import { mkdir, mkdtemp, rm } from "node:fs/promises";
6
6
  import { join } from "node:path";
7
+ import { writeTempOwnerMarker } from "./temp-hygiene.ts";
7
8
 
8
9
  export interface ForkedSession {
9
10
  sessionDir: string;
@@ -53,6 +54,7 @@ export async function forkRetainedSession(options: {
53
54
  const root = options.targetRoot;
54
55
  await mkdir(root, { recursive: true });
55
56
  const sessionDir = await mkdtemp(join(root, "pi-subagent-session-fork-"));
57
+ writeTempOwnerMarker(sessionDir);
56
58
  try {
57
59
  // Supplying the new directory makes createBranchedSession write there.
58
60
  // cwdOverride rewrites the cloned header so a settled isolated session can
package/src/setup.ts CHANGED
@@ -2,11 +2,10 @@
2
2
  * Interactive configuration wizard for /subagents-setup.
3
3
  *
4
4
  * The wizard stays one level deep and exposes only what most users touch:
5
- * which agents run, the model and thinking strength each runs on, and the
6
- * delegation directive toggle. Everything else (agent scope, idle timeout,
7
- * result lines, notifications) is config-file-only; model failures hand
8
- * directly to the current main model, and thinking defaults to capability-
9
- * aware Auto.
5
+ * which agents run and the model and thinking strength each runs on.
6
+ * Everything else (agent scope, idle timeout, result lines, notifications) is
7
+ * config-file-only; model failures hand directly to the current main model,
8
+ * and thinking defaults to capability-aware Auto.
10
9
  */
11
10
 
12
11
  import { stat } from "node:fs/promises";
@@ -40,10 +39,11 @@ import { promptSelectMany, promptSelectOne } from "./ui.ts";
40
39
 
41
40
  /** Short, selection-friendly descriptions for the built-in agents. */
42
41
  const MODULE_HINTS: Record<string, string> = {
43
- explorer: "read-only codebase recon (fast model)",
42
+ explorer: "read-only codebase recon (fast, read-only tools)",
44
43
  worker: "implement / fix / refactor / test (full tools)",
45
44
  cleaner: "apply proven cleanup and deduplicate code (full tools)",
46
45
  documenter: "sync diff or whole-codebase comments/docs (docs write)",
46
+ synthesizer: "merge fan-out results/long sources into one brief (read-only)",
47
47
  reviewer: "read-only audits and pre-commit gates",
48
48
  };
49
49
 
@@ -230,14 +230,6 @@ async function configureOneAgent(
230
230
  }
231
231
  }
232
232
 
233
- async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Promise<boolean | undefined> {
234
- const on = "On — inject the delegation directive (recommended)";
235
- const off = "Off — rely on tool descriptions only";
236
- const choice = await ctx.ui.select("Proactive dispatch injection?", [current ? `${on} (current)` : on, current ? off : `${off} (current)`]);
237
- if (choice === undefined) return undefined;
238
- return choice.startsWith("On");
239
- }
240
-
241
233
  function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string[]): Record<string, T> {
242
234
  const keep = new Set(enabled);
243
235
  return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
@@ -254,9 +246,6 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
254
246
  agentModels = applyAgentModelChoice(agentModels, agentName, choice);
255
247
  }
256
248
 
257
- const injection = await pickInjection(ctx, base.proactiveInjection);
258
- if (injection === undefined) return false;
259
-
260
249
  const next: SubagentsConfig = {
261
250
  enabledAgents: enabled,
262
251
  agentModels,
@@ -264,7 +253,6 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
264
253
  agentThinkingLevels: {},
265
254
  notifyOnReviewPass: base.notifyOnReviewPass,
266
255
  maxResultLines: base.maxResultLines,
267
- proactiveInjection: injection,
268
256
  agentScope: base.agentScope,
269
257
  idleTimeoutSec: base.idleTimeoutSec,
270
258
  };
@@ -278,7 +266,6 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
278
266
  const choice = await ctx.ui.select("pi-subagents settings", [
279
267
  "Enable/disable agents",
280
268
  "Configure an agent (model + thinking)",
281
- "Proactive injection",
282
269
  "Full re-setup",
283
270
  ]);
284
271
  if (choice === undefined) return;
@@ -296,31 +283,28 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
296
283
  const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
297
284
  if (enabled === undefined) continue;
298
285
  next.enabledAgents = enabled;
299
- // Newly enabling cleaner inherits the reviewer's configured model and
300
- // thinking level, so the file reflects what cleaner will actually run
301
- // instead of silently falling back to the current main model.
302
- if (!config.enabledAgents.includes("cleaner") && enabled.includes("cleaner")) {
303
- if (!next.agentModels.cleaner && config.agentModels.reviewer) {
304
- next.agentModels.cleaner = config.agentModels.reviewer;
305
- }
306
- if (!next.agentThinkingLevels.cleaner && config.agentThinkingLevels.reviewer) {
307
- next.agentThinkingLevels.cleaner = config.agentThinkingLevels.reviewer;
286
+ // A newly enabled role inherits a kindred role's configured model and
287
+ // thinking level, so the file reflects what it will actually run
288
+ // instead of silently falling back to the current main model: cleaner
289
+ // follows the reviewer; documenter and synthesizer intentionally
290
+ // follow the faster explorer route.
291
+ const modelInheritance: ReadonlyArray<[agent: string, from: string]> = [
292
+ ["cleaner", "reviewer"],
293
+ ["documenter", "explorer"],
294
+ ["synthesizer", "explorer"],
295
+ ];
296
+ for (const [agent, from] of modelInheritance) {
297
+ if (config.enabledAgents.includes(agent) || !enabled.includes(agent)) continue;
298
+ if (!next.agentModels[agent] && config.agentModels[from]) {
299
+ next.agentModels[agent] = config.agentModels[from];
308
300
  }
309
- }
310
- // Documenter intentionally follows the faster explorer route. When it
311
- // is re-enabled after being explicitly disabled, it inherits any
312
- // explorer overrides instead of silently choosing a stronger model.
313
- if (!config.enabledAgents.includes("documenter") && enabled.includes("documenter")) {
314
- if (!next.agentModels.documenter && config.agentModels.explorer) {
315
- next.agentModels.documenter = config.agentModels.explorer;
316
- }
317
- if (!next.agentThinkingLevels.documenter && config.agentThinkingLevels.explorer) {
318
- next.agentThinkingLevels.documenter = config.agentThinkingLevels.explorer;
301
+ if (!next.agentThinkingLevels[agent] && config.agentThinkingLevels[from]) {
302
+ next.agentThinkingLevels[agent] = config.agentThinkingLevels[from];
319
303
  }
320
304
  }
321
305
  next.agentModels = keepAgentEntries(next.agentModels, enabled);
322
306
  next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
323
- } else if (choice.startsWith("Configure")) {
307
+ } else {
324
308
  // Per-agent loop: thinking Esc returns to that agent's model picker;
325
309
  // model Esc returns to the agent picker; agent-picker Esc saves completed
326
310
  // choices and returns to this settings menu.
@@ -338,10 +322,6 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
338
322
  ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
339
323
  config = next;
340
324
  continue;
341
- } else {
342
- const injection = await pickInjection(ctx, next.proactiveInjection);
343
- if (injection === undefined) continue;
344
- next.proactiveInjection = injection;
345
325
  }
346
326
 
347
327
  await saveConfig(next, configPath);