@nredd/pi-subagents 0.22.0-nredd.7 → 0.22.0-nredd.8

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.
Files changed (46) hide show
  1. package/README.md +124 -37
  2. package/package.json +8 -8
  3. package/src/agent-file-toggle.ts +6 -4
  4. package/src/agent-manager.ts +131 -28
  5. package/src/agent-runner.ts +155 -62
  6. package/src/cross-extension-rpc.ts +11 -36
  7. package/src/custom-agents.ts +9 -5
  8. package/src/default-agents.ts +2 -4
  9. package/src/enabled-models.ts +8 -7
  10. package/src/enterprise-local.ts +5 -5
  11. package/src/home-paths.ts +27 -0
  12. package/src/index.ts +191 -90
  13. package/src/invocation-config.ts +3 -16
  14. package/src/memory.ts +9 -9
  15. package/src/mention-clone.ts +9 -5
  16. package/src/model-resolver.ts +41 -11
  17. package/src/model-scope.ts +2 -1
  18. package/src/model-selection.ts +159 -0
  19. package/src/model-tiers.ts +171 -0
  20. package/src/nested-tools.ts +22 -26
  21. package/src/output-file.ts +23 -1
  22. package/src/package-resources.ts +25 -10
  23. package/src/process-budget.ts +6 -3
  24. package/src/quota-admission.ts +21 -9
  25. package/src/resource-loader.ts +51 -0
  26. package/src/schedule.ts +36 -18
  27. package/src/settings.ts +42 -12
  28. package/src/skill-loader.ts +8 -7
  29. package/src/stale-ctx.ts +16 -0
  30. package/src/types.ts +27 -0
  31. package/src/ui/agent-row.ts +13 -3
  32. package/src/ui/agent-widget.ts +15 -4
  33. package/src/ui/conversation-viewer.ts +6 -3
  34. package/src/ui/fallback-flag.ts +90 -0
  35. package/src/ui/fleet-list.ts +363 -53
  36. package/src/ui/fleet-state.ts +154 -0
  37. package/src/ui/notification-view.ts +18 -2
  38. package/src/ui/workflow-card.ts +5 -1
  39. package/src/ui/workflow-dialog.ts +10 -0
  40. package/src/workflow/host.ts +43 -45
  41. package/src/workflow/journal.ts +13 -1
  42. package/src/workflow/progress.ts +4 -0
  43. package/src/workflow/runtime.ts +25 -8
  44. package/src/workflow/saved.ts +19 -17
  45. package/src/workflow/task.ts +10 -0
  46. package/src/workflow/worker-source.ts +156 -38
package/src/index.ts CHANGED
@@ -12,13 +12,13 @@
12
12
 
13
13
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
14
14
  import { isAbsolute, join } from "node:path";
15
- import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ExtensionFactory, getAgentDir, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
15
+ import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ExtensionFactory, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
16
16
  import { Container, isKeyRelease, Key, matchesKey, type ScrollViewScrollbar, type SettingItem, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
17
17
  import { nanoid } from "nanoid";
18
18
  import { Type } from "typebox";
19
19
  import { abortable } from "./abortable.js";
20
20
  import { hasAgentBadge, renderAgentName } from "./agent-color.js";
21
- import { buildNewAgentFile, disableInContent, enableInContent, isEmptyStub, locateAgentFile, personalAgentsDir, projectAgentsDir, serializeAgentFile } from "./agent-file-toggle.js";
21
+ import { buildNewAgentFile, disableInContent, enableInContent, isEmptyStub, locateAgentFile as locateAgentFileAtHome, personalAgentsDir as personalAgentsDirAtHome, projectAgentsDir, serializeAgentFile } from "./agent-file-toggle.js";
22
22
  import { AgentManager, describeEvicted, isTopLevelAgent, markConsumed } from "./agent-manager.js";
23
23
  import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, getRememberAgents, normalizeMaxTurns, resolveEffectiveMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, setRememberAgents, steerAgent } from "./agent-runner.js";
24
24
  import { rowDescription, sourceIndicator, sourceLegend } from "./agent-source-badge.js";
@@ -39,14 +39,17 @@ import {
39
39
  } from "./detached-results.js";
40
40
  import { readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
41
41
  import { GroupJoinManager } from "./group-join.js";
42
+ import { resolveAgentDir } from "./home-paths.js";
42
43
  import { cwdParam, isolationParam, resolveAgentInvocationConfig, resolveCwdParam, resolveJoinMode } from "./invocation-config.js";
43
44
  import { describeMention, handleBase, isReservedHandle, parseMention, resolveHandleToType, stripAgentPrefix } from "./mention.js";
44
45
  import { runMentionClone } from "./mention-clone.js";
45
46
  import { applyModelCallEvent, type ModelCallEvent } from "./model-call.js";
46
- import { describeModel, describeVirtualModel, isVirtualModel, type ModelRegistry, resolveModel, setAgentModels, setModelAliases, setModelScopeProvider } from "./model-resolver.js";
47
- import { checkModelScope, isScopeModelsEnabled, setScopeModelsEnabled } from "./model-scope.js";
47
+ import { type ModelFallback, type ModelRegistry, setAgentModels, setModelAliases, setModelScopeProvider } from "./model-resolver.js";
48
+ import { isScopeModelsEnabled, setScopeModelsEnabled } from "./model-scope.js";
49
+ import { admitModelSelection, describeSelection, modelRequest, modelSelectionNote, selectModel } from "./model-selection.js";
50
+ import { tierDisclosure } from "./model-tiers.js";
48
51
  import { getMaxSubagentDepth, setMaxSubagentDepth } from "./nested-tools.js";
49
- import { createOutputFilePath, ensureOutputFile, getOutputTranscriptDefault, sessionTaskDir, setOutputTranscriptDefault, streamToOutputFile, writeInitialEntry } from "./output-file.js";
52
+ import { appendModelFallbackEntry, appendModelTierEntry, createOutputFilePath, ensureOutputFile, getOutputTranscriptDefault, sessionTaskDir, setOutputTranscriptDefault, streamToOutputFile, writeInitialEntry } from "./output-file.js";
50
53
  import { getPackageAgentsGate, getPackageWorkflowsGate, invalidatePackageCache, isProjectTrusted, packageNameForPath, setPackageAgentsGate, setPackageWorkflowsGate, setProjectTrusted } from "./package-resources.js";
51
54
  import { getBudgetPolicy, setBudgetPolicy } from "./process-budget.js";
52
55
  import { checkAdmission, describeBlock, mayPark, modelRef, QUOTA_RESTART_GRACE_MS } from "./quota-admission.js";
@@ -75,6 +78,7 @@ import {
75
78
  } from "./ui/agent-widget.js";
76
79
  import { ConversationViewer, getConversationOverlayOptions, type ViewerBackgroundAction } from "./ui/conversation-viewer.js";
77
80
  import { FleetList, type FleetUICtx, type FleetWorkflow } from "./ui/fleet-list.js";
81
+ import { FLEET_STATE_ENTRY, FLEET_STATE_VERSION } from "./ui/fleet-state.js";
78
82
  import { renderNotification } from "./ui/notification-view.js";
79
83
  import { showSchedulesMenu } from "./ui/schedule-menu.js";
80
84
  import { selectItem } from "./ui/select-item.js";
@@ -204,6 +208,7 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number, showC
204
208
  record.outputFile ? `<output-file>${escapeXml(record.outputFile)}</output-file>` : null,
205
209
  `<status>${escapeXml(status)}</status>`,
206
210
  `<summary>Agent "${escapeXml(record.description)}" ${record.status}${getStatusNote(record.status)}</summary>`,
211
+ modelSelectionNote(record) ? `<model-note>${escapeXml(modelSelectionNote(record))}</model-note>` : null,
207
212
  `<result>${escapeXml(resultPreview)}</result>`,
208
213
  `<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}${costXml}<duration_ms>${durationMs}</duration_ms></usage>`,
209
214
  `</task-notification>`,
@@ -212,7 +217,7 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number, showC
212
217
 
213
218
  /** Build AgentDetails from a base + record-specific fields. */
214
219
  function buildDetails(
215
- base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
220
+ base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "modelNote" | "modelFallback" | "modelTier" | "tags">,
216
221
  record: { toolUses: number; startedAt: number; completedAt?: number; status: string; error?: string; id?: string; session?: any; outputFile?: string; lifetimeUsage: LifetimeUsage },
217
222
  activity?: AgentActivity,
218
223
  overrides?: Partial<AgentDetails>,
@@ -255,6 +260,8 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
255
260
  durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
256
261
  outputFile: record.outputFile,
257
262
  error: record.error,
263
+ modelNote: modelSelectionNote(record) || undefined,
264
+ modelTier: record.modelTier,
258
265
  resultPreview: record.result
259
266
  ? record.result.length > resultMaxLen
260
267
  ? record.result.slice(0, resultMaxLen) + "…"
@@ -287,12 +294,18 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
287
294
  * agents and those with `extensions: false`.
288
295
  */
289
296
  export function formatToolsSuffix(cfg: AgentConfig | undefined): string {
290
- const tools = cfg?.builtinToolNames;
297
+ const denied = cfg?.disallowedTools ?? [];
298
+ const tools = denied.length > 0
299
+ ? (cfg?.builtinToolNames ?? BUILTIN_TOOL_NAMES).filter((tool) => !denied.includes(tool))
300
+ : cfg?.builtinToolNames;
301
+ // Only claim what is known at registration: extensions may supply command
302
+ // tools (including ssh_exec), so missing bash is not proof of no remote access.
303
+ const shellNote = tools && !tools.includes("bash") ? "; no built-in shell: cannot run bash commands" : "";
291
304
  // `isolated` overrides extensions to false in the runner, so both mean the
292
305
  // agent has no extension tools.
293
306
  const noExtensionTools = cfg?.isolated === true || cfg?.extensions === false;
294
307
  if (tools && tools.length === 0) {
295
- return noExtensionTools ? "none" : "no built-ins, extension tools only";
308
+ return (noExtensionTools ? "none" : "no built-ins, extension tools only") + shellNote;
296
309
  }
297
310
  const isFullSet = !tools
298
311
  || (tools.length === BUILTIN_TOOL_NAMES.length && BUILTIN_TOOL_NAMES.every((t) => tools.includes(t)));
@@ -300,7 +313,7 @@ export function formatToolsSuffix(cfg: AgentConfig | undefined): string {
300
313
  // Extension tools resolve at run time, so they cannot be enumerated here, but
301
314
  // omitting the fact reads as "built-ins only": an agent that can search the
302
315
  // web through an extension then looks web-less, and the work gets routed away.
303
- return noExtensionTools ? builtins : `${builtins} + extension tools`;
316
+ return (noExtensionTools ? builtins : `${builtins} + extension tools`) + shellNote;
304
317
  }
305
318
 
306
319
  /** CLI flag that runs a workflow script at session start. */
@@ -320,6 +333,8 @@ export type { Clock, TimerHandle } from "./clock.js";
320
333
 
321
334
  /** Optional host dependencies, shared by the scheduler and completion delivery. */
322
335
  export interface SubagentsExtensionOptions {
336
+ /** Home for every user-scoped read and child run. Omitted uses host defaults at call time. */
337
+ homeDir?: string;
323
338
  clock?: Clock;
324
339
  }
325
340
 
@@ -332,6 +347,10 @@ export default createSubagentsExtension();
332
347
 
333
348
  function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions) {
334
349
  const clock = options.clock ?? realClock;
350
+ const getAgentDir = () => resolveAgentDir(options.homeDir);
351
+ const personalAgentsDir = () => personalAgentsDirAtHome(options.homeDir);
352
+ const locateAgentFile: typeof locateAgentFileAtHome = (name, path, cwd, source) =>
353
+ locateAgentFileAtHome(name, path, cwd, source, options.homeDir);
335
354
  // Child AgentSessions load normal extensions. Re-entering this extension there
336
355
  // would create another manager and leak handlers. Nested orchestration is
337
356
  // injected as scoped custom tools by the existing manager instead.
@@ -383,11 +402,11 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
383
402
  });
384
403
 
385
404
  // Fuzzy model names ("opus") prefer what the user enabled in pi's settings.
386
- setModelScopeProvider(registry => resolveEnabledModels(readEnabledModels(process.cwd()), registry, process.cwd()));
405
+ setModelScopeProvider(registry => resolveEnabledModels(readEnabledModels(process.cwd(), options.homeDir), registry, process.cwd(), options.homeDir));
387
406
 
388
407
  // Read directly rather than waiting for applyAndEmitLoaded below: these decide
389
408
  // the initial load, which happens hundreds of lines before settings are applied.
390
- const bootSettings = loadSettings(process.cwd());
409
+ const bootSettings = loadSettings(process.cwd(), options.homeDir);
391
410
  let strictAgentFiles = bootSettings.strictAgentFiles === true;
392
411
  // Strict agent files fail a bind by throwing the host's `ExtensionStartupError`
393
412
  // from `session_start`. A host without that class would silently degrade to
@@ -412,7 +431,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
412
431
 
413
432
  /** Reload agents from package/global/project agent dirs and merge with defaults (called on init and each Agent invocation). */
414
433
  const reloadCustomAgents = (strict = false) => {
415
- const userAgents = loadCustomAgents(process.cwd(), strict);
434
+ const userAgents = loadCustomAgents(process.cwd(), strict, { homeDir: options.homeDir });
416
435
  registerAgents(userAgents);
417
436
  };
418
437
 
@@ -571,6 +590,8 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
571
590
  durationMs,
572
591
  tokens,
573
592
  usage,
593
+ // Present only on a fallback, so the payload keeps its shape otherwise.
594
+ ...fallbackFields(record),
574
595
  };
575
596
  }
576
597
 
@@ -580,6 +601,26 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
580
601
  */
581
602
  let detachedSessionFile: string | undefined;
582
603
 
604
+ /** `modelFallback` + `modelNote` event fields, or nothing. Only what is KNOWN when the event fires. */
605
+ function fallbackFields(record: AgentRecord | undefined): Record<string, unknown> {
606
+ return {
607
+ ...(record?.modelTier ? { modelTier: record.modelTier } : {}),
608
+ ...(record?.modelFallback ? { modelFallback: record.modelFallback } : {}),
609
+ ...(modelSelectionNote(record) ? { modelNote: modelSelectionNote(record) } : {}),
610
+ };
611
+ }
612
+
613
+ /** The `subagents:record` history entry; the ONE builder, shared by live completion and the detached spool. */
614
+ function historyRecord(record: AgentRecord): Record<string, unknown> {
615
+ return {
616
+ id: record.id, type: record.type, description: record.description,
617
+ status: record.status, result: record.result, error: record.error,
618
+ startedAt: record.startedAt, completedAt: record.completedAt,
619
+ ...(record.modelFallback ? { modelFallback: record.modelFallback } : {}),
620
+ ...(record.modelTier ? { modelTier: record.modelTier } : {}),
621
+ };
622
+ }
623
+
583
624
  function spoolDetachedResult(sessionFile: string, record: AgentRecord) {
584
625
  if (record.resultConsumed) return;
585
626
  const footer = record.outputFile ? `\nFull transcript available at: ${record.outputFile}` : "";
@@ -588,11 +629,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
588
629
  id: record.id,
589
630
  content: formatTaskNotification(record, 500, showCost) + footer,
590
631
  details: buildNotificationDetails(record, 500),
591
- record: {
592
- id: record.id, type: record.type, description: record.description,
593
- status: record.status, result: record.result, error: record.error,
594
- startedAt: record.startedAt, completedAt: record.completedAt,
595
- },
632
+ record: historyRecord(record),
596
633
  });
597
634
  } catch { /* spool unwritable: the transcript file still has the result */ }
598
635
  }
@@ -604,7 +641,11 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
604
641
 
605
642
  function deliverDetachedResult(result: DetachedResult) {
606
643
  try {
607
- if (result.record) pi.appendEntry("subagents:record", result.record);
644
+ if (result.record) {
645
+ pi.appendEntry("subagents:record", result.record);
646
+ fleet.ingestFailure(result.record);
647
+ fleet.update();
648
+ }
608
649
  const details = result.details as NotificationDetails;
609
650
  outbox.add({
610
651
  ids: [details.id, ...(details.others?.map(other => other.id) ?? [])],
@@ -631,6 +672,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
631
672
  return;
632
673
  }
633
674
 
675
+ if (!record.resultConsumed) fleet.ingestFailure(record);
634
676
  // Emit lifecycle event based on terminal status
635
677
  const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
636
678
  const eventData = buildEventData(record);
@@ -641,11 +683,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
641
683
  }
642
684
 
643
685
  // Persist final record for cross-extension history reconstruction
644
- pi.appendEntry("subagents:record", {
645
- id: record.id, type: record.type, description: record.description,
646
- status: record.status, result: record.result, error: record.error,
647
- startedAt: record.startedAt, completedAt: record.completedAt,
648
- });
686
+ pi.appendEntry("subagents:record", historyRecord(record));
649
687
 
650
688
  // Skip notification if result was already consumed via get_subagent_result
651
689
  if (record.resultConsumed) {
@@ -685,6 +723,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
685
723
  id: record.id,
686
724
  type: record.type,
687
725
  description: record.description,
726
+ ...fallbackFields(record),
688
727
  });
689
728
  }, (record, info) => {
690
729
  if (!isTopLevelAgent(record)) return;
@@ -696,6 +735,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
696
735
  reason: info.reason,
697
736
  tokensBefore: info.tokensBefore,
698
737
  compactionCount: record.compactionCount,
738
+ ...fallbackFields(record),
699
739
  });
700
740
  }, (record, usage) => {
701
741
  // Every assistant message from every agent — nested included, exactly once.
@@ -731,6 +771,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
731
771
  // clamped it to the model, or an agent file outranked it), so a ledger
732
772
  // can show `low (asked minimal)` instead of silently relabelling.
733
773
  ...(invocation?.requestedThinking !== undefined ? { requestedThinking: invocation.requestedThinking } : {}),
774
+ ...fallbackFields(record),
734
775
  depth: record.depth ?? 1,
735
776
  ...(record.parentAgentId !== undefined ? { parentAgentId: record.parentAgentId } : {}),
736
777
  ...(record.workflowId !== undefined ? { workflowId: record.workflowId } : {}),
@@ -739,7 +780,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
739
780
  }, (record, message) => {
740
781
  if (!isTopLevelAgent(record)) return;
741
782
  pi.events.emit("subagents:steered", { id: record.id, message });
742
- }, clock);
783
+ }, clock, options.homeDir);
743
784
 
744
785
  // Expose manager via Symbol.for() global registry for cross-package access.
745
786
  // Standard Node.js pattern for cross-package singletons (used by OpenTelemetry, etc.).
@@ -814,6 +855,13 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
814
855
  // awaits nothing. A forged `blocking` would charge it to the foreground
815
856
  // pool and could defer it behind a queue whose gate nobody is holding.
816
857
  delete safeOptions.blocking;
858
+ // The spawn path's own disclosure; a caller-supplied one could hide a
859
+ // fallback or invent one. The runner reports the real outcome itself.
860
+ delete safeOptions.modelFallback;
861
+ delete safeOptions.modelSelection;
862
+ delete safeOptions.modelTier;
863
+ delete safeOptions.previousModelTier;
864
+ delete safeOptions.previousModelFallback;
817
865
  return spawnResolved(piRef, ctxRef, type, prompt, safeOptions);
818
866
  };
819
867
 
@@ -847,6 +895,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
847
895
 
848
896
  // --- Cross-extension RPC via pi.events ---
849
897
  let currentCtx: ExtensionContext | undefined;
898
+ let unwatchFleetSchedules: (() => void) | undefined;
850
899
  // RPC handlers + the `subagents:ready` broadcast are wired on `session_start`
851
900
  // (a bound lifecycle event), not at factory time. pi runs every extension
852
901
  // factory before the `extensions:` filter and only fires lifecycle events for
@@ -862,7 +911,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
862
911
  // Session-scoped: store is constructed inside session_start once sessionId
863
912
  // is available. Mirrors pi-chonky-tasks's session-scoped task store —
864
913
  // schedules reset on /new, restore on /resume.
865
- const scheduler = new SubagentScheduler({ clock: options.clock });
914
+ const scheduler = new SubagentScheduler({ clock: options.clock, homeDir: options.homeDir });
866
915
 
867
916
  function startScheduler(ctx: ExtensionContext) {
868
917
  try {
@@ -884,6 +933,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
884
933
  // bound session_start, so a filtered-out activation never advertises (#142).
885
934
  pi.on("session_start", async (_event, ctx) => {
886
935
  currentCtx = ctx;
936
+ restoreFleetDisclosure(ctx);
887
937
  // Pi resolves project trust before the first session starts, so this is the
888
938
  // earliest honest answer. The activation-time load defaulted to untrusted,
889
939
  // so a trusted project's `packages[]` only becomes visible now.
@@ -911,6 +961,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
911
961
  widget.setUICtx(ctx.ui);
912
962
  fleet.setUICtx(ctx.ui as any);
913
963
  watchHandoffKey(ctx.ui);
964
+ unwatchFleetSchedules ??= pi.events.on("subagents:scheduled", () => fleet.update());
914
965
  }
915
966
  manager.clearCompleted(true);
916
967
  const sessionFile = ctx.sessionManager?.getSessionFile?.();
@@ -925,6 +976,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
925
976
  // fires once per activation, but a double-bind must not leak listeners.
926
977
  if (!rpcHandle) {
927
978
  rpcHandle = registerRpcHandlers({
979
+ homeDir: options.homeDir,
928
980
  events: pi.events,
929
981
  pi,
930
982
  getCtx: () => currentCtx,
@@ -944,6 +996,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
944
996
  if (!record || record.parentAgentId) return false;
945
997
  if (record.status === "running" || record.status === "queued") return false;
946
998
  markConsumed(record, clock.now());
999
+ fleet.acknowledgeFailure(record.id);
947
1000
  outbox.cancel(record.id);
948
1001
  return true;
949
1002
  },
@@ -956,6 +1009,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
956
1009
  pi.events.emit("subagents:ready", {});
957
1010
  }
958
1011
  if (isSchedulingEnabled() && !scheduler.isActive()) startScheduler(ctx);
1012
+ fleet.update(); // restored quota-parked jobs have no agent record
959
1013
  // Stack `@handle` suggestions on pi's built-in autocomplete. Registered at
960
1014
  // most once per activation: pi appends wrappers to a list it never prunes,
961
1015
  // so a second call would layer a duplicate provider on the first. TUI only
@@ -1116,6 +1170,9 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
1116
1170
  description: entry.description,
1117
1171
  reclaim: { handle: entry.handle, alias: entry.alias },
1118
1172
  resumeSessionFile: entry.sessionFile,
1173
+ // Historical disclosures never replace the continuation's current selection.
1174
+ ...(entry.modelFallback ? { previousModelFallback: entry.modelFallback } : {}),
1175
+ ...(entry.modelTier ? { previousModelTier: entry.modelTier } : {}),
1119
1176
  isBackground: true,
1120
1177
  });
1121
1178
  // The agent may still be starting — wait, so a startup failure lands in
@@ -1169,7 +1226,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
1169
1226
  // Not awaited: the clone runs a full model turn, and prompt() is blocked
1170
1227
  // until this hook returns. The user gets their prompt back immediately
1171
1228
  // and the agent appears in the widget when it starts.
1172
- void runMentionClone({ ctx, type, message: mention.message, agentTool: registeredAgentTool })
1229
+ void runMentionClone({ ctx, type, message: mention.message, agentTool: registeredAgentTool, homeDir: options.homeDir })
1173
1230
  .then(async (result) => {
1174
1231
  if (result.spawned) return;
1175
1232
  // A clone that could not run must not swallow the mention: start the
@@ -1195,7 +1252,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
1195
1252
  }
1196
1253
 
1197
1254
  try {
1198
- // Nothing else to pass: runAgent resolves model, thinking, max turns and
1255
+ // Nothing else to pass: the manager resolves model; runAgent resolves thinking, max turns and
1199
1256
  // `isolated` from the agent's own config when the spawn omits them, and the
1200
1257
  // manager's onStart/onComplete callbacks own the widget, the fleet list
1201
1258
  // and the completion notification — the same contract the scheduler and
@@ -1227,6 +1284,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
1227
1284
  // to this session (see detached-results.ts). Only agents that cannot outlive the
1228
1285
  // session are aborted.
1229
1286
  pi.on("session_shutdown", async (event: { reason?: string }) => {
1287
+ fleet.flushState(); // appendEntry must run before this activation loses its context
1230
1288
  const sessionFile = currentCtx?.sessionManager?.getSessionFile?.();
1231
1289
  unwatchDetached?.();
1232
1290
  unwatchDetached = undefined;
@@ -1276,6 +1334,8 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
1276
1334
  widget.dispose();
1277
1335
  outbox.dispose();
1278
1336
  fleet.dispose();
1337
+ unwatchFleetSchedules?.();
1338
+ unwatchFleetSchedules = undefined;
1279
1339
  unsubHandoffKey?.();
1280
1340
  unsubHandoffKey = undefined;
1281
1341
  handoffKeyUi = undefined;
@@ -1348,6 +1408,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
1348
1408
  type: record.type,
1349
1409
  description: record.description,
1350
1410
  isBackground: true,
1411
+ ...fallbackFields(record),
1351
1412
  });
1352
1413
  }
1353
1414
  }
@@ -1412,6 +1473,17 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
1412
1473
  getFullscreenScrollbar, backgroundActionFor, isShowModelEnabled,
1413
1474
  // Agents the previous instance of this session left running (/reload, /new, ...).
1414
1475
  () => listRunningDetached(currentCtx?.sessionManager?.getSessionFile?.()), clock);
1476
+ fleet.setParkedSource(() => scheduler.list().filter(job => job.enabled && job.quotaParked && job.lastStatus !== "running").length);
1477
+ /** Replay UI-only state from the active branch, like workflow/goal custom snapshots. */
1478
+ function restoreFleetDisclosure(ctx: ExtensionContext): void {
1479
+ fleet.restoreSession(() => ctx.sessionManager?.getBranch?.() ?? [], state => {
1480
+ pi.appendEntry(FLEET_STATE_ENTRY, { ...state, version: FLEET_STATE_VERSION });
1481
+ });
1482
+ }
1483
+ pi.on("session_tree", (_event, ctx) => {
1484
+ restoreFleetDisclosure(ctx);
1485
+ fleet.update();
1486
+ });
1415
1487
  /** What the Agent tool row reads at render time, so its summary stays live. */
1416
1488
  const agentRowLive: AgentRowLive = {
1417
1489
  getRecord: (id) => manager.getRecord(id),
@@ -1584,6 +1656,9 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
1584
1656
  if (opts.outputTranscript) {
1585
1657
  existing.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
1586
1658
  ensureOutputFile(existing.outputFile);
1659
+ // A fresh transcript for a later resume must still say what the agent ran on.
1660
+ if (existing.modelFallback) appendModelFallbackEntry(existing.outputFile, id, existing.modelFallback);
1661
+ if (existing.modelTier) appendModelTierEntry(existing.outputFile, id, existing.modelTier);
1587
1662
  }
1588
1663
  // Anchor streaming past the turns already on disk, captured BEFORE the
1589
1664
  // run starts. The resumed prompt lands as an ordinary user message at
@@ -1642,6 +1717,7 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
1642
1717
  type: existing.type,
1643
1718
  description: existing.description,
1644
1719
  isBackground: true,
1720
+ ...fallbackFields(existing),
1645
1721
  });
1646
1722
 
1647
1723
  return record;
@@ -1740,6 +1816,8 @@ function activateSubagents(pi: ExtensionAPI, options: SubagentsExtensionOptions)
1740
1816
  setPackageWorkflows: setPackageWorkflowsGate,
1741
1817
  },
1742
1818
  (event, payload) => pi.events.emit(event, payload),
1819
+ process.cwd(),
1820
+ options.homeDir,
1743
1821
  );
1744
1822
 
1745
1823
  // ---- Agent tool ----
@@ -1822,7 +1900,7 @@ If the target is already known, use a direct tool — \`read\` for a known path,
1822
1900
  - Use steer_subagent to send mid-run messages to a running background agent.
1823
1901
  - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
1824
1902
  - If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
1825
- - Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
1903
+ - Use model to select a configured tier (e.g. "@cheap"), an exact "provider/modelId", or a fuzzy tool-call input (e.g. "haiku").
1826
1904
  - Use thinking to control extended thinking level.
1827
1905
  - Use inherit_context if the agent needs the parent conversation history.${isolationGuideline}${scheduleGuideline}
1828
1906
 
@@ -1924,7 +2002,7 @@ Terse command-style prompts produce shallow, generic work.
1924
2002
  model: Type.Optional(
1925
2003
  Type.String({
1926
2004
  description:
1927
- 'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
2005
+ 'Optional model override. Accepts a configured tier (e.g. "@cheap"), "provider/modelId", or fuzzy name (e.g. "haiku"). Explicit values win over agentModels and the agent definition.',
1928
2006
  }),
1929
2007
  ),
1930
2008
  thinking: Type.Optional(
@@ -2040,7 +2118,7 @@ Terse command-style prompts produce shallow, generic work.
2040
2118
  // foreground path. Resume deliberately doesn't: it replays the stored
2041
2119
  // session and ignores `subagent_type` entirely, so a note about type
2042
2120
  // substitution would be describing something that didn't happen.
2043
- const fallbackNote = dispatch.ok && dispatch.fellBackFrom !== undefined
2121
+ const typeNote = dispatch.ok && dispatch.fellBackFrom !== undefined
2044
2122
  ? `Note: Unknown agent type "${dispatch.fellBackFrom}" — using ${resolveType(subagentType) ? subagentType : "the fallback agent config"}.\n\n`
2045
2123
  : "";
2046
2124
 
@@ -2050,36 +2128,22 @@ Terse command-style prompts produce shallow, generic work.
2050
2128
  const customConfig = getAgentConfig(subagentType);
2051
2129
 
2052
2130
  const resolvedConfig = resolveAgentInvocationConfig(customConfig, params, {
2053
- agentType: subagentType,
2054
2131
  worktreeAllowed: isWorktreeIsolationEnabled(),
2055
2132
  defaultRunInBackground: getBackgroundByDefault(),
2056
2133
  });
2057
2134
 
2058
- // Resolve an explicit tool-call model first; agent config supplies only a default.
2059
- let model = ctx.model;
2060
- if (resolvedConfig.modelInput) {
2061
- const resolved = resolveModel(resolvedConfig.modelInput, ctx.modelRegistry);
2062
- if (typeof resolved === "string") {
2063
- if (resolvedConfig.modelFromParams) return textResult(resolved);
2064
- // config-specified: silent fallback to parent
2065
- } else {
2066
- model = resolved;
2067
- }
2068
- }
2069
-
2070
- // Scope validation: the effective resolved model is checked against the
2071
- // user's enabledModels list. Policy (hard error vs warn-and-proceed) lives
2072
- // in model-scope.ts so the nested delegation tools apply the same rule.
2073
- const scopeVerdict = checkModelScope({
2074
- model,
2075
- cwd: ctx.cwd,
2076
- modelRegistry: ctx.modelRegistry,
2077
- callerSupplied: resolvedConfig.modelFromParams,
2078
- agentLabel: customConfig?.displayName ?? subagentType,
2079
- modelInput: resolvedConfig.modelInput,
2135
+ // A live resume keeps its record's selection, ignoring new model inputs.
2136
+ const selection = params.resume ? undefined : selectModel({
2137
+ homeDir: options.homeDir,
2138
+ agentType: subagentType, definitionModel: customConfig?.model, paramModel: params.model,
2139
+ parentModel: ctx.model, registry: ctx.modelRegistry, cwd: ctx.cwd, agentLabel: customConfig?.displayName,
2080
2140
  });
2081
- if (scopeVerdict.kind === "error") return textResult(scopeVerdict.message);
2082
- if (scopeVerdict.kind === "warn") ctx.ui.notify(scopeVerdict.message, "warning");
2141
+ if (typeof selection === "string") return { ...textResult(selection), isError: true };
2142
+ const tierBlock = selection?.tierBinding && !params.schedule ? await admitModelSelection(selection, ctx.modelRegistry, pi.events) : undefined;
2143
+ const model = selection?.model;
2144
+ const modelFallback = selection?.fallback;
2145
+
2146
+ if (selection?.scopeWarning) ctx.ui.notify(selection.scopeWarning, "warning");
2083
2147
 
2084
2148
  const thinking = resolvedConfig.thinking;
2085
2149
  const inheritContext = resolvedConfig.inheritContext;
@@ -2088,7 +2152,7 @@ Terse command-style prompts produce shallow, generic work.
2088
2152
  const isolation = resolvedConfig.isolation;
2089
2153
  // Undefined = the session cwd. The manager validates it (absolute,
2090
2154
  // existing directory) at spawn and again at start.
2091
- const cwd = resolveCwdParam(params.cwd, ctx.cwd);
2155
+ const cwd = resolveCwdParam(params.cwd, ctx.cwd, options.homeDir);
2092
2156
  // Whether this spawn writes its .output transcript. Per-agent
2093
2157
  // frontmatter (`output_transcript`) wins; otherwise the project/global
2094
2158
  // default applies. `attachTranscript` below is the SOLE gate — every
@@ -2106,9 +2170,7 @@ Terse command-style prompts produce shallow, generic work.
2106
2170
  // parent's model used to show the level with nothing to attach it to.
2107
2171
  // This is the pre-session snapshot — agent-manager overwrites it with the
2108
2172
  // effective values the moment a session reports them.
2109
- const { modelName, modelId } = model
2110
- ? isVirtualModel(model) ? describeVirtualModel(model) : describeModel(model)
2111
- : { modelName: undefined, modelId: undefined };
2173
+ const { modelName, modelId } = describeSelection(selection, ctx.modelRegistry);
2112
2174
  const effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? getDefaultMaxTurns());
2113
2175
  const agentInvocation: AgentInvocation = {
2114
2176
  modelName,
@@ -2126,11 +2188,33 @@ Terse command-style prompts produce shallow, generic work.
2126
2188
  const modeLabel = getPromptModeLabel(subagentType);
2127
2189
  const { tags: invocationTags } = buildInvocationTags(agentInvocation);
2128
2190
  const agentTags = modeLabel ? [modeLabel, ...invocationTags] : invocationTags;
2191
+ /**
2192
+ * The lead-in for a result: the type note, then the model note. The model
2193
+ * note is read off the record the shared spawn path created. Before a
2194
+ * record exists (scheduling) it is this call's own resolution, the only
2195
+ * one there is. Never recomputed for a resume, which runs on its old session.
2196
+ */
2197
+ const modelNoteOf = (rec: AgentRecord | undefined): string | undefined =>
2198
+ modelSelectionNote(rec ?? { modelFallback, modelTier: selection?.tier }) || undefined;
2199
+ const modelFallbackOf = (rec: AgentRecord | undefined): ModelFallback | undefined =>
2200
+ rec ? rec.modelFallback : modelFallback;
2201
+ /** A resume's lead-in: the record's note only (the type note describes a spawn, not a resume). */
2202
+ const resumeNoteFor = (rec: AgentRecord): string => {
2203
+ const note = modelNoteOf(rec);
2204
+ return note ? `${note}\n\n` : "";
2205
+ };
2206
+ const fallbackNoteFor = (rec: AgentRecord | undefined): string => {
2207
+ const modelNote = modelNoteOf(rec);
2208
+ return typeNote + (modelNote ? `${modelNote}\n\n` : "");
2209
+ };
2129
2210
  const detailBase = {
2130
2211
  displayName,
2131
2212
  description: params.description,
2132
2213
  subagentType,
2133
2214
  modelName,
2215
+ modelNote: modelNoteOf(undefined),
2216
+ modelFallback: modelFallbackOf(undefined),
2217
+ modelTier: selection?.tier ? tierDisclosure(selection.tier) : undefined,
2134
2218
  tags: agentTags.length > 0 ? agentTags : undefined,
2135
2219
  };
2136
2220
 
@@ -2148,7 +2232,10 @@ Terse command-style prompts produce shallow, generic work.
2148
2232
  * buildInvocationTags would silently drop `twin`.
2149
2233
  */
2150
2234
  const detailBaseFor = (rec: AgentRecord | undefined): typeof detailBase => {
2151
- if (!rec?.invocation) return detailBase;
2235
+ // A record's disclosure outranks this call's, even before its session
2236
+ // reports an invocation: resume reuses the old session, so this call's
2237
+ // config says nothing about what ran.
2238
+ if (!rec?.invocation) return rec ? { ...detailBase, modelNote: modelNoteOf(rec), modelFallback: modelFallbackOf(rec), modelTier: rec.modelTier } : detailBase;
2152
2239
  const type = rec.type;
2153
2240
  const { modelName: recModelName, tags } = buildInvocationTags(rec.invocation);
2154
2241
  const recModeLabel = getPromptModeLabel(type);
@@ -2158,6 +2245,9 @@ Terse command-style prompts produce shallow, generic work.
2158
2245
  description: rec.description,
2159
2246
  subagentType: type,
2160
2247
  modelName: recModelName,
2248
+ modelNote: modelNoteOf(rec),
2249
+ modelFallback: modelFallbackOf(rec),
2250
+ modelTier: rec.modelTier,
2161
2251
  tags: recTags.length > 0 ? recTags : undefined,
2162
2252
  };
2163
2253
  };
@@ -2191,7 +2281,7 @@ Terse command-style prompts produce shallow, generic work.
2191
2281
  // at fire time, and the original is what a user edits.
2192
2282
  subagent_type: requestedType,
2193
2283
  prompt: params.prompt as string,
2194
- model: params.model as string | undefined,
2284
+ model: selection?.requested?.startsWith("@") ? selection.requested : params.model as string | undefined,
2195
2285
  thinking: thinking,
2196
2286
  max_turns: effectiveMaxTurns,
2197
2287
  isolated: isolated,
@@ -2199,7 +2289,7 @@ Terse command-style prompts produce shallow, generic work.
2199
2289
  });
2200
2290
  const next = scheduler.getNextRun(job.id);
2201
2291
  return textResult(
2202
- `${fallbackNote}Scheduled "${job.name}" (id: ${job.id}, type: ${job.scheduleType}). ` +
2292
+ `${fallbackNoteFor(undefined)}Scheduled "${job.name}" (id: ${job.id}, type: ${job.scheduleType}). ` +
2203
2293
  `Next run: ${next ?? "(unknown)"}. ` +
2204
2294
  `Manage via /agents → Scheduled jobs.`,
2205
2295
  );
@@ -2252,7 +2342,7 @@ Terse command-style prompts produce shallow, generic work.
2252
2342
 
2253
2343
  const isQueued = record.status === "queued";
2254
2344
  return textResult(
2255
- `Agent ${isQueued ? "queued" : "resumed"} in background.\n` +
2345
+ `${resumeNoteFor(record)}Agent ${isQueued ? "queued" : "resumed"} in background.\n` +
2256
2346
  `Agent ID: ${id}\n` +
2257
2347
  `Type: ${existing.type}\n` +
2258
2348
  (record.outputFile ? `Output file: ${record.outputFile}\n` : "") +
@@ -2294,10 +2384,10 @@ Terse command-style prompts produce shallow, generic work.
2294
2384
  // A failed resume surfaces the error, plus any partial output THIS
2295
2385
  // resume produced (never the previous turn's answer, #144).
2296
2386
  if (record.status === "error") {
2297
- return textResult(`Agent failed: ${record.error}${partialOutputSuffix(record)}`, buildDetails(detailBaseFor(record), record, resumeState));
2387
+ return textResult(`${resumeNoteFor(record)}Agent failed: ${record.error}${partialOutputSuffix(record)}`, buildDetails(detailBaseFor(record), record, resumeState));
2298
2388
  }
2299
2389
  return textResult(
2300
- record.result?.trim() || "No output.",
2390
+ `${resumeNoteFor(record)}${record.result?.trim() || "No output."}`,
2301
2391
  buildDetails(detailBaseFor(record), record, resumeState),
2302
2392
  );
2303
2393
  }
@@ -2308,7 +2398,7 @@ Terse command-style prompts produce shallow, generic work.
2308
2398
  const backgroundResult = (id: string, record: AgentRecord | undefined) => {
2309
2399
  const isQueued = record?.status === "queued";
2310
2400
  return textResult(
2311
- `${fallbackNote}Agent ${isQueued ? "queued" : "started"} in background.\n` +
2401
+ `${fallbackNoteFor(record)}Agent ${isQueued ? "queued" : "started"} in background.\n` +
2312
2402
  `Agent ID: ${id}\n` +
2313
2403
  `Type: ${displayName}\n` +
2314
2404
  `Description: ${params.description}\n` +
@@ -2327,13 +2417,14 @@ Terse command-style prompts produce shallow, generic work.
2327
2417
  // exhaustion policy says fail now (`wait: false`). Anything else fails
2328
2418
  // now, with the reset, rather than starting on an exhausted provider.
2329
2419
  const dispatchModel = model ?? ctx.model;
2330
- const quotaBlock = dispatchModel ? await checkAdmission(pi.events, modelRef(dispatchModel), undefined, clock) : undefined;
2420
+ const quotaBlock = selection?.tierBinding ? tierBlock
2421
+ : dispatchModel ? await checkAdmission(pi.events, modelRef(dispatchModel), undefined, clock) : undefined;
2331
2422
  if (quotaBlock) {
2332
2423
  // A parked job is a scheduled job, which has no `cwd` to carry.
2333
2424
  const canPark = runInBackground && !inheritContext && cwd === undefined && mayPark(quotaBlock)
2334
2425
  && isSchedulingEnabled() && scheduler.isActive();
2335
2426
  if (!canPark || !mayPark(quotaBlock)) {
2336
- throw new Error(`${describeBlock(quotaBlock)} The agent was not started.`);
2427
+ throw new Error(`${selection?.tier ? `${modelNoteOf(undefined)}\n` : ""}${describeBlock(quotaBlock)} The agent was not started.`);
2337
2428
  }
2338
2429
  const job = scheduler.addJob({
2339
2430
  name: `${params.description} (quota ${nanoid(4)})`,
@@ -2341,7 +2432,9 @@ Terse command-style prompts produce shallow, generic work.
2341
2432
  schedule: new Date(quotaBlock.resetAt + QUOTA_RESTART_GRACE_MS).toISOString(),
2342
2433
  subagent_type: requestedType,
2343
2434
  prompt: params.prompt as string,
2344
- model: dispatchModel ? modelRef(dispatchModel) : undefined,
2435
+ // Keep the request, not today's configured/parent pick. Fire-time
2436
+ // selection must still apply agentModels and aliases.
2437
+ model: selection?.requested?.startsWith("@") ? selection.requested : params.model,
2345
2438
  thinking,
2346
2439
  max_turns: effectiveMaxTurns,
2347
2440
  isolated,
@@ -2349,7 +2442,7 @@ Terse command-style prompts produce shallow, generic work.
2349
2442
  quotaParked: true,
2350
2443
  });
2351
2444
  return textResult(
2352
- `${fallbackNote}${describeBlock(quotaBlock)} Queued "${job.name}" (id: ${job.id}) to start at ` +
2445
+ `${fallbackNoteFor(undefined)}${describeBlock(quotaBlock)} Queued "${job.name}" (id: ${job.id}) to start at ` +
2353
2446
  `${scheduler.getNextRun(job.id) ?? job.schedule}. Manage via /agents → Scheduled jobs.`,
2354
2447
  );
2355
2448
  }
@@ -2375,6 +2468,7 @@ Terse command-style prompts produce shallow, generic work.
2375
2468
  // tool call failed, whereas a returned message reads to the model as a
2376
2469
  // subagent that ran and reported this (#179).
2377
2470
  id = manager.spawn(pi, ctx, subagentType, params.prompt, {
2471
+ modelSelection: selection,
2378
2472
  description: params.description,
2379
2473
  name: params.name as string | undefined,
2380
2474
  model,
@@ -2420,6 +2514,7 @@ Terse command-style prompts produce shallow, generic work.
2420
2514
  type: subagentType,
2421
2515
  description: params.description,
2422
2516
  isBackground: true,
2517
+ ...fallbackFields(manager.getRecord(id)),
2423
2518
  });
2424
2519
 
2425
2520
  return backgroundResult(id, record);
@@ -2528,6 +2623,7 @@ Terse command-style prompts produce shallow, generic work.
2528
2623
  let fgResult: { record: AgentRecord } | undefined;
2529
2624
  try {
2530
2625
  fgResult = await Promise.race([manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
2626
+ modelSelection: selection,
2531
2627
  description: params.description,
2532
2628
  name: params.name as string | undefined,
2533
2629
  model,
@@ -2591,7 +2687,7 @@ Terse command-style prompts produce shallow, generic work.
2591
2687
 
2592
2688
  if (record.status === "error") {
2593
2689
  // Error headline + any partial output the run produced before failing.
2594
- return textResult(`${fallbackNote}Agent failed: ${record.error}${partialOutputSuffix(record)}`, details);
2690
+ return textResult(`${fallbackNoteFor(record)}Agent failed: ${record.error}${partialOutputSuffix(record)}`, details);
2595
2691
  }
2596
2692
 
2597
2693
  const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
@@ -2602,7 +2698,7 @@ Terse command-style prompts produce shallow, generic work.
2602
2698
  if (costText) statsParts.push(costText);
2603
2699
  }
2604
2700
  return textResult(
2605
- `${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getForegroundOutcomeNote(record.status)}.\n\n` +
2701
+ `${fallbackNoteFor(record)}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getForegroundOutcomeNote(record.status)}.\n\n` +
2606
2702
  (record.result?.trim() || "No output."),
2607
2703
  details,
2608
2704
  );
@@ -2692,6 +2788,7 @@ Terse command-style prompts produce shallow, generic work.
2692
2788
  startedAt: task.startTime,
2693
2789
  ...(task.endTime !== undefined ? { completedAt: task.endTime } : {}),
2694
2790
  tokens: task.totalTokens,
2791
+ observationKey: task,
2695
2792
  }));
2696
2793
  }
2697
2794
 
@@ -2708,6 +2805,7 @@ Terse command-style prompts produce shallow, generic work.
2708
2805
  args: task.args,
2709
2806
  signal: task.abortController.signal,
2710
2807
  host: createWorkflowHost({
2808
+ homeDir: options.homeDir,
2711
2809
  pi,
2712
2810
  ctx,
2713
2811
  manager,
@@ -2865,6 +2963,7 @@ Terse command-style prompts produce shallow, generic work.
2865
2963
  ? { scriptPath: resumeFrom.scriptPath }
2866
2964
  : params,
2867
2965
  ctx.cwd,
2966
+ options.homeDir,
2868
2967
  );
2869
2968
  if (!resolved.ok) return textResult(resolved.message);
2870
2969
 
@@ -3150,7 +3249,9 @@ Terse command-style prompts produce shallow, generic work.
3150
3249
  let output =
3151
3250
  `Agent: ${record.id}\n` +
3152
3251
  `Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
3153
- `Description: ${record.description}\n\n`;
3252
+ `Description: ${record.description}\n` +
3253
+ (modelSelectionNote(record) ? `${modelSelectionNote(record)}\n` : "") +
3254
+ `\n`;
3154
3255
 
3155
3256
  if (record.status === "running") {
3156
3257
  output += "Agent is still running. Use wait: true or check back later.";
@@ -3163,6 +3264,7 @@ Terse command-style prompts produce shallow, generic work.
3163
3264
  // Mark result as consumed — suppresses the completion notification
3164
3265
  if (record.status !== "running" && record.status !== "queued") {
3165
3266
  markConsumed(record, clock.now());
3267
+ fleet.acknowledgeFailure(record.id);
3166
3268
  outbox.cancel(record.id);
3167
3269
  }
3168
3270
 
@@ -3245,19 +3347,21 @@ Terse command-style prompts produce shallow, generic work.
3245
3347
 
3246
3348
  function getModelLabel(type: string, registry?: ModelRegistry): string {
3247
3349
  const cfg = getAgentConfig(type);
3248
- if (!cfg?.model) return "inherit"; // no model configured → really inherits parent
3249
- const label = getModelLabelFromConfig(cfg.model);
3350
+ const input = modelRequest(type, cfg?.model).requested;
3351
+ if (!input) return "inherit";
3352
+ const label = getModelLabelFromConfig(input);
3250
3353
  if (!registry) return label;
3251
- const resolved = resolveModel(cfg.model, registry);
3252
- // Configured but unresolvable: the runtime silently falls back to the parent
3253
- // model, so flag it (and the fallback) rather than hiding the config.
3254
- if (typeof resolved === "string") return `${label} (unavailable, fallback: inherit)`;
3354
+ const selection = selectModel({ homeDir: options.homeDir, agentType: type, definitionModel: cfg?.model, parentModel: undefined, registry, cwd: currentCtx?.cwd ?? process.cwd() });
3355
+ if (typeof selection === "string" || selection.outcome === "fallback" || !selection.model) {
3356
+ return `${label} (unavailable, fallback: inherit)`;
3357
+ }
3358
+ const resolved = selection.model;
3255
3359
  // Surface what it actually resolved to when that differs from the config —
3256
3360
  // e.g. a provider fallback or a looser version pin. Cosmetic separator/date
3257
3361
  // differences are normalized away so an effectively-identical match stays quiet.
3258
3362
  const resolvedFull = `${resolved.provider}/${resolved.id}`;
3259
3363
  const norm = (s: string) => s.toLowerCase().replace(/\./g, "-").replace(/-\d{8}$/, "");
3260
- if (norm(cfg.model) === norm(resolvedFull)) return label;
3364
+ if (norm(input) === norm(resolvedFull)) return label;
3261
3365
  return `${label} (→ ${resolvedFull.replace(/-\d{8}$/, "")})`;
3262
3366
  }
3263
3367
 
@@ -3337,7 +3441,7 @@ Terse command-style prompts produce shallow, generic work.
3337
3441
  */
3338
3442
  function packageNameOf(cfg: AgentConfig | undefined): string | undefined {
3339
3443
  if (cfg?.source !== "package" || !cfg.sourcePath) return undefined;
3340
- return packageNameForPath(cfg.sourcePath, process.cwd());
3444
+ return packageNameForPath(cfg.sourcePath, process.cwd(), "agents", { homeDir: options.homeDir });
3341
3445
  }
3342
3446
 
3343
3447
  async function showAllAgentsList(ctx: ExtensionCommandContext) {
@@ -3391,8 +3495,9 @@ Terse command-style prompts produce shallow, generic work.
3391
3495
  };
3392
3496
  });
3393
3497
 
3394
- if (selected && getAgentConfig(selected)) {
3395
- await showAgentDetail(ctx, selected);
3498
+ const cfg = selected ? getAgentConfig(selected) : undefined;
3499
+ if (selected && cfg) {
3500
+ await showAgentDetail(ctx, selected, cfg);
3396
3501
  await showAllAgentsList(ctx);
3397
3502
  }
3398
3503
  }
@@ -3446,13 +3551,7 @@ Terse command-style prompts produce shallow, generic work.
3446
3551
  );
3447
3552
  }
3448
3553
 
3449
- async function showAgentDetail(ctx: ExtensionCommandContext, name: string) {
3450
- const cfg = getAgentConfig(name);
3451
- if (!cfg) {
3452
- ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
3453
- return;
3454
- }
3455
-
3554
+ async function showAgentDetail(ctx: ExtensionCommandContext, name: string, cfg: AgentConfig) {
3456
3555
  const isPackage = cfg.source === "package";
3457
3556
  const file = locateAgentFile(name, cfg.sourcePath, undefined, cfg.source);
3458
3557
  const isDefault = cfg.isDefault === true;
@@ -3862,6 +3961,8 @@ Write the file using the write tool. Only write the file, nothing else.`;
3862
3961
  maxActiveSubagents: undefined,
3863
3962
  nestedSubagentReserve: undefined,
3864
3963
  modelAliases: undefined,
3964
+ modelTiers: undefined,
3965
+ modelResolution: undefined,
3865
3966
  agentModels: undefined,
3866
3967
  reportUsage: isReportUsageEnabled(),
3867
3968
  showCost: isShowCostEnabled(),