@selesai/code 0.13.2 → 0.13.4

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 (120) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/defaults/models.json +85 -13
  3. package/dist/extensions/cost-reconcile.test.ts +200 -4
  4. package/dist/extensions/cost-reconcile.ts +88 -102
  5. package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
  6. package/dist/extensions/pi-intercom/README.md +4 -5
  7. package/dist/extensions/pi-intercom/config.test.ts +3 -31
  8. package/dist/extensions/pi-intercom/config.ts +0 -15
  9. package/dist/extensions/pi-intercom/index.ts +9 -46
  10. package/dist/extensions/pi-intercom/intercom.integration.test.ts +49 -57
  11. package/dist/extensions/pi-intercom/package.json +1 -1
  12. package/dist/extensions/pi-intercom/reply-tracker.test.ts +20 -0
  13. package/dist/extensions/pi-intercom/reply-tracker.ts +8 -0
  14. package/dist/extensions/pi-subagents/CHANGELOG.md +27 -0
  15. package/dist/extensions/pi-subagents/docs/tool-reference.md +4 -1
  16. package/dist/extensions/pi-subagents/docs/workflows.md +2 -2
  17. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  18. package/dist/extensions/pi-subagents/package.json +1 -1
  19. package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +48 -243
  20. package/dist/extensions/pi-subagents/skills/council-mode/references/pass-contracts.md +150 -0
  21. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +87 -37
  22. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  23. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +49 -8
  24. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  25. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +34 -27
  27. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +73 -0
  28. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +157 -28
  29. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
  30. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  31. package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -0
  32. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +4 -1
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +3 -0
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +3 -2
  37. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +13 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +5 -1
  39. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +10 -2
  40. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +3 -0
  41. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +11 -2
  42. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +98 -1
  43. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +138 -4
  44. package/dist/extensions/pi-subagents/src/runs/shared/background-process-options.ts +9 -0
  45. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
  46. package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +52 -3
  47. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +47 -1
  48. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +45 -18
  49. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +21 -2
  50. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +15 -0
  51. package/dist/extensions/pi-subagents/src/shared/types.ts +34 -1
  52. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +11 -3
  53. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +31 -0
  54. package/dist/extensions/pi-subagents/src/tui/render.ts +597 -112
  55. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +40 -1
  56. package/dist/extensions/pi-subagents/src/workflows/host-command.ts +6 -1
  57. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +53 -2
  58. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +55 -6
  59. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +111 -1
  60. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +206 -38
  61. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +522 -31
  62. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +123 -0
  63. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +48 -0
  64. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +57 -1
  65. package/dist/extensions/pi-subagents/test/unit/background-process-options.test.ts +17 -0
  66. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +1 -1
  67. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +44 -4
  68. package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +91 -0
  69. package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +1 -0
  70. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +0 -1
  71. package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +20 -3
  72. package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +27 -0
  73. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +93 -17
  74. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +1 -0
  75. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +103 -26
  76. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +58 -0
  77. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +22 -2
  78. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +21 -0
  79. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +13 -0
  80. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +54 -0
  81. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +2 -0
  82. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +32 -0
  83. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +48 -1
  84. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +21 -6
  85. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +11 -0
  86. package/dist/extensions/pi-web-agent/CHANGELOG.md +410 -0
  87. package/dist/extensions/pi-web-agent/README.md +131 -0
  88. package/dist/extensions/pi-web-agent/package.json +4 -2
  89. package/dist/extensions/pi-web-agent/src/backends/config.ts +62 -5
  90. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  91. package/dist/extensions/pi-web-agent/src/backends/factory.ts +96 -6
  92. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +183 -47
  93. package/dist/extensions/pi-web-agent/src/extension.ts +62 -25
  94. package/dist/extensions/pi-web-agent/src/extract/readability.ts +19 -11
  95. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +5 -4
  96. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +2 -25
  97. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +5 -2
  98. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +2 -0
  99. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +72 -7
  100. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +8 -1
  101. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +28 -3
  102. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +4 -0
  103. package/dist/extensions/pi-web-agent/src/orchestration/url.ts +35 -0
  104. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +18 -6
  105. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +14 -2
  106. package/dist/extensions/pi-web-agent/src/readers/github-reader.ts +150 -0
  107. package/dist/extensions/pi-web-agent/src/readers/limits.ts +3 -0
  108. package/dist/extensions/pi-web-agent/src/readers/pdf-reader.ts +87 -0
  109. package/dist/extensions/pi-web-agent/src/readers/resolver.ts +25 -0
  110. package/dist/extensions/pi-web-agent/src/readers/types.ts +11 -0
  111. package/dist/extensions/pi-web-agent/src/readers/youtube-reader.ts +79 -0
  112. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +32 -5
  113. package/dist/extensions/pi-web-agent/src/search/exa.ts +109 -0
  114. package/dist/extensions/pi-web-agent/src/search/fanout.ts +154 -0
  115. package/dist/extensions/pi-web-agent/src/search/tavily.ts +113 -0
  116. package/dist/extensions/pi-web-agent/src/search/youcom.ts +109 -0
  117. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +22 -9
  118. package/dist/extensions/pi-web-agent/src/types.ts +19 -4
  119. package/dist/extensions/tokenin-onboarding.ts +302 -0
  120. package/package.json +1 -1
@@ -117,6 +117,7 @@ const FAST_MODE_ALLOWED_MODELS = new Set([
117
117
  "openai-codex/gpt-5.6-luna",
118
118
  "openai-codex/gpt-5.6-sol",
119
119
  ]);
120
+ const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
120
121
  export const SUBAGENT_CHILD_ENV = "SELESAI_SUBAGENT_CHILD";
121
122
  export const SUBAGENT_ORCHESTRATOR_TARGET_ENV =
122
123
  "SELESAI_SUBAGENT_ORCHESTRATOR_TARGET";
@@ -139,6 +140,7 @@ export const SUBAGENT_PARENT_PATH_ENV = "SELESAI_SUBAGENT_PARENT_PATH";
139
140
  export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV =
140
141
  "SELESAI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
141
142
  export const SUBAGENT_PARENT_SESSION_ENV = "SELESAI_SUBAGENT_PARENT_SESSION";
143
+ export const SUBAGENT_FORK_CACHE_KEY_ENV = "SELESAI_SUBAGENT_FORK_CACHE_KEY";
142
144
  export const SUBAGENT_STEER_INBOX_ENV = "SELESAI_SUBAGENT_STEER_INBOX";
143
145
  export const SUBAGENT_STEER_CAPABILITY_ENV = "SELESAI_SUBAGENT_STEER_CAPABILITY";
144
146
  export const SUBAGENT_STEER_ACK_DIR_ENV = "SELESAI_SUBAGENT_STEER_ACK_DIR";
@@ -146,8 +148,16 @@ export const PI_INTERCOM_STABLE_ID_ENV = "PI_INTERCOM_STABLE_ID";
146
148
  export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
147
149
  export const SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV = "SELESAI_SUBAGENT_INHERIT_GLOBAL_CONTEXT";
148
150
 
151
+ export function deriveForkPromptCacheKey(parentSessionId: string | undefined): string | undefined {
152
+ const parent = parentSessionId?.trim();
153
+ if (!parent) return undefined;
154
+ const digest = createHash("sha256").update(parent).digest("hex").slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH - "pi-fork:".length);
155
+ return `pi-fork:${digest}`;
156
+ }
157
+
149
158
  export interface BuildPiArgsInput {
150
159
  parentSessionId?: string;
160
+ forkCacheKey?: string;
151
161
  baseArgs: string[];
152
162
  task: string;
153
163
  sessionEnabled: boolean;
@@ -507,8 +517,13 @@ export function resolvePiLaunchToolPlan(
507
517
  throw new Error(formatUnresolvedMcpDirectToolSelectors(mcpResolution.unresolvedSelectors));
508
518
  }
509
519
  const resolvedMcpSelections = mcpResolution.selections;
520
+ const resolvedMcpNames = new Set(resolvedMcpSelections.map((selection) => selection.name));
521
+ const legacyMcpNameCounts = countLegacyUnderscoreMcpToolNames(resolvedMcpSelections);
510
522
  const effectiveMcpSelections = resolvedMcpSelections.filter(
511
- (selection) => !allowedToolSet || allowedToolSet.has(selection.name),
523
+ (selection) =>
524
+ !allowedToolSet ||
525
+ allowedToolSet.has(selection.name) ||
526
+ isLegacyUnderscoreMcpToolAllowed(selection, allowedToolSet, resolvedMcpNames, legacyMcpNameCounts),
512
527
  );
513
528
  const effectiveMcpTools = effectiveMcpSelections.map(
514
529
  (selection) => selection.name,
@@ -663,6 +678,36 @@ export function resolvePiLaunchToolPlan(
663
678
  };
664
679
  }
665
680
 
681
+ // Capability ceilings persisted before #1685 may still name hyphenated MCP server prefixes with underscores.
682
+ function countLegacyUnderscoreMcpToolNames(selections: readonly ResolvedMcpDirectToolSelection[]): Map<string, number> {
683
+ const counts = new Map<string, number>();
684
+ for (const selection of selections) {
685
+ const legacyName = legacyUnderscoreMcpToolName(selection);
686
+ if (legacyName !== selection.name) counts.set(legacyName, (counts.get(legacyName) ?? 0) + 1);
687
+ }
688
+ return counts;
689
+ }
690
+
691
+ function isLegacyUnderscoreMcpToolAllowed(
692
+ selection: ResolvedMcpDirectToolSelection,
693
+ allowedToolSet: ReadonlySet<string>,
694
+ resolvedMcpNames: ReadonlySet<string>,
695
+ legacyMcpNameCounts: ReadonlyMap<string, number>,
696
+ ): boolean {
697
+ const legacyName = legacyUnderscoreMcpToolName(selection);
698
+ return legacyMcpNameCounts.get(legacyName) === 1 && !resolvedMcpNames.has(legacyName) && allowedToolSet.has(legacyName);
699
+ }
700
+
701
+ function legacyUnderscoreMcpToolName(selection: ResolvedMcpDirectToolSelection): string {
702
+ const slash = selection.selector.indexOf("/");
703
+ if (slash < 1) return selection.name;
704
+ const toolName = selection.selector.slice(slash + 1);
705
+ const suffix = `_${toolName}`;
706
+ if (!selection.name.endsWith(suffix)) return selection.name;
707
+ const prefix = selection.name.slice(0, -suffix.length);
708
+ return `${prefix.replace(/-/g, "_")}${suffix}`;
709
+ }
710
+
666
711
  /** Escape XML-significant characters in a string for safe attribute interpolation. */
667
712
  function escapeXmlAttr(value: string): string {
668
713
  return value
@@ -974,6 +1019,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
974
1019
 
975
1020
  env[SUBAGENT_PARENT_SESSION_ENV] =
976
1021
  input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
1022
+ env[SUBAGENT_FORK_CACHE_KEY_ENV] = input.forkCacheKey?.trim() || undefined;
977
1023
 
978
1024
  return {
979
1025
  args,
@@ -8,6 +8,17 @@ export interface SingleOutputSnapshot {
8
8
  exists: boolean;
9
9
  mtimeMs?: number;
10
10
  size?: number;
11
+ error?: string;
12
+ }
13
+
14
+ function missingFileStatError(error: unknown): boolean {
15
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
16
+ return code === "ENOENT" || code === "ENOTDIR";
17
+ }
18
+
19
+ function snapshotFromStatError(error: unknown): SingleOutputSnapshot {
20
+ if (missingFileStatError(error)) return { exists: false };
21
+ return { exists: false, error: error instanceof Error ? error.message : String(error) };
11
22
  }
12
23
 
13
24
  /**
@@ -183,12 +194,36 @@ export function captureSingleOutputSnapshot(outputPath: string | undefined): Sin
183
194
  try {
184
195
  const stat = fs.statSync(outputPath);
185
196
  return { exists: true, mtimeMs: stat.mtimeMs, size: stat.size };
186
- } catch {
187
- // The snapshot is advisory; resolveSingleOutput reports concrete read/write failures.
188
- return { exists: false };
197
+ } catch (error) {
198
+ return snapshotFromStatError(error);
199
+ }
200
+ }
201
+
202
+ function inspectSingleOutputChange(
203
+ outputPath: string,
204
+ beforeRun: SingleOutputSnapshot | undefined,
205
+ ): { changed: boolean; error?: string } {
206
+ if (beforeRun?.error) return { changed: false, error: beforeRun.error };
207
+ try {
208
+ const stat = fs.statSync(outputPath);
209
+ return {
210
+ changed: !beforeRun?.exists || stat.mtimeMs !== beforeRun.mtimeMs || stat.size !== beforeRun.size,
211
+ };
212
+ } catch (error) {
213
+ if (missingFileStatError(error)) return { changed: false };
214
+ return { changed: false, error: error instanceof Error ? error.message : String(error) };
189
215
  }
190
216
  }
191
217
 
218
+ export function hasSingleOutputChangedSinceSnapshot(
219
+ outputPath: string | undefined,
220
+ beforeRun: SingleOutputSnapshot | undefined,
221
+ ): boolean | undefined {
222
+ if (!outputPath) return undefined;
223
+ const inspected = inspectSingleOutputChange(outputPath, beforeRun);
224
+ return inspected.error ? undefined : inspected.changed;
225
+ }
226
+
192
227
  function persistSingleOutput(
193
228
  outputPath: string | undefined,
194
229
  fullOutput: string,
@@ -216,23 +251,15 @@ export function resolveSingleOutput(
216
251
  const claimError = outputClaimError(outputPath, expectedClaimPath);
217
252
  if (claimError) return { fullOutput: fallbackOutput, saveError: claimError, fatalError: true };
218
253
 
219
- let changedSinceStart = false;
220
- try {
221
- const stat = fs.statSync(outputPath);
222
- changedSinceStart = !beforeRun?.exists
223
- || stat.mtimeMs !== beforeRun.mtimeMs
224
- || stat.size !== beforeRun.size;
225
- } catch (error) {
226
- const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
227
- if (code !== "ENOENT" && code !== "ENOTDIR") {
228
- return {
229
- fullOutput: fallbackOutput,
230
- saveError: `Failed to inspect output file: ${error instanceof Error ? error.message : String(error)}`,
231
- };
232
- }
254
+ const changedSinceStart = inspectSingleOutputChange(outputPath, beforeRun);
255
+ if (changedSinceStart.error) {
256
+ return {
257
+ fullOutput: fallbackOutput,
258
+ saveError: `Failed to inspect output file: ${changedSinceStart.error}`,
259
+ };
233
260
  }
234
261
 
235
- if (changedSinceStart) {
262
+ if (changedSinceStart.changed) {
236
263
  try {
237
264
  return { fullOutput: fs.readFileSync(outputPath, "utf-8"), savedPath: outputPath };
238
265
  } catch (error) {
@@ -1,12 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import type { ExtensionAPI, ExtensionContext } from "@selesai/code";
4
+ import type { BeforeProviderRequestEvent, ExtensionAPI, ExtensionContext } from "@selesai/code";
5
5
  import { registerNativeSupervisorClient } from "../../intercom/native-supervisor-channel.ts";
6
6
  import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
7
7
  import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, PERMISSION_POLICY_ENV } from "./permissions.ts";
8
8
  import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
9
- import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
9
+ import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_FORK_CACHE_KEY_ENV, SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
10
10
  import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
11
11
  import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
12
12
  import {
@@ -296,6 +296,23 @@ const COMPOSITE_TOOL_ID_APIS = new Set([
296
296
  "openai-responses",
297
297
  ]);
298
298
 
299
+ const PROMPT_CACHE_KEY_APIS = new Set([
300
+ "azure-openai-responses",
301
+ "openai-codex-responses",
302
+ "openai-completions",
303
+ "openai-responses",
304
+ ]);
305
+
306
+ export function rewriteForkCacheProviderRequest(event: BeforeProviderRequestEvent, ctx?: Pick<ExtensionContext, "model">): unknown {
307
+ const forkCacheKey = process.env[SUBAGENT_FORK_CACHE_KEY_ENV]?.trim();
308
+ if (!forkCacheKey || !PROMPT_CACHE_KEY_APIS.has(ctx?.model?.api ?? "")) return undefined;
309
+ if (!event || typeof event !== "object") return undefined;
310
+ const payload = event.payload;
311
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
312
+ if (typeof (payload as { prompt_cache_key?: unknown }).prompt_cache_key !== "string") return undefined;
313
+ return { ...payload, prompt_cache_key: forkCacheKey };
314
+ }
315
+
299
316
  function portableToolId(id: string): string {
300
317
  if (PORTABLE_TOOL_ID_PATTERN.test(id) && id.length <= MAX_PORTABLE_TOOL_ID_LENGTH) return id;
301
318
  const encoded = `tool_${Buffer.from(id).toString("base64url") || "empty"}`;
@@ -748,6 +765,8 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
748
765
  });
749
766
  }
750
767
 
768
+ onRuntimeEvent("before_provider_request", (event: unknown, ctx?: ExtensionContext) => rewriteForkCacheProviderRequest(event as BeforeProviderRequestEvent, ctx));
769
+
751
770
  onRuntimeEvent("context", (event: unknown, ctx?: ExtensionContext) => {
752
771
  if (!event || typeof event !== "object" || !("messages" in event) || !Array.isArray(event.messages)) return undefined;
753
772
  const messages = stripParentOnlySubagentMessages(event.messages, {
@@ -13,6 +13,21 @@ export interface WorkflowGraphBuildInput {
13
13
  dynamicGroupStatuses?: Record<number, { status: WorkflowNodeStatus; error?: string; acceptance?: SingleResult["acceptance"] }>;
14
14
  }
15
15
 
16
+ /** Return displayable workflow stages while hiding structural parallel groups and host monitors. */
17
+ export function workflowGraphStageNodes(graph: WorkflowGraphSnapshot | undefined): WorkflowGraphNode[] {
18
+ if (!graph?.nodes?.length) return [];
19
+ const stages: WorkflowGraphNode[] = [];
20
+ const visit = (node: WorkflowGraphNode): void => {
21
+ if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
22
+ for (const child of node.children ?? []) visit(child);
23
+ return;
24
+ }
25
+ if (node.kind !== "host-step") stages.push(node);
26
+ };
27
+ for (const node of graph.nodes) visit(node);
28
+ return stages;
29
+ }
30
+
16
31
  function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undefined {
17
32
  switch (status) {
18
33
  case "complete":
@@ -559,6 +559,10 @@ export interface TimeoutRecoverySummary {
559
559
  termination: "timed-out" | "stopped";
560
560
  changedFiles: string[];
561
561
  truncated?: boolean;
562
+ /** True only when a timed-out child left tracked changes without its requested report. */
563
+ recoveryNeeded?: boolean;
564
+ reason?: "timed-out-with-dirty-worktree";
565
+ reportStatus?: "missing" | "written" | "not-requested" | "unknown";
562
566
  currentTool?: string;
563
567
  currentToolArgs?: string;
564
568
  currentPath?: string;
@@ -569,6 +573,9 @@ export interface TimeoutRecoverySummary {
569
573
  message: string;
570
574
  }
571
575
 
576
+ /** Safe parent-facing subset of a timeout recovery summary. */
577
+ export type TimeoutRecoveryProjection = Pick<TimeoutRecoverySummary, "termination" | "changedFiles" | "truncated" | "recoveryNeeded" | "reason" | "reportStatus">;
578
+
572
579
  export const SUBAGENT_LIFECYCLE_ARTIFACT_VERSION = 3;
573
580
  export type SubagentLifecycleArtifactVersion = typeof SUBAGENT_LIFECYCLE_ARTIFACT_VERSION;
574
581
 
@@ -1272,6 +1279,7 @@ export interface WaitCompletionChild {
1272
1279
  model?: string;
1273
1280
  contextOverflow?: boolean;
1274
1281
  artifactPaths?: Partial<ArtifactPaths>;
1282
+ timeoutRecovery?: TimeoutRecoveryProjection;
1275
1283
  }
1276
1284
 
1277
1285
  /**
@@ -1290,6 +1298,27 @@ export interface WaitCompletion {
1290
1298
  workflowChildren?: WorkflowChildSummaryV1;
1291
1299
  }
1292
1300
 
1301
+ export interface AgentCapabilitiesSnapshot {
1302
+ agents: AgentCapabilityRow[];
1303
+ restrictedCount: number;
1304
+ capabilityCeilingSources?: string[];
1305
+ }
1306
+
1307
+ export interface AgentCapabilityRow {
1308
+ name: string;
1309
+ description: string;
1310
+ source: "builtin" | "package" | "user" | "project" | "runtime";
1311
+ executable: boolean;
1312
+ restrictionSources?: string[];
1313
+ aliases?: string[];
1314
+ runner: { type: "pi" } | { type: "external-cli"; adapter?: string; capabilities: ExternalCliCapabilities } | { type: "external-job"; provider: string; available?: boolean; capabilities: ExternalJobRunnerStatus["capabilities"] };
1315
+ tools: { ambient: boolean; names: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
1316
+ model?: { value?: string; fallbackModels?: string[]; thinking?: string | false };
1317
+ execution?: { defaultAsync?: boolean; timeoutMs?: number };
1318
+ output?: { path?: string; mode?: OutputMode };
1319
+ extensions?: { names?: string[]; subagentOnly?: string[]; skills?: string[] };
1320
+ }
1321
+
1293
1322
  export interface Details {
1294
1323
  mode: SubagentResultMode | "management";
1295
1324
  runId?: string;
@@ -1354,6 +1383,7 @@ export interface Details {
1354
1383
  activeAsyncCapacity?: ActiveAsyncCapacitySnapshot;
1355
1384
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1356
1385
  capabilityAudit?: SubagentCapabilityAudit;
1386
+ agentCapabilities?: AgentCapabilitiesSnapshot;
1357
1387
  parallelHandoff?: ParallelHandoffReference;
1358
1388
  lifecycleStatus?: {
1359
1389
  processTerminal?: ProcessTerminalV1;
@@ -1852,9 +1882,10 @@ export interface AsyncStatus {
1852
1882
  parallelHandoff?: ParallelHandoffReference;
1853
1883
  }
1854
1884
 
1855
- export type AsyncJobStep = NonNullable<AsyncStatus["steps"]>[number] & {
1885
+ export type AsyncJobStep = Omit<NonNullable<AsyncStatus["steps"]>[number], "timeoutRecovery"> & {
1856
1886
  index?: number;
1857
1887
  description?: string;
1888
+ timeoutRecovery?: TimeoutRecoveryProjection;
1858
1889
  };
1859
1890
 
1860
1891
  export interface AsyncJobState {
@@ -1889,6 +1920,8 @@ export interface AsyncJobState {
1889
1920
  parallelGroups?: AsyncParallelGroupStatus[];
1890
1921
  /** Bounded host-owned CI/gate nodes loaded from the workflow status graph. */
1891
1922
  hostSteps?: HostStepNodeV1[];
1923
+ /** Full bounded workflow plan, including not-yet-materialized stages. */
1924
+ workflowGraph?: WorkflowGraphSnapshot;
1892
1925
  steps?: AsyncJobStep[];
1893
1926
  preflight?: WorkflowPreflightV1;
1894
1927
  stepsTotal?: number;
@@ -4,6 +4,7 @@ import { snapshotExternalRuns } from "../api/external-runs.ts";
4
4
  import { formatModelThinking } from "../shared/formatters.ts";
5
5
  import type { AsyncJobState, AsyncJobStep, FleetViewPlacement, HerdrProjectPaneSnapshot, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentState } from "../shared/types.ts";
6
6
  import { projectAsyncWorkflowRows, type AsyncStatusWorkflowRow } from "../runs/shared/async-status-projection.ts";
7
+ import { contextModeLabel } from "../runs/shared/context-mode.ts";
7
8
  import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
8
9
  import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
9
10
  import { isStaleExtensionContextError } from "../shared/extension-context.ts";
@@ -362,7 +363,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
362
363
  if (job.mode === "workflow") {
363
364
  const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
364
365
  const workflowSteps = workflowStepsWithoutMaterializedChildren(job.steps, materializedChildrenByWorkflow.get(`async:${job.asyncId}`));
365
- const workflowRows = projectAsyncWorkflowRows(workflowSteps, job.hostSteps, job.preflight);
366
+ const workflowRows = projectAsyncWorkflowRows(workflowSteps, job.workflowGraph ?? job.hostSteps, job.preflight);
366
367
  entries.push({
367
368
  key: `async:${job.asyncId}`,
368
369
  ...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
@@ -550,7 +551,12 @@ export class SubagentFleetStatus {
550
551
  this.lastRenderKey = renderKey;
551
552
  return;
552
553
  }
553
- if (renderKey === this.lastRenderKey) return;
554
+ if (renderKey === this.lastRenderKey) {
555
+ // Repaint anyway while anything is running so the wall-clock
556
+ // spinner animates between state changes (500ms tick).
557
+ if (this.entries.some((entry) => entry.state === "running")) this.tui?.requestRender();
558
+ return;
559
+ }
554
560
  this.lastRenderKey = renderKey;
555
561
  this.tui?.requestRender();
556
562
  }
@@ -728,6 +734,7 @@ export class SubagentFleetStatus {
728
734
  const marker = last ? "└─" : "├─";
729
735
  const indent = " ";
730
736
  if (row.overflow !== undefined) return truncateToWidth(`${indent}${marker} ${theme.fg("dim", `+${row.overflow} hidden workflow steps`)}`, width);
737
+ const context = contextModeLabel(row.context);
731
738
  const modelThinking = row.modelThinking ? ` (${row.modelThinking})` : "";
732
739
  const activity = row.activity ? ` · ${row.activity}` : "";
733
740
  const kind = row.kind ? `${row.kind}: ` : "";
@@ -738,7 +745,7 @@ export class SubagentFleetStatus {
738
745
  row.preflight.expectedOutput ? `expected:${row.preflight.expectedOutput}` : undefined,
739
746
  row.preflight.independence ? `independence:${row.preflight.independence}` : undefined,
740
747
  ].filter((value): value is string => Boolean(value)).join(" · ") : "";
741
- const left = `${indent}${marker} ${this.workflowRowGlyph(row, theme)} ${theme.fg("muted", `${kind}${row.name}${modelThinking}`)} · ${this.workflowRowStateLabel(row, theme)}${activity}${hints ? ` · ${hints}` : ""}`;
748
+ const left = `${indent}${marker} ${this.workflowRowGlyph(row, theme)} ${theme.fg("muted", `${kind}${row.name}${context ? ` ${context}` : ""}${modelThinking}`)} · ${this.workflowRowStateLabel(row, theme)}${activity}${hints ? ` · ${hints}` : ""}`;
742
749
  const details = [
743
750
  row.startedAt !== undefined ? formatFleetElapsed(Date.now() - row.startedAt) : undefined,
744
751
  row.tokens !== undefined ? formatFleetTokens(row.tokens, row.window) : undefined,
@@ -806,6 +813,7 @@ export class SubagentFleetStatus {
806
813
  row.kind,
807
814
  row.name,
808
815
  row.state,
816
+ row.context,
809
817
  row.modelThinking,
810
818
  row.activity,
811
819
  row.startedAt,
@@ -30,6 +30,37 @@ export function fuzzyFilter<T extends { name: string; description: string; model
30
30
  .map((x) => x.item);
31
31
  }
32
32
 
33
+ /** Remove a repeated job name from a child label when a safe separator follows it. */
34
+ export function stripRepeatedAgentPrefix(label: string, jobName: string | undefined): string {
35
+ const value = label.trim();
36
+ const prefix = jobName?.trim();
37
+ if (!prefix || !value.startsWith(prefix)) return value;
38
+
39
+ const suffix = value.slice(prefix.length);
40
+ if (!suffix || !/^(?::|·|\s)/u.test(suffix)) return value;
41
+ return suffix.replace(/^\s*(?::|·)?\s*/u, "").trim();
42
+ }
43
+
44
+ /** Whether a status label may omit the one logical step marker. */
45
+ export function shouldSuppressSingleStep(chainStepCount?: number, stepsTotal?: number): boolean {
46
+ return (chainStepCount ?? stepsTotal) === 1;
47
+ }
48
+
49
+ /** Add an Agent fraction only when visible candidates collide. */
50
+ export function withDuplicateLabelDiscriminators<T extends { index: number; displayName: string }>(
51
+ rows: readonly T[],
52
+ total: number,
53
+ ): Array<T & { rowLabel: string }> {
54
+ const counts = new Map<string, number>();
55
+ for (const row of rows) counts.set(row.displayName, (counts.get(row.displayName) ?? 0) + 1);
56
+ return rows.map((row) => ({
57
+ ...row,
58
+ rowLabel: (counts.get(row.displayName) ?? 0) > 1
59
+ ? `Agent ${row.index + 1}/${total}: ${row.displayName}`
60
+ : row.displayName,
61
+ }));
62
+ }
63
+
33
64
  export function pad(s: string, len: number): string {
34
65
  const vis = visibleWidth(s);
35
66
  return s + " ".repeat(Math.max(0, len - vis));