pi-subagents 0.47.0 → 0.47.1

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.
@@ -53,6 +53,7 @@ import {
53
53
  } from "../../shared/settings.ts";
54
54
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
55
55
  import { buildAsyncRunnerSteps, DEFAULT_ASYNC_TIMEOUT_MS, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
56
+ import { updateActiveRunIndex } from "../background/active-run-index.ts";
56
57
  import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
57
58
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
58
59
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
@@ -93,6 +94,7 @@ import {
93
94
  workflowForegroundSteeringLaunchOptions,
94
95
  } from "./workflow-foreground-steering.ts";
95
96
  import { stopAsyncRun } from "./async-stop-action.ts";
97
+ import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
96
98
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
97
99
  import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
98
100
  import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
@@ -162,7 +164,7 @@ import {
162
164
  wrapForkTask,
163
165
  } from "../../shared/types.ts";
164
166
 
165
- const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
167
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "dismiss", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
166
168
  const DESTRUCTIVE_MANAGEMENT_ACTIONS = new Set(["delete", "eject", "disable", "reset", "mission.close", "worktree.discard", "refine.rollback", "inspector.close", "project.close", "stop", "interrupt", "reject-checkpoint", "schedule.delete"]);
167
169
 
168
170
  function editDistance(left: string, right: string): number {
@@ -4464,9 +4466,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4464
4466
  workflow: { trace: [], emits: [], console: [] },
4465
4467
  };
4466
4468
  const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
4469
+ let indexedState: AsyncStatus["state"] | undefined;
4467
4470
  const persist = () => {
4468
4471
  status.lastUpdate = Date.now();
4469
4472
  writeAtomicJson(statusPath, status);
4473
+ if (indexedState !== status.state) {
4474
+ updateActiveRunIndex(asyncDir, status.state);
4475
+ indexedState = status.state;
4476
+ }
4470
4477
  const job = deps.state.asyncJobs.get(workflowRunId);
4471
4478
  if (job) {
4472
4479
  job.status = status.state;
@@ -5249,6 +5256,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5249
5256
  }
5250
5257
  return deps.handleScheduledRunAction(paramsWithResolvedCwd, ctx);
5251
5258
  }
5259
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
5260
+ return {
5261
+ content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
5262
+ isError: true,
5263
+ details: { mode: "management" as const, results: [] },
5264
+ };
5265
+ }
5266
+ if (action === "dismiss") {
5267
+ const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
5268
+ if (!targetRunId) return { content: [{ type: "text", text: "action='dismiss' requires id." }], isError: true, details: { mode: "management", results: [] } };
5269
+ let resolved: ResolvedSubagentRunId | undefined;
5270
+ try {
5271
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
5272
+ } catch (error) {
5273
+ const message = error instanceof Error ? error.message : String(error);
5274
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
5275
+ }
5276
+ if (resolved?.kind !== "async") {
5277
+ return { content: [{ type: "text", text: `Run '${targetRunId}' is not a recovered workflow.` }], isError: true, details: { mode: "management", results: [] } };
5278
+ }
5279
+ return dismissRecoveredWorkflow(deps.state, resolved.location);
5280
+ }
5252
5281
  if (action === "stop") {
5253
5282
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
5254
5283
  const workflowController = targetRunId ? deps.state.workflowControllers?.get(targetRunId) : undefined;
@@ -5260,6 +5289,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5260
5289
  if (paramsWithResolvedCwd.dir) {
5261
5290
  try {
5262
5291
  const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
5292
+ const existingStatus = readStatus(location.asyncDir ?? "");
5293
+ if (existingStatus?.mode === "workflow" && existingStatus.state === "running") {
5294
+ return { content: [{ type: "text", text: `Workflow ${existingStatus.runId} is not controlled by this extension runtime; reload recovery cannot stop it safely.` }], isError: true, details: { mode: "management", results: [] } };
5295
+ }
5263
5296
  const stopResult = stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
5264
5297
  return stopResult ?? { content: [{ type: "text", text: `No running or queued async run was found for '${targetRunId ?? paramsWithResolvedCwd.dir}'.` }], isError: true, details: { mode: "management", results: [] } };
5265
5298
  } catch (error) {
@@ -5344,13 +5377,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5344
5377
  details: { mode: "management" as const, results: [] },
5345
5378
  };
5346
5379
  }
5347
- if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
5348
- return {
5349
- content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
5350
- isError: true,
5351
- details: { mode: "management" as const, results: [] },
5352
- };
5353
- }
5354
5380
  return handleManagementAction(action, paramsWithResolvedCwd, {
5355
5381
  ...ctx,
5356
5382
  cwd: requestCwd,
@@ -31,6 +31,7 @@ const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEX
31
31
  const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
32
32
  export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
33
33
  const STEERING_LEGACY_SETTLE_FALLBACK_MS = 1000;
34
+ const STEERING_SAFETY_POLL_INTERVAL_MS = 5000;
34
35
 
35
36
  const STRUCTURED_OUTPUT_INSTRUCTIONS = [
36
37
  "This subagent step has a strict structured output contract.",
@@ -331,7 +332,13 @@ function registerToolBudget(pi: ExtensionAPI, budget: ResolvedToolBudget | undef
331
332
 
332
333
  export function registerSteeringInbox(
333
334
  pi: ExtensionAPI,
334
- deps: { watch?: typeof fs.watch; nativeRealpath?: (filePath: string) => string; legacySettleFallbackMs?: number } = {},
335
+ deps: {
336
+ watch?: typeof fs.watch;
337
+ nativeRealpath?: (filePath: string) => string;
338
+ legacySettleFallbackMs?: number;
339
+ safetyPollIntervalMs?: number;
340
+ timers?: Pick<typeof globalThis, "setInterval" | "clearInterval">;
341
+ } = {},
335
342
  ): void {
336
343
  const steerInbox = process.env[SUBAGENT_STEER_INBOX_ENV]?.trim();
337
344
  if (!steerInbox) return;
@@ -350,6 +357,7 @@ export function registerSteeringInbox(
350
357
  let canSteer = typeof sendUserMessage === "function";
351
358
  let watcher: fs.FSWatcher | undefined;
352
359
  let interval: NodeJS.Timeout | undefined;
360
+ let safetyInterval: NodeJS.Timeout | undefined;
353
361
  let settleFallback: NodeJS.Timeout | undefined;
354
362
  const legacySettleFallbackMs = deps.legacySettleFallbackMs ?? STEERING_LEGACY_SETTLE_FALLBACK_MS;
355
363
  const acknowledge = (request: SteerRequest, state: "delivered" | "queued" | "failed", message: string, deliveryStatus?: SteerDeliveryStatus): void => {
@@ -431,14 +439,24 @@ export function registerSteeringInbox(
431
439
  return;
432
440
  }
433
441
  started = true;
442
+ const startPolling = (): void => {
443
+ if (interval || disposed) return;
444
+ interval = (deps.timers?.setInterval ?? setInterval)(flush, 250) as NodeJS.Timeout;
445
+ interval.unref?.();
446
+ };
447
+ const startSafetyPolling = (): void => {
448
+ if (safetyInterval || disposed) return;
449
+ safetyInterval = (deps.timers?.setInterval ?? setInterval)(flush, deps.safetyPollIntervalMs ?? STEERING_SAFETY_POLL_INTERVAL_MS) as NodeJS.Timeout;
450
+ safetyInterval.unref?.();
451
+ };
434
452
  try {
435
453
  watcher = (deps.watch ?? fs.watch)(resolveWatchPath(steerInbox, deps.nativeRealpath), () => flush());
436
- watcher.on("error", () => {});
454
+ watcher.on("error", startPolling);
455
+ startSafetyPolling();
437
456
  } catch {
438
457
  watcher = undefined;
458
+ startPolling();
439
459
  }
440
- interval = setInterval(flush, 250);
441
- interval.unref?.();
442
460
  };
443
461
  const activate = (): undefined => {
444
462
  start();
@@ -534,7 +552,8 @@ export function registerSteeringInbox(
534
552
  disposed = true;
535
553
  clearSettleFallback();
536
554
  try { watcher?.close(); } catch {}
537
- if (interval) clearInterval(interval);
555
+ if (interval) (deps.timers?.clearInterval ?? clearInterval)(interval);
556
+ if (safetyInterval) (deps.timers?.clearInterval ?? clearInterval)(safetyInterval);
538
557
  });
539
558
  }
540
559
 
@@ -228,7 +228,7 @@ export function appendJsonl(filePath: string, line: string): void {
228
228
  }
229
229
 
230
230
  export function cleanupOldArtifacts(dir: string, maxAgeDays: number): void {
231
- if (!fs.existsSync(dir)) return;
231
+ if (maxAgeDays <= 0 || !fs.existsSync(dir)) return;
232
232
 
233
233
  const markerPath = path.join(dir, CLEANUP_MARKER_FILE);
234
234
  const now = Date.now();
@@ -5,6 +5,7 @@ interface TimerApi {
5
5
 
6
6
  interface FileCoalescer {
7
7
  schedule(file: string, delayMs?: number): boolean;
8
+ flush(file: string): boolean;
8
9
  clear(): void;
9
10
  }
10
11
 
@@ -30,6 +31,14 @@ export function createFileCoalescer(
30
31
  pending.set(file, timer);
31
32
  return true;
32
33
  },
34
+ flush(file: string): boolean {
35
+ const timer = pending.get(file);
36
+ if (timer === undefined) return false;
37
+ timerApi.clearTimeout(timer);
38
+ pending.delete(file);
39
+ handler(file);
40
+ return true;
41
+ },
33
42
  clear(): void {
34
43
  for (const timer of pending.values()) {
35
44
  timerApi.clearTimeout(timer);
@@ -1290,6 +1290,8 @@ export interface AsyncStatus {
1290
1290
  context?: "fresh" | "fork" | "mixed";
1291
1291
  isNested?: boolean;
1292
1292
  state: "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
1293
+ /** Display-only dismissal marker for a reload-orphaned workflow. */
1294
+ displayDismissedAt?: number;
1293
1295
  error?: string;
1294
1296
  activityState?: ActivityState;
1295
1297
  lastActivityAt?: number;
@@ -1854,6 +1856,8 @@ export interface ExtensionConfig {
1854
1856
  worktreeBaseDir?: string;
1855
1857
  /** Where to store subagent artifact files. Defaults to "project" (cwd/.pi/subagents). Set to "session" for pi session dir, or "temp" for OS temp. */
1856
1858
  artifactDir?: ArtifactDirPreference;
1859
+ /** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
1860
+ artifactConfig?: Pick<ArtifactConfig, "cleanupDays">;
1857
1861
  intercomBridge?: IntercomBridgeConfig;
1858
1862
  proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
1859
1863
  scheduledRuns?: ScheduledRunsConfig;
@@ -1961,7 +1965,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
1961
1965
  export const POLL_INTERVAL_MS = 250;
1962
1966
  export const MAX_WIDGET_JOBS = 4;
1963
1967
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
1964
- export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
1968
+ export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
1965
1969
 
1966
1970
  export const DEFAULT_FORK_PREAMBLE =
1967
1971
  "You are a delegated subagent running from a fork of the parent session. " +
@@ -469,10 +469,13 @@ function validateKey(value: unknown, owner = "runs.run"): string {
469
469
  return value;
470
470
  }
471
471
 
472
- function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label"> {
472
+ function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label" | "agent"> {
473
473
  return {
474
474
  ...(typeof params.phase === "string" && params.phase.trim() ? { phase: params.phase.trim() } : {}),
475
475
  ...(typeof params.label === "string" && params.label.trim() ? { label: params.label.trim() } : {}),
476
+ // Requested agent name, so a child is identifiable while it runs. Launch
477
+ // resolution overwrites this with the canonical name on the terminal entry.
478
+ ...(typeof params.agent === "string" && params.agent.trim() ? { agent: params.agent.trim() } : {}),
476
479
  };
477
480
  }
478
481