@ferris1225/pi-subagents 4.1.2 → 4.1.3

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.
@@ -1,80 +1,80 @@
1
- /** Pi SessionManager-backed cloning of a retained sub-agent session branch. */
2
-
3
- import { SessionManager } from "@earendil-works/pi-coding-agent";
4
- import { existsSync } from "node:fs";
5
- import { mkdtemp, rm } from "node:fs/promises";
6
- import { tmpdir } from "node:os";
7
- import { join } from "node:path";
8
-
9
- export interface ForkedSession {
10
- sessionDir: string;
11
- sessionId: string;
12
- sessionFile: string;
13
- }
14
-
15
- /** Locate one retained session by its authoritative header id. */
16
- export async function findRetainedSessionFile(
17
- sessionDir: string,
18
- sessionId: string,
19
- ): Promise<string> {
20
- // The retained header may point at a worktree that has since been removed.
21
- // The session id is authoritative inside this explicit private directory;
22
- // listing the directory directly avoids a stale-cwd filter rejecting it.
23
- const sessions = await SessionManager.listAll(sessionDir);
24
- const matches = sessions.filter((session) => session.id === sessionId);
25
- if (matches.length === 0) {
26
- throw new Error(`Retained session ${sessionId} was not found in ${sessionDir}.`);
27
- }
28
- if (matches.length > 1) {
29
- throw new Error(`Retained session id ${sessionId} is ambiguous in ${sessionDir}.`);
30
- }
31
- return matches[0].path;
32
- }
33
-
34
- /**
35
- * Copy only the source file's active branch into a new isolated temp session
36
- * directory. SessionManager performs all JSONL/tree handling; source state is
37
- * never mutated.
38
- */
39
- export async function forkRetainedSession(options: {
40
- /** Cwd stored in the source session header (used for exact lookup). */
41
- cwd: string;
42
- /** Optional cwd for the cloned session header and future child tools. */
43
- targetCwd?: string;
44
- sessionDir: string;
45
- sessionId: string;
46
- }): Promise<ForkedSession> {
47
- const sourceSessionFile = await findRetainedSessionFile(
48
- options.sessionDir,
49
- options.sessionId,
50
- );
51
- const sessionDir = await mkdtemp(join(tmpdir(), "pi-subagent-session-fork-"));
52
- try {
53
- // Supplying the new directory makes createBranchedSession write there.
54
- // cwdOverride rewrites the cloned header so a settled isolated session can
55
- // safely continue in its fresh worktree instead of a removed old path.
56
- const manager = SessionManager.open(
57
- sourceSessionFile,
58
- sessionDir,
59
- options.targetCwd ?? options.cwd,
60
- );
61
- const leafId = manager.getLeafId();
62
- if (!leafId) throw new Error(`Retained session ${options.sessionId} has no active branch to fork.`);
63
- const sessionFile = manager.createBranchedSession(leafId);
64
- if (!sessionFile) throw new Error("Pi SessionManager did not create a persistent fork.");
65
- // Pi defers branch files that contain no assistant response. Such a file
66
- // cannot be resumed by RPC without creating a blank session, so reject
67
- // rather than pretending context was preserved.
68
- if (!existsSync(sessionFile)) {
69
- throw new Error(`Forked session branch has no persisted assistant checkpoint at ${sessionFile}.`);
70
- }
71
- return {
72
- sessionDir,
73
- sessionId: manager.getSessionId(),
74
- sessionFile,
75
- };
76
- } catch (error) {
77
- await rm(sessionDir, { recursive: true, force: true }).catch(() => undefined);
78
- throw error;
79
- }
80
- }
1
+ /** Pi SessionManager-backed cloning of a retained sub-agent session branch. */
2
+
3
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
4
+ import { existsSync } from "node:fs";
5
+ import { mkdtemp, rm } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+
9
+ export interface ForkedSession {
10
+ sessionDir: string;
11
+ sessionId: string;
12
+ sessionFile: string;
13
+ }
14
+
15
+ /** Locate one retained session by its authoritative header id. */
16
+ export async function findRetainedSessionFile(
17
+ sessionDir: string,
18
+ sessionId: string,
19
+ ): Promise<string> {
20
+ // The retained header may point at a worktree that has since been removed.
21
+ // The session id is authoritative inside this explicit private directory;
22
+ // listing the directory directly avoids a stale-cwd filter rejecting it.
23
+ const sessions = await SessionManager.listAll(sessionDir);
24
+ const matches = sessions.filter((session) => session.id === sessionId);
25
+ if (matches.length === 0) {
26
+ throw new Error(`Retained session ${sessionId} was not found in ${sessionDir}.`);
27
+ }
28
+ if (matches.length > 1) {
29
+ throw new Error(`Retained session id ${sessionId} is ambiguous in ${sessionDir}.`);
30
+ }
31
+ return matches[0].path;
32
+ }
33
+
34
+ /**
35
+ * Copy only the source file's active branch into a new isolated temp session
36
+ * directory. SessionManager performs all JSONL/tree handling; source state is
37
+ * never mutated.
38
+ */
39
+ export async function forkRetainedSession(options: {
40
+ /** Cwd stored in the source session header (used for exact lookup). */
41
+ cwd: string;
42
+ /** Optional cwd for the cloned session header and future child tools. */
43
+ targetCwd?: string;
44
+ sessionDir: string;
45
+ sessionId: string;
46
+ }): Promise<ForkedSession> {
47
+ const sourceSessionFile = await findRetainedSessionFile(
48
+ options.sessionDir,
49
+ options.sessionId,
50
+ );
51
+ const sessionDir = await mkdtemp(join(tmpdir(), "pi-subagent-session-fork-"));
52
+ try {
53
+ // Supplying the new directory makes createBranchedSession write there.
54
+ // cwdOverride rewrites the cloned header so a settled isolated session can
55
+ // safely continue in its fresh worktree instead of a removed old path.
56
+ const manager = SessionManager.open(
57
+ sourceSessionFile,
58
+ sessionDir,
59
+ options.targetCwd ?? options.cwd,
60
+ );
61
+ const leafId = manager.getLeafId();
62
+ if (!leafId) throw new Error(`Retained session ${options.sessionId} has no active branch to fork.`);
63
+ const sessionFile = manager.createBranchedSession(leafId);
64
+ if (!sessionFile) throw new Error("Pi SessionManager did not create a persistent fork.");
65
+ // Pi defers branch files that contain no assistant response. Such a file
66
+ // cannot be resumed by RPC without creating a blank session, so reject
67
+ // rather than pretending context was preserved.
68
+ if (!existsSync(sessionFile)) {
69
+ throw new Error(`Forked session branch has no persisted assistant checkpoint at ${sessionFile}.`);
70
+ }
71
+ return {
72
+ sessionDir,
73
+ sessionId: manager.getSessionId(),
74
+ sessionFile,
75
+ };
76
+ } catch (error) {
77
+ await rm(sessionDir, { recursive: true, force: true }).catch(() => undefined);
78
+ throw error;
79
+ }
80
+ }
package/src/setup.ts CHANGED
@@ -63,7 +63,7 @@ const MODULE_HINTS: Record<string, string> = {
63
63
  explorer: "read-only codebase recon (fast model)",
64
64
  worker: "implement / fix / refactor / test (full tools)",
65
65
  cleaner: "apply proven cleanup and deduplicate code (full tools)",
66
- documenter: "sync diff or whole-codebase comments/docs (full tools)",
66
+ documenter: "sync diff or whole-codebase comments/docs (docs write)",
67
67
  reviewer: "read-only audits and pre-commit gates",
68
68
  };
69
69
 
@@ -89,7 +89,7 @@ async function pickEnabledAgents(
89
89
  return promptSelectMany(
90
90
  ctx,
91
91
  "Enable which sub-agents?",
92
- "Space toggles • Enter confirms • Esc cancels",
92
+ "Space toggles • Enter confirms • Esc returns to settings",
93
93
  items,
94
94
  current,
95
95
  );
@@ -120,7 +120,7 @@ async function pickAgentModel(
120
120
  ctx: ExtensionCommandContext,
121
121
  agentName: string,
122
122
  currentRef: string | undefined,
123
- escNote = "cancels setup",
123
+ escNote = "cancels this setup pass",
124
124
  ): Promise<string | undefined> {
125
125
  return pickConfiguredModel(ctx, `Model for "${agentName}"?`, currentRef, escNote);
126
126
  }
@@ -151,7 +151,7 @@ async function pickAgentStrength(
151
151
  model: Model<Api> | undefined,
152
152
  current: ThinkingLevel | undefined,
153
153
  agentDefault: ThinkingLevel,
154
- escNote = "cancels setup",
154
+ escNote = "cancels this setup pass",
155
155
  ): Promise<ThinkingLevel | typeof AUTO_THINKING | undefined> {
156
156
  const supported = supportedThinkingLevels(model);
157
157
  const automatic = resolveThinkingLevel(model, agentDefault);
@@ -191,44 +191,48 @@ async function pickAgentToConfigure(
191
191
  return promptSelectOne(
192
192
  ctx,
193
193
  "Configure which agent?",
194
- "Type to filter • ↑/↓ • Enter selects • Esc ends this pass",
194
+ "Type to filter • ↑/↓ • Enter selects • Esc returns to settings",
195
195
  enabledAgents.map((name) => ({ value: name, label: moduleLabel(name) })),
196
196
  );
197
197
  }
198
198
 
199
- /** One agent: model, then thinking if the model exposes a choice. Esc at any
200
- * step ends the caller's pass; earlier agents in that pass stay applied. */
199
+ interface ConfiguredAgentChoice {
200
+ name: string;
201
+ model: string;
202
+ strength: ThinkingLevel | typeof AUTO_THINKING;
203
+ }
204
+
205
+ /** Configure one agent while preserving the UI back stack: thinking → model →
206
+ * agent selection. Esc from agent selection ends this configuration pass. */
201
207
  async function configureOneAgent(
202
208
  ctx: ExtensionCommandContext,
203
209
  config: SubagentsConfig,
204
- ): Promise<
205
- | {
206
- name: string;
207
- model: string;
208
- strength: ThinkingLevel | typeof AUTO_THINKING;
209
- }
210
- | undefined
211
- > {
212
- const name = await pickAgentToConfigure(ctx, config.enabledAgents);
213
- if (name === undefined) return undefined;
214
- const modelChoice = await pickAgentModel(
215
- ctx,
216
- name,
217
- config.agentModels[name],
218
- "stops earlier agent changes are kept",
219
- );
220
- if (modelChoice === undefined) return undefined;
221
- const model = effectiveModelForChoice(ctx, modelChoice);
222
- const strength = await pickAgentStrength(
223
- ctx,
224
- name,
225
- model,
226
- config.agentThinkingLevels[name],
227
- actualAgentThinkingDefault(ctx, config, name),
228
- "stops — earlier agent changes are kept",
229
- );
230
- if (strength === undefined) return undefined;
231
- return { name, model: modelChoice, strength };
210
+ ): Promise<ConfiguredAgentChoice | undefined> {
211
+ while (true) {
212
+ const name = await pickAgentToConfigure(ctx, config.enabledAgents);
213
+ if (name === undefined) return undefined;
214
+
215
+ while (true) {
216
+ const modelChoice = await pickAgentModel(
217
+ ctx,
218
+ name,
219
+ config.agentModels[name],
220
+ "returns to agent selection",
221
+ );
222
+ if (modelChoice === undefined) break;
223
+ const model = effectiveModelForChoice(ctx, modelChoice);
224
+ const strength = await pickAgentStrength(
225
+ ctx,
226
+ name,
227
+ model,
228
+ config.agentThinkingLevels[name],
229
+ actualAgentThinkingDefault(ctx, config, name),
230
+ "returns to model selection",
231
+ );
232
+ if (strength === undefined) continue;
233
+ return { name, model: modelChoice, strength };
234
+ }
235
+ }
232
236
  }
233
237
 
234
238
  async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Promise<boolean | undefined> {
@@ -280,21 +284,21 @@ function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string
280
284
  return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
281
285
  }
282
286
 
283
- async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<void> {
287
+ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<boolean> {
284
288
  const enabled = await pickEnabledAgents(ctx, base.enabledAgents);
285
- if (enabled === undefined) return notifyCancelled(ctx);
289
+ if (enabled === undefined) return false;
286
290
 
287
291
  let agentModels = keepAgentEntries(base.agentModels, enabled);
288
292
  for (const agentName of enabled) {
289
293
  const choice = await pickAgentModel(ctx, agentName, agentModels[agentName]);
290
- if (choice === undefined) return notifyCancelled(ctx);
294
+ if (choice === undefined) return false;
291
295
  agentModels = applyAgentModelChoice(agentModels, agentName, choice);
292
296
  }
293
297
 
294
298
  const injection = await pickInjection(ctx, base.proactiveInjection);
295
- if (injection === undefined) return notifyCancelled(ctx);
299
+ if (injection === undefined) return false;
296
300
  const scope = await pickScope(ctx, base.agentScope);
297
- if (scope === undefined) return notifyCancelled(ctx);
301
+ if (scope === undefined) return false;
298
302
  const maxConcurrency = await pickCount(
299
303
  ctx,
300
304
  "Max sub-agents running at once?",
@@ -302,7 +306,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
302
306
  base.maxConcurrency,
303
307
  DEFAULT_MAX_CONCURRENCY,
304
308
  );
305
- if (maxConcurrency === undefined) return notifyCancelled(ctx);
309
+ if (maxConcurrency === undefined) return false;
306
310
  const maxFixRounds = await pickCount(
307
311
  ctx,
308
312
  "Reviewer worker-fix rounds? (0 = no automatic fixes)",
@@ -310,7 +314,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
310
314
  base.maxFixRounds,
311
315
  DEFAULT_MAX_FIX_ROUNDS,
312
316
  );
313
- if (maxFixRounds === undefined) return notifyCancelled(ctx);
317
+ if (maxFixRounds === undefined) return false;
314
318
  const idleTimeoutSec = await pickCount(
315
319
  ctx,
316
320
  "Idle timeout in seconds? (0 = disabled)",
@@ -318,7 +322,7 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
318
322
  base.idleTimeoutSec,
319
323
  DEFAULT_IDLE_TIMEOUT_SEC,
320
324
  );
321
- if (idleTimeoutSec === undefined) return notifyCancelled(ctx);
325
+ if (idleTimeoutSec === undefined) return false;
322
326
 
323
327
  const next: SubagentsConfig = {
324
328
  enabledAgents: enabled,
@@ -342,114 +346,123 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
342
346
  };
343
347
  await saveConfig(next, configPath);
344
348
  ctx.ui.notify(`pi-subagents configured with Auto thinking. Saved to ${configPath}`, "info");
349
+ return true;
345
350
  }
346
351
 
347
352
  async function updateRuntimeSetting(
348
353
  ctx: ExtensionCommandContext,
349
354
  config: SubagentsConfig,
350
355
  ): Promise<SubagentsConfig | undefined> {
351
- const choice = await ctx.ui.select("Runtime setting", [
352
- "Proactive injection",
353
- "Agent scope",
354
- "Max concurrency",
355
- "Reviewer worker-fix rounds",
356
- "Idle timeout",
357
- ]);
358
- if (choice === undefined) return undefined;
359
- const next = { ...config };
360
- if (choice.startsWith("Proactive")) {
361
- const value = await pickInjection(ctx, config.proactiveInjection);
362
- if (value === undefined) return undefined;
363
- next.proactiveInjection = value;
364
- } else if (choice.startsWith("Agent scope")) {
365
- const value = await pickScope(ctx, config.agentScope);
366
- if (value === undefined) return undefined;
367
- next.agentScope = value;
368
- } else if (choice.startsWith("Max concurrency")) {
369
- const value = await pickCount(ctx, "Max sub-agents running at once?", CONCURRENCY_STEPS, config.maxConcurrency, DEFAULT_MAX_CONCURRENCY);
370
- if (value === undefined) return undefined;
371
- next.maxConcurrency = value;
372
- } else if (choice.startsWith("Reviewer")) {
373
- const value = await pickCount(ctx, "Reviewer worker-fix rounds?", FIX_ROUNDS_STEPS, config.maxFixRounds, DEFAULT_MAX_FIX_ROUNDS);
374
- if (value === undefined) return undefined;
375
- next.maxFixRounds = value;
376
- } else {
377
- const value = await pickCount(ctx, "Idle timeout in seconds?", IDLE_TIMEOUT_STEPS, config.idleTimeoutSec, DEFAULT_IDLE_TIMEOUT_SEC);
378
- if (value === undefined) return undefined;
379
- next.idleTimeoutSec = value;
356
+ while (true) {
357
+ const choice = await ctx.ui.select("Runtime setting", [
358
+ "Proactive injection",
359
+ "Agent scope",
360
+ "Max concurrency",
361
+ "Reviewer worker-fix rounds",
362
+ "Idle timeout",
363
+ ]);
364
+ if (choice === undefined) return undefined;
365
+ const next = { ...config };
366
+ if (choice.startsWith("Proactive")) {
367
+ const value = await pickInjection(ctx, config.proactiveInjection);
368
+ if (value === undefined) continue;
369
+ next.proactiveInjection = value;
370
+ } else if (choice.startsWith("Agent scope")) {
371
+ const value = await pickScope(ctx, config.agentScope);
372
+ if (value === undefined) continue;
373
+ next.agentScope = value;
374
+ } else if (choice.startsWith("Max concurrency")) {
375
+ const value = await pickCount(ctx, "Max sub-agents running at once?", CONCURRENCY_STEPS, config.maxConcurrency, DEFAULT_MAX_CONCURRENCY);
376
+ if (value === undefined) continue;
377
+ next.maxConcurrency = value;
378
+ } else if (choice.startsWith("Reviewer")) {
379
+ const value = await pickCount(ctx, "Reviewer worker-fix rounds?", FIX_ROUNDS_STEPS, config.maxFixRounds, DEFAULT_MAX_FIX_ROUNDS);
380
+ if (value === undefined) continue;
381
+ next.maxFixRounds = value;
382
+ } else {
383
+ const value = await pickCount(ctx, "Idle timeout in seconds?", IDLE_TIMEOUT_STEPS, config.idleTimeoutSec, DEFAULT_IDLE_TIMEOUT_SEC);
384
+ if (value === undefined) continue;
385
+ next.idleTimeoutSec = value;
386
+ }
387
+ return next;
380
388
  }
381
- return next;
382
389
  }
383
390
 
384
391
  async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
385
- const choice = await ctx.ui.select("pi-subagents settings", [
386
- "Enable/disable agents",
387
- "Configure an agent (model + thinking)",
388
- "Runtime settings",
389
- "Full re-setup",
390
- ]);
391
- if (choice === undefined) return notifyCancelled(ctx);
392
- if (choice.startsWith("Full")) return runFullSetup(ctx, configPath, config);
392
+ while (true) {
393
+ const choice = await ctx.ui.select("pi-subagents settings", [
394
+ "Enable/disable agents",
395
+ "Configure an agent (model + thinking)",
396
+ "Runtime settings",
397
+ "Full re-setup",
398
+ ]);
399
+ if (choice === undefined) return;
400
+ if (choice.startsWith("Full")) {
401
+ if (await runFullSetup(ctx, configPath, config)) return;
402
+ continue;
403
+ }
393
404
 
394
- let next: SubagentsConfig = {
395
- ...config,
396
- agentModels: { ...config.agentModels },
397
- agentThinkingLevels: { ...config.agentThinkingLevels },
398
- };
399
- if (choice.startsWith("Enable")) {
400
- const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
401
- if (enabled === undefined) return notifyCancelled(ctx);
402
- next.enabledAgents = enabled;
403
- // Newly enabling cleaner inherits the reviewer's configured model and
404
- // thinking level, so the file reflects what cleaner will actually run
405
- // instead of silently falling back to the current main model.
406
- if (!config.enabledAgents.includes("cleaner") && enabled.includes("cleaner")) {
407
- if (!next.agentModels.cleaner && config.agentModels.reviewer) {
408
- next.agentModels.cleaner = config.agentModels.reviewer;
409
- }
410
- if (!next.agentThinkingLevels.cleaner && config.agentThinkingLevels.reviewer) {
411
- next.agentThinkingLevels.cleaner = config.agentThinkingLevels.reviewer;
405
+ let next: SubagentsConfig = {
406
+ ...config,
407
+ agentModels: { ...config.agentModels },
408
+ agentThinkingLevels: { ...config.agentThinkingLevels },
409
+ };
410
+ if (choice.startsWith("Enable")) {
411
+ const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
412
+ if (enabled === undefined) continue;
413
+ next.enabledAgents = enabled;
414
+ // Newly enabling cleaner inherits the reviewer's configured model and
415
+ // thinking level, so the file reflects what cleaner will actually run
416
+ // instead of silently falling back to the current main model.
417
+ if (!config.enabledAgents.includes("cleaner") && enabled.includes("cleaner")) {
418
+ if (!next.agentModels.cleaner && config.agentModels.reviewer) {
419
+ next.agentModels.cleaner = config.agentModels.reviewer;
420
+ }
421
+ if (!next.agentThinkingLevels.cleaner && config.agentThinkingLevels.reviewer) {
422
+ next.agentThinkingLevels.cleaner = config.agentThinkingLevels.reviewer;
423
+ }
412
424
  }
413
- }
414
- // Documenter intentionally follows the faster explorer route. Fresh
415
- // installs leave it unselected; enabling it later inherits any explorer
416
- // overrides instead of silently choosing a stronger model.
417
- if (!config.enabledAgents.includes("documenter") && enabled.includes("documenter")) {
418
- if (!next.agentModels.documenter && config.agentModels.explorer) {
419
- next.agentModels.documenter = config.agentModels.explorer;
425
+ // Documenter intentionally follows the faster explorer route. Fresh
426
+ // installs leave it unselected; enabling it later inherits any explorer
427
+ // overrides instead of silently choosing a stronger model.
428
+ if (!config.enabledAgents.includes("documenter") && enabled.includes("documenter")) {
429
+ if (!next.agentModels.documenter && config.agentModels.explorer) {
430
+ next.agentModels.documenter = config.agentModels.explorer;
431
+ }
432
+ if (!next.agentThinkingLevels.documenter && config.agentThinkingLevels.explorer) {
433
+ next.agentThinkingLevels.documenter = config.agentThinkingLevels.explorer;
434
+ }
420
435
  }
421
- if (!next.agentThinkingLevels.documenter && config.agentThinkingLevels.explorer) {
422
- next.agentThinkingLevels.documenter = config.agentThinkingLevels.explorer;
436
+ next.agentModels = keepAgentEntries(next.agentModels, enabled);
437
+ next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
438
+ } else if (choice.startsWith("Configure")) {
439
+ // Per-agent loop: thinking Esc returns to that agent's model picker;
440
+ // model Esc returns to the agent picker; agent-picker Esc saves completed
441
+ // choices and returns to this settings menu.
442
+ let configuredAny = false;
443
+ while (true) {
444
+ const picked = await configureOneAgent(ctx, next);
445
+ if (!picked) break;
446
+ configuredAny = true;
447
+ next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
448
+ if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
449
+ else next.agentThinkingLevels[picked.name] = picked.strength;
423
450
  }
451
+ if (!configuredAny) continue;
452
+ await saveConfig(next, configPath);
453
+ ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
454
+ config = next;
455
+ continue;
456
+ } else {
457
+ const updated = await updateRuntimeSetting(ctx, next);
458
+ if (updated === undefined) continue;
459
+ next = updated;
424
460
  }
425
- next.agentModels = keepAgentEntries(next.agentModels, enabled);
426
- next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
427
- } else if (choice.startsWith("Configure")) {
428
- // Per-agent loop: model (+ thinking when the model exposes a choice), then
429
- // back to the agent picker so several agents can be set in one pass. Esc
430
- // at any step ends the loop; agents already configured in this pass are kept.
431
- let configuredAny = false;
432
- while (true) {
433
- const picked = await configureOneAgent(ctx, next);
434
- if (picked === undefined) break;
435
- configuredAny = true;
436
- next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
437
- if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
438
- else next.agentThinkingLevels[picked.name] = picked.strength;
439
- }
440
- if (!configuredAny) return notifyCancelled(ctx);
441
- } else {
442
- const updated = await updateRuntimeSetting(ctx, next);
443
- if (updated === undefined) return notifyCancelled(ctx);
444
- next = updated;
445
- }
446
461
 
447
- await saveConfig(next, configPath);
448
- ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
449
- }
450
-
451
- function notifyCancelled(ctx: ExtensionCommandContext): void {
452
- ctx.ui.notify("pi-subagents setup cancelled.", "info");
462
+ await saveConfig(next, configPath);
463
+ ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
464
+ return;
465
+ }
453
466
  }
454
467
 
455
468
  /** Entry point for the /subagents-setup command. */
@@ -462,7 +475,9 @@ export async function runSetup(ctx: ExtensionCommandContext, configPath: string
462
475
  const exists = await configExists(configPath);
463
476
  const config = await loadConfig(configPath);
464
477
  if (exists) await runMenu(ctx, configPath, config);
465
- else await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] });
478
+ else if (!(await runFullSetup(ctx, configPath, { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_ENABLED_AGENTS] }))) {
479
+ ctx.ui.notify("pi-subagents setup cancelled.", "info");
480
+ }
466
481
  } catch (error) {
467
482
  ctx.ui.notify(`pi-subagents setup failed: ${errorMessage(error)}`, "error");
468
483
  }
package/src/spawn.ts CHANGED
@@ -327,7 +327,7 @@ export function getResultOutput(result: SingleResult): string {
327
327
  }
328
328
 
329
329
  export function buildResumePrompt(task: string, reason: string): string {
330
- return `You are resuming an earlier sub-agent session after ${reason}. Your earlier work — searches, reads, edits, and reasoning — is preserved in this session's history above; review it before acting. Original task: ${task}. Pick up exactly where you left off and finish it. Do NOT redo searches, reads, or edits you already completed unless a step clearly failed. Continue now.`;
330
+ return `You are resuming an earlier sub-agent session after ${reason}. Your earlier work — searches, reads, edits, and reasoning — is preserved in this session's history above; review it before acting. Current objective: ${task}. Pick up exactly where you left off and finish it. Do NOT redo searches, reads, or edits you already completed unless a step clearly failed. Continue now.`;
331
331
  }
332
332
 
333
333
  export function buildFallbackResumeReason(fromModel?: string): string {
@@ -351,6 +351,9 @@ export interface RunSingleOptions {
351
351
  sessionId?: string;
352
352
  /** Initial RPC prompt. Kept under the old name to limit caller churn. */
353
353
  stdinText?: string;
354
+ /** Refresh parent-derived tools immediately before every startup retry and
355
+ * selected-to-main fallback process is spawned. */
356
+ resolveAgentForAttempt?: (agent: AgentConfig) => AgentConfig;
354
357
  signal?: AbortSignal;
355
358
  onLive?: (event: SubagentLiveEvent) => void;
356
359
  makeDetails: (results: SingleResult[]) => SubagentDetails;
@@ -496,7 +499,10 @@ export async function runSingleAgentWithMainFallback(
496
499
  }
497
500
  const start = Date.now();
498
501
  try {
499
- lastResult = await runSingleAgent(opts);
502
+ const attemptOptions = opts.resolveAgentForAttempt
503
+ ? { ...opts, agent: opts.resolveAgentForAttempt(opts.agent) }
504
+ : opts;
505
+ lastResult = await runSingleAgent(attemptOptions);
500
506
  } catch (error) {
501
507
  const failed = await dispatchFailure(error);
502
508
  return controlledDisposition(opts, failed) ?? failed;