pi-subagents 0.47.1 → 0.49.0

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 (66) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +1 -0
  4. package/docs/configuration.md +74 -7
  5. package/docs/missions.md +4 -2
  6. package/docs/observability.md +27 -3
  7. package/docs/tool-reference.md +1 -1
  8. package/package.json +1 -1
  9. package/src/agents/agents.ts +36 -15
  10. package/src/api/preflight.ts +1 -1
  11. package/src/extension/config.ts +26 -0
  12. package/src/extension/doctor.ts +40 -0
  13. package/src/extension/index.ts +39 -8
  14. package/src/extension/public-execution.ts +35 -4
  15. package/src/extension/rpc.ts +3 -9
  16. package/src/extension/schemas.ts +10 -9
  17. package/src/extension/tool-description.ts +10 -8
  18. package/src/inspectors/herdr/actions.ts +11 -2
  19. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  20. package/src/intercom/intercom-bridge.ts +7 -2
  21. package/src/missions/lifecycle.ts +4 -7
  22. package/src/missions/store.ts +12 -7
  23. package/src/missions/workflow-state.ts +6 -2
  24. package/src/runs/background/active-async-capacity.ts +431 -0
  25. package/src/runs/background/active-run-index.ts +9 -5
  26. package/src/runs/background/async-execution.ts +141 -65
  27. package/src/runs/background/async-job-tracker.ts +4 -0
  28. package/src/runs/background/async-resume.ts +18 -2
  29. package/src/runs/background/async-status.ts +11 -5
  30. package/src/runs/background/chain-append.ts +33 -15
  31. package/src/runs/background/fleet-view.ts +18 -5
  32. package/src/runs/background/owned-process-tree.ts +104 -0
  33. package/src/runs/background/process-terminal.ts +17 -3
  34. package/src/runs/background/resume-guidance.ts +27 -7
  35. package/src/runs/background/retained-children.ts +14 -6
  36. package/src/runs/background/run-status.ts +101 -5
  37. package/src/runs/background/stale-run-reconciler.ts +3 -3
  38. package/src/runs/background/subagent-runner.ts +60 -32
  39. package/src/runs/foreground/chain-execution.ts +37 -2
  40. package/src/runs/foreground/execution.ts +90 -19
  41. package/src/runs/foreground/foreground-control.ts +12 -0
  42. package/src/runs/foreground/prompt-audit.ts +172 -0
  43. package/src/runs/foreground/subagent-executor.ts +829 -203
  44. package/src/runs/shared/acceptance.ts +13 -4
  45. package/src/runs/shared/completion-guard.ts +17 -1
  46. package/src/runs/shared/llm-intent-arbiter.ts +302 -0
  47. package/src/runs/shared/parallel-utils.ts +2 -0
  48. package/src/runs/shared/pi-args.ts +44 -1
  49. package/src/runs/shared/run-fanout-budget.ts +280 -0
  50. package/src/runs/shared/single-output.ts +4 -2
  51. package/src/runs/shared/task-intent.ts +19 -3
  52. package/src/runs/shared/worktree.ts +17 -5
  53. package/src/shared/agent-stream-options.ts +5 -0
  54. package/src/shared/artifacts.ts +2 -6
  55. package/src/shared/display-text.ts +50 -0
  56. package/src/shared/node-executable.ts +21 -0
  57. package/src/shared/types.ts +115 -4
  58. package/src/shared/utils.ts +3 -1
  59. package/src/tui/fleet-status.ts +7 -5
  60. package/src/tui/fleet-transcript.ts +1 -48
  61. package/src/tui/fleet.ts +228 -13
  62. package/src/tui/render.ts +86 -39
  63. package/src/watchdog/permission-arbiter.ts +2 -1
  64. package/src/watchdog/review.ts +4 -3
  65. package/src/workflows/chat-progress.ts +8 -2
  66. package/src/workflows/scripted-workflow.ts +66 -10
@@ -30,6 +30,7 @@ import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-sta
30
30
  import { createSubagentParamsSchema } from "./schemas.ts";
31
31
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
32
32
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
33
+ import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
33
34
  import { createResultWatcher } from "../runs/background/result-watcher.ts";
34
35
  import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
35
36
  import { registerSlashCommands } from "../slash/slash-commands.ts";
@@ -58,6 +59,7 @@ import { resolveMissionStoreLocation } from "../missions/store.ts";
58
59
  import { listRetainedChildren } from "../runs/background/retained-children.ts";
59
60
  import {
60
61
  type Details,
62
+ type MainWindowRendererConfig,
61
63
  type SubagentState,
62
64
  DIRS,
63
65
  DEFAULT_ARTIFACT_CONFIG,
@@ -65,6 +67,7 @@ import {
65
67
  SLASH_TEXT_RESULT_TYPE,
66
68
  SUBAGENT_ASYNC_COMPLETE_EVENT,
67
69
  SUBAGENT_ASYNC_STARTED_EVENT,
70
+ SUBAGENT_PROCESS_TERMINAL_EVENT,
68
71
  SUBAGENT_CONTROL_EVENT,
69
72
  SUBAGENT_STEERING_NOTICE_EVENT,
70
73
  WIDGET_KEY,
@@ -251,12 +254,13 @@ function rebuildSlashResultContainer(
251
254
  result: AgentToolResult<Details>,
252
255
  options: { expanded: boolean },
253
256
  theme: ExtensionContext["ui"]["theme"],
257
+ rendererConfig?: MainWindowRendererConfig,
254
258
  ): void {
255
259
  container.clear();
256
260
  container.addChild(new Spacer(1));
257
261
  const boxTheme = isSlashResultRunning(result) ? "toolPendingBg" : isSlashResultError(result) ? "toolErrorBg" : "toolSuccessBg";
258
262
  const box = new Box(1, 1, (text: string) => theme.bg(boxTheme, text));
259
- box.addChild(renderSubagentResult(result, options, theme));
263
+ box.addChild(renderSubagentResult(result, options, theme, undefined, rendererConfig));
260
264
  container.addChild(box);
261
265
  }
262
266
 
@@ -264,6 +268,7 @@ function createSlashResultComponent(
264
268
  details: SlashMessageDetails,
265
269
  options: { expanded: boolean },
266
270
  theme: ExtensionContext["ui"]["theme"],
271
+ rendererConfig?: MainWindowRendererConfig,
267
272
  ): Container {
268
273
  const container = new Container();
269
274
  let lastVersion = -1;
@@ -271,7 +276,7 @@ function createSlashResultComponent(
271
276
  const snapshot = getSlashRenderableSnapshot(details);
272
277
  if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
273
278
  lastVersion = snapshot.version;
274
- rebuildSlashResultContainer(container, snapshot.result, options, theme);
279
+ rebuildSlashResultContainer(container, snapshot.result, options, theme, rendererConfig);
275
280
  }
276
281
  return Container.prototype.render.call(container, width);
277
282
  };
@@ -376,6 +381,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
376
381
  ...(config.authorityPolicy ? { authorityPolicy: config.authorityPolicy } : {}),
377
382
  ...(config.missions ? { missionStoreConfig: config.missions } : {}),
378
383
  parentSessionFile: null,
384
+ trustedSessionRoots: [],
379
385
  subagentInProgress: false,
380
386
  subagentSpawns: {
381
387
  sessionId: null,
@@ -384,6 +390,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
384
390
  granted: 0,
385
391
  grantHistory: [],
386
392
  },
393
+ activeAsyncCapacity: { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 },
387
394
  asyncJobs: new Map(),
388
395
  fleetJobs: new Map(),
389
396
  foregroundRuns: new Map(),
@@ -478,7 +485,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
478
485
  pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
479
486
  const details = resolveSlashMessageDetails(message.details);
480
487
  if (!details) return undefined;
481
- return createSlashResultComponent(details, options, theme);
488
+ return createSlashResultComponent(details, options, theme, config.mainWindowRenderer);
482
489
  });
483
490
 
484
491
  pi.registerMessageRenderer<undefined>(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
@@ -578,22 +585,24 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
578
585
  },
579
586
 
580
587
  renderCall(args, theme) {
588
+ const gap = " ".repeat(config.mainWindowRenderer?.horizontalSpacing ?? 1);
589
+ const title = theme.fg("toolTitle", theme.bold("subagent"));
581
590
  if (args.action) {
582
591
  const target = args.agent || args.chainName || "";
583
592
  return new Text(
584
- `${theme.fg("toolTitle", theme.bold("subagent "))}${args.action}${target ? ` ${theme.fg("accent", target)}` : ""}`,
593
+ `${title}${gap}${args.action}${target ? `${gap}${theme.fg("accent", target)}` : ""}`,
585
594
  0, 0,
586
595
  );
587
596
  }
588
597
  if (args.workflowScript)
589
598
  return new Text(
590
- `${theme.fg("toolTitle", theme.bold("subagent "))}${formatWorkflowManifest(args.workflowScript, args.async, false)}`,
599
+ `${title}${gap}${formatWorkflowManifest(args.workflowScript, args.async, false)}`,
591
600
  0,
592
601
  0,
593
602
  );
594
- const asyncLabel = args.async === true ? theme.fg("warning", " [async]") : "";
603
+ const asyncLabel = args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : "";
595
604
  return new Text(
596
- `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
605
+ `${title}${gap}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
597
606
  0,
598
607
  0,
599
608
  );
@@ -604,7 +613,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
604
613
  const renderedResult = { ...result, isError: context.isError };
605
614
  return summaryInlineToolDisplay
606
615
  ? renderSubagentSummary(renderedResult, options, theme)
607
- : renderSubagentResult(renderedResult, options, theme);
616
+ : renderSubagentResult(renderedResult, options, theme, undefined, config.mainWindowRenderer);
608
617
  },
609
618
 
610
619
  };
@@ -677,12 +686,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
677
686
  };
678
687
  const asyncCompleteHandler = (payload: unknown) => {
679
688
  handleComplete(payload);
689
+ refreshActiveAsyncCapacity();
680
690
  scheduledRunManager.handleAsyncCompletion(payload);
681
691
  fleetStatus?.refresh();
682
692
  };
683
693
  const eventUnsubscribes = [
684
694
  pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, asyncStartedHandler),
685
695
  pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
696
+ pi.events.on(SUBAGENT_PROCESS_TERMINAL_EVENT, () => {
697
+ refreshActiveAsyncCapacity();
698
+ fleetStatus?.refresh();
699
+ }),
686
700
  pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
687
701
  pi.events.on(SUBAGENT_STEERING_NOTICE_EVENT, steeringNoticeHandler),
688
702
  herdrStatusBridge.dispose,
@@ -728,12 +742,28 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
728
742
  fleetStatus?.refresh();
729
743
  };
730
744
 
745
+ const refreshActiveAsyncCapacity = () => {
746
+ if (!state.currentSessionId) {
747
+ state.activeAsyncCapacity = { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 };
748
+ return;
749
+ }
750
+ state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
751
+ state.currentSessionId,
752
+ resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession),
753
+ { liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []) },
754
+ );
755
+ };
756
+
731
757
  const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
732
758
  state.widgetsSuspended = false;
733
759
  state.baseCwd = ctx.cwd;
734
760
  goalTurnId = 0;
735
761
  state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
736
762
  state.parentSessionFile = ctx.sessionManager.getSessionFile();
763
+ state.trustedSessionRoots = [...new Set([
764
+ ...(config.defaultSessionDir ? [path.resolve(expandTilde(config.defaultSessionDir))] : []),
765
+ ...(state.parentSessionFile ? [getSubagentSessionRoot(state.parentSessionFile)] : []),
766
+ ])];
737
767
  state.subagentSpawns = {
738
768
  sessionId: state.currentSessionId,
739
769
  count: 0,
@@ -753,6 +783,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
753
783
  }
754
784
  }
755
785
  state.lastUiContext = ctx;
786
+ refreshActiveAsyncCapacity();
756
787
  cleanupSessionArtifacts(ctx);
757
788
  state.foregroundControls.clear();
758
789
  state.lastForegroundControlId = null;
@@ -11,6 +11,8 @@ export interface PublicSubagentExecutionParams {
11
11
  workflowScript?: unknown;
12
12
  resume?: unknown;
13
13
  clarify?: unknown;
14
+ runFanoutBudget?: unknown;
15
+ runFanoutAdmitted?: unknown;
14
16
  }
15
17
 
16
18
  export type PublicSubagentExecutionMode = "workflow" | "management";
@@ -24,6 +26,9 @@ export type PublicSubagentExecutionNormalization<T> =
24
26
  * Internal runs.run children and structured owned delegation bypass this boundary.
25
27
  */
26
28
  export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
29
+ if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
30
+ return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
31
+ }
27
32
  const action = params.action;
28
33
  if (action !== undefined && (typeof action !== "string" || !action.trim())) {
29
34
  return { ok: false, error: "action must be a non-empty management/control action, or omit action and use workflowScript.", mode: "management" };
@@ -42,7 +47,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
42
47
  if (normalizedAction !== undefined) {
43
48
  const legacyAction = normalizedAction.toLowerCase();
44
49
  if (legacyAction === "single") {
45
- return { ok: false, error: "Direct execution was removed. Use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
50
+ return { ok: false, error: "action='single' is not supported. Omit action and pass { agent, task } for one child.", mode: "workflow" };
46
51
  }
47
52
  if (legacyAction === "parallel" || legacyAction === "tasks" || legacyAction === "chain") {
48
53
  return { ok: false, error: "Legacy top-level chain and parallel inputs were removed; use workflowScript.", mode: "workflow" };
@@ -59,13 +64,39 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
59
64
  if (params.workflowScript !== undefined) {
60
65
  return { ok: false, error: "workflowScript execution must omit action; only schedule.create accepts action with workflowScript.", mode: "management" };
61
66
  }
67
+ if (params.task !== undefined) {
68
+ return { ok: false, error: "Structured single-child task cannot be combined with a management/control action.", mode: "management" };
69
+ }
62
70
  return { ok: true, params: { ...params, action: normalizedAction } };
63
71
  }
64
- if (params.agent !== undefined || params.task !== undefined || params.step !== undefined) {
65
- return { ok: false, error: "Direct execution was removed. Use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
72
+ if (params.step !== undefined) {
73
+ return { ok: false, error: "step is only available with action='append-step'; it is not an execution mode.", mode: "workflow" };
74
+ }
75
+ if (params.workflowScript !== undefined && (params.agent !== undefined || params.task !== undefined)) {
76
+ return { ok: false, error: "Structured single-child execution cannot be combined with workflowScript.", mode: "workflow" };
77
+ }
78
+ if (params.agent !== undefined || params.task !== undefined) {
79
+ if (typeof params.agent !== "string" || !params.agent.trim()) {
80
+ return { ok: false, error: "Structured single-child execution requires agent to be a non-empty string.", mode: "workflow" };
81
+ }
82
+ if (params.task !== undefined && typeof params.task !== "string") {
83
+ return { ok: false, error: "Structured single-child task must be a string when provided.", mode: "workflow" };
84
+ }
85
+ const { agent: _agent, task: _task, ...workflowDefaults } = params;
86
+ const child = {
87
+ agent: params.agent.trim(),
88
+ ...(params.task !== undefined ? { task: params.task } : {}),
89
+ };
90
+ return {
91
+ ok: true,
92
+ params: {
93
+ ...workflowDefaults,
94
+ workflowScript: `console.info("Converted structured single-child request to workflow runs.run('main', ...)."); return runs.run("main", ${JSON.stringify(child)})`,
95
+ } as T,
96
+ };
66
97
  }
67
98
  if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) {
68
- return { ok: false, error: "Execution requires a non-empty workflowScript. Direct execution was removed; use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
99
+ return { ok: false, error: "Execution requires either { agent, task? } for one child or a non-empty workflowScript for orchestration.", mode: "workflow" };
69
100
  }
70
101
  return { ok: true, params };
71
102
  }
@@ -19,7 +19,6 @@ import {
19
19
  import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
20
20
  import { readStatus } from "../shared/utils.ts";
21
21
  import { SubagentParams } from "./schemas.ts";
22
- import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
23
22
  import { normalizePublicSubagentExecution } from "./public-execution.ts";
24
23
 
25
24
  export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
@@ -91,6 +90,7 @@ export interface SubagentRpcFleetStatus {
91
90
  entries: SubagentRpcFleetEntry[];
92
91
  /** Total active children before the bounded entries window. */
93
92
  totalActive: number;
93
+ topLevelAsyncCapacity: { used: number; limit: number };
94
94
  omitted: number;
95
95
  }
96
96
 
@@ -154,7 +154,7 @@ function buildFleetStatus(
154
154
  }
155
155
  if (!state || !authoritativeSessionId || state.currentSessionId !== authoritativeSessionId) {
156
156
  keyState.keys.clear();
157
- return { version: 1, entries: [], totalActive: 0, omitted: 0 };
157
+ return { version: 1, entries: [], totalActive: 0, topLevelAsyncCapacity: { used: 0, limit: 0 }, omitted: 0 };
158
158
  }
159
159
 
160
160
  let totalActive = 0;
@@ -173,7 +173,6 @@ function buildFleetStatus(
173
173
  effort: child.thinking,
174
174
  startedAt: child.startedAt,
175
175
  tokens: { input: child.inputTokens ?? 0, output: child.outputTokens ?? 0, total: child.tokens ?? 0 },
176
- goal: child.description ?? control.description,
177
176
  });
178
177
  } else {
179
178
  addCandidate({
@@ -183,7 +182,6 @@ function buildFleetStatus(
183
182
  effort: control.thinking,
184
183
  startedAt: control.startedAt,
185
184
  tokens: { input: control.inputTokens ?? 0, output: control.outputTokens ?? 0, total: control.tokens ?? 0 },
186
- goal: control.description,
187
185
  });
188
186
  }
189
187
  }
@@ -191,13 +189,11 @@ function buildFleetStatus(
191
189
  if (job.sessionId !== authoritativeSessionId || !activeState(job.status)) continue;
192
190
  const startedAt = job.startedAt ?? job.updatedAt;
193
191
  if (job.mode === "workflow") {
194
- const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
195
192
  addCandidate({
196
193
  internalKey: `async:${job.asyncId}`,
197
194
  agent: "workflow",
198
195
  startedAt,
199
196
  tokens: job.totalTokens,
200
- goal: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
201
197
  });
202
198
  continue;
203
199
  }
@@ -214,7 +210,6 @@ function buildFleetStatus(
214
210
  agent: job.mode ?? "subagent",
215
211
  startedAt,
216
212
  tokens: job.totalTokens,
217
- goal: job.description,
218
213
  });
219
214
  continue;
220
215
  }
@@ -230,7 +225,6 @@ function buildFleetStatus(
230
225
  effort: step.thinking,
231
226
  startedAt: step.startedAt ?? startedAt,
232
227
  tokens: step.tokens ?? (steps.length === 1 ? job.totalTokens : undefined),
233
- goal: job.description,
234
228
  });
235
229
  }
236
230
  }
@@ -271,7 +265,7 @@ function buildFleetStatus(
271
265
  if (!activeKeys.has(internalKey)) keyState.keys.delete(internalKey);
272
266
  }
273
267
  const omitted = Math.max(0, totalActive - entries.length);
274
- return { version: 1, entries, totalActive, omitted };
268
+ return { version: 1, entries, totalActive, topLevelAsyncCapacity: state.activeAsyncCapacity ?? { used: 0, limit: 0 }, omitted };
275
269
  }
276
270
 
277
271
  interface RegisterSubagentRpcBridgeOptions {
@@ -255,21 +255,22 @@ const ControlOverrides = Type.Object({
255
255
  });
256
256
 
257
257
  const SubagentParamProperties = {
258
- agent: Type.Optional(Type.String({ description: "Agent target for management actions such as get, update, delete, and models." })),
258
+ agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
259
+ task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action or workflowScript." })),
259
260
  resume: Type.Optional(Type.String({ description: "Retained child run id for a workflowScript runs.run/runs.all item. Mutually exclusive with agent; task supplies the follow-up." })),
260
261
  // Management action (when present, tool operates in management mode)
261
262
  action: Type.Optional(Type.String({ minLength: 1,
262
- description: "Optional management/control action. Omit this field for workflowScript execution; use it only for management/control actions."
263
+ description: "Optional management/control action. Omit this field for structured single-child or workflowScript execution; use it only for management/control actions."
263
264
  })),
264
265
  name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
265
266
  id: Type.Optional(Type.String({
266
- description: "Run id or prefix for status, interrupt, stop, dismiss, resume, steer, append-step, approve-checkpoint, reject-checkpoint, mission.attach-run, or the decision id for mission.resolve-decision."
267
+ description: "Run id/prefix for status/debug.run, interrupt, steer, append-step, approve-checkpoint, reject-checkpoint, or mission."
267
268
  })),
268
269
  runId: Type.Optional(Type.String({
269
- description: "Target run ID for interrupt, stop, dismiss, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run. Prefer id for new calls."
270
+ description: "Target run ID for debug.run, interrupt, steer, append-step, or mission.attach-run. Prefer id."
270
271
  })),
271
272
  dir: Type.Optional(Type.String({
272
- description: "Async run directory for action='status', action='stop', action='resume', or action='steer'."
273
+ description: "Async run directory for status/debug.run, stop, resume, or steer."
273
274
  })),
274
275
  handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
275
276
  index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
@@ -324,8 +325,8 @@ const SubagentParamProperties = {
324
325
  description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child in the invocation. If omitted, each requested agent uses its own defaultContext; agents without defaultContext: 'fork' run fresh.",
325
326
  })),
326
327
  async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
327
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Run timeout. Foreground runs and async children default to 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
328
- maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground runs and async children default to 30m; async composites have no default parent deadline." })),
328
+ timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
329
+ maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline." })),
329
330
  turnBudget: Type.Optional(TurnBudgetOverride),
330
331
  toolBudget: Type.Optional(ToolBudgetOverride),
331
332
  usageBudget: Type.Optional(UsageBudgetOverride),
@@ -359,10 +360,10 @@ const { step: _legacyChainStep, ...subagentParamPropertiesWithoutStep } = Subage
359
360
  const trimmedSubagentParamProperties = {
360
361
  ...subagentParamPropertiesWithoutStep,
361
362
  id: Type.Optional(Type.String({
362
- description: "Run id or prefix for status, interrupt, stop, resume, steer, mission.attach-run, or the decision id for mission.resolve-decision."
363
+ description: "Run id/prefix for status/debug.run, interrupt, steer, or mission.attach-run."
363
364
  })),
364
365
  runId: Type.Optional(Type.String({
365
- description: "Target run ID for interrupt, stop, resume, steer, or mission.attach-run. Prefer id for new calls."
366
+ description: "Target run ID for debug.run, interrupt, steer, or mission.attach-run. Prefer id."
366
367
  })),
367
368
  };
368
369
  const SubagentParamsSchema = Type.Object(SubagentParamProperties);
@@ -8,24 +8,25 @@ const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
8
8
 
9
9
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
10
10
  • Use { action: "list" } before execution and only run executable/non-disabled agents.
11
- • Keep execution and management separate: omit action for workflowScript execution; use action only for management/control.
12
- • Async/background runs are the default. Use async:false only when a blocking foreground result is needed. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
11
+ • Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
12
+ • Async/background runs are the default. Use async:false only when a blocking foreground result is needed. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
13
13
  • Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
14
14
  • Keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers for independent review, then have the parent synthesize and apply fixes.
15
- • Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, and status via { action: "status", id }. Include output paths and residual risks when reporting results.`;
15
+ • Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, status via { action: "status", id }, and lifecycle diagnostics via { action: "debug.run", id }. Include output paths and residual risks when reporting results.`;
16
16
 
17
- export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run subagents only through { workflowScript }; omit action. Use action only for management/control actions.
17
+ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for orchestration. Omit action for execution. Use action only for management/control actions.
18
18
 
19
19
  EXECUTION:
20
20
  • Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
21
- • WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Every execution is a workflow. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable plain task text, then pass the result explicitly as task. Use {action:"children.list"} to list up to 10 completed retained children from this parent session, then continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); resume and agent are mutually exclusive, resume keeps the stored agent/model/tool contract, workflow resumes wait for completed output, and loops must continue from each latest returned runId. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, prompts.render, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
21
+ • SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one child through the workflow runtime. Workflow-level fields such as model, context, cwd, worktree, output, budgets, acceptance, and async remain defaults for that child. Do not combine agent/task with action or workflowScript.
22
+ • WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable plain task text, then pass the result explicitly as task. Use {action:"children.list"} to list up to 10 completed retained children from this parent session, then continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); resume and agent are mutually exclusive, resume keeps the stored agent/model/tool contract, workflow resumes wait for completed output, and loops must continue from each latest returned runId. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, prompts.render, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
22
23
  • Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
23
24
  • Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
24
25
  • Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
25
26
  • Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work. A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
26
27
 
27
28
  MANAGEMENT / CONTROL (use action; omit execution fields):
28
- • list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
29
+ • list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
29
30
  • status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
30
31
  • { action: "append-step", id: "...", step: {agent:"agent-c", task:"Use {previous}"} } appends one step to an already-running durable legacy chain. step is control-only, not an execution mode.
31
32
  • approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.
@@ -33,10 +34,11 @@ MANAGEMENT / CONTROL (use action; omit execution fields):
33
34
 
34
35
  ${SUBAGENT_SAFETY_GUIDANCE}`;
35
36
 
36
- export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run subagents only through { workflowScript }; omit action. Use action only for management/control actions.
37
+ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for orchestration. Omit action for execution. Use action only for management/control actions.
37
38
 
38
39
  EXECUTE:
39
40
  • Call { action:"list" } first and use only executable/non-disabled agents.
41
+ • SINGLE {agent:"worker",task:"..."} starts exactly one child through the workflow runtime. Workflow-level fields remain child defaults. Do not combine agent/task with action or workflowScript.
40
42
  • SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable task text and pass it explicitly to runs.run. Use {action:"children.list"} for the last 10 retained children in this parent session, then runs.run(key,{resume:"run-id",task:"follow-up"}) to continue one with its stored contract; workflow resumes wait for completion and loops continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off.
41
43
  • Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
42
44
  • context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
@@ -47,7 +49,7 @@ MANAGE / CONTROL:
47
49
  • A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
48
50
 
49
51
  ASYNC / SAFETY:
50
- • Omitted async detaches background work. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
52
+ • Omitted async detaches background work. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
51
53
  • Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
52
54
  • Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
53
55
 
@@ -10,6 +10,7 @@ import { writeAtomicJson } from "../../shared/atomic-json.ts";
10
10
  import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
11
11
  import { readStatus } from "../../shared/utils.ts";
12
12
  import { resolveSubagentRunId } from "../../runs/background/run-id-resolver.ts";
13
+ import { resolveNodeExecutable } from "../../shared/node-executable.ts";
13
14
  import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from "./client.ts";
14
15
 
15
16
  export const HERDR_INSPECTOR_ACTIONS = ["inspector.open", "inspector.status", "inspector.close"] as const;
@@ -45,6 +46,7 @@ interface InspectorDeps {
45
46
  client?: HerdrClient;
46
47
  missions?: MissionStoreConfig;
47
48
  authorityPolicy?: AuthorityPolicyConfig;
49
+ sessionRoots?: string[];
48
50
  cwd: string;
49
51
  signal?: AbortSignal;
50
52
  now?: () => Date;
@@ -89,8 +91,8 @@ function shellQuote(value: string): string {
89
91
  return `'${value.replaceAll("'", "'\\''")}'`;
90
92
  }
91
93
 
92
- function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean }): string {
93
- const args = [process.execPath, input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop)];
94
+ function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean; sessionRoots: string[] }): string {
95
+ const args = [resolveNodeExecutable(), input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop), "--session-roots", JSON.stringify(input.sessionRoots)];
94
96
  if (input.index !== undefined) args.push("--index", String(input.index));
95
97
  if (input.missionPath) args.push("--mission-path", input.missionPath);
96
98
  return `${process.platform === "win32" ? "& " : ""}${args.map(shellQuote).join(" ")}`;
@@ -114,6 +116,12 @@ function pathWithin(base: string, candidate: string): boolean {
114
116
  return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
115
117
  }
116
118
 
119
+ function herdrSessionRoots(target: { runId: string }, deps: InspectorDeps): string[] {
120
+ const roots = deps.sessionRoots ?? deps.state?.trustedSessionRoots ?? [];
121
+ const job = deps.state?.asyncJobs.get(target.runId) ?? deps.state?.fleetJobs?.get(target.runId);
122
+ return [...new Set([...roots, ...(job?.sessionRoot ? [job.sessionRoot] : [])])];
123
+ }
124
+
117
125
  function isTrustedAsyncDir(asyncDir: string, deps: InspectorDeps): boolean {
118
126
  try {
119
127
  if (fs.lstatSync(asyncDir).isSymbolicLink() || !fs.statSync(asyncDir).isDirectory()) return false;
@@ -204,6 +212,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
204
212
  missionPath: mission?.path,
205
213
  allowSteer: resolveAuthorityDecision({ action: "steerRun", policy: deps.authorityPolicy }) === "auto",
206
214
  allowStop: resolveAuthorityDecision({ action: "stopRun", policy: deps.authorityPolicy }) === "auto",
215
+ sessionRoots: herdrSessionRoots(target, deps),
207
216
  });
208
217
  const started = await client.run(["pane", "run", paneId, command], { timeoutMs: 15_000, signal: deps.signal });
209
218
  if (started.ok === false) {
@@ -17,6 +17,7 @@ export interface RunnerOptions {
17
17
  refreshMs: number;
18
18
  allowSteer?: boolean;
19
19
  allowStop?: boolean;
20
+ sessionRoots: string[];
20
21
  }
21
22
 
22
23
  function readMission(filePath: string | undefined): MissionRecord | undefined {
@@ -24,7 +25,7 @@ function readMission(filePath: string | undefined): MissionRecord | undefined {
24
25
  try { return parseMissionRecord(JSON.parse(fs.readFileSync(filePath, "utf-8")), filePath); } catch { return undefined; }
25
26
  }
26
27
 
27
- export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir: string; index?: number; mission?: MissionRecord; allowSteer?: boolean; allowStop?: boolean }): string {
28
+ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir: string; index?: number; mission?: MissionRecord; allowSteer?: boolean; allowStop?: boolean; sessionRoots?: string[] }): string {
28
29
  const { status, asyncDir, mission } = input;
29
30
  const lines = [
30
31
  `pi-subagents inspector for ${status.runId}`,
@@ -37,7 +38,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
37
38
  if (open.length) lines.push(`Open decisions: ${open.map((decision) => `${decision.id}: ${decision.title}`).join(" | ")}`);
38
39
  lines.push("");
39
40
  }
40
- lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60 }));
41
+ lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60, sessionRoots: input.sessionRoots }));
41
42
  const controls = [input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
42
43
  lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).");
43
44
  return lines.join("\n");
@@ -57,6 +58,17 @@ function parseArgs(argv: string[]): RunnerOptions {
57
58
  const indexRaw = values.get("--index");
58
59
  const childIndex = indexRaw === undefined ? undefined : Number(indexRaw);
59
60
  if (childIndex !== undefined && (!Number.isInteger(childIndex) || childIndex < 0)) throw new Error("--index must be a non-negative integer.");
61
+ const sessionRootsRaw = values.get("--session-roots");
62
+ let sessionRoots: string[] = [];
63
+ if (sessionRootsRaw !== undefined) {
64
+ try {
65
+ const parsed = JSON.parse(sessionRootsRaw) as unknown;
66
+ if (!Array.isArray(parsed) || parsed.some((root) => typeof root !== "string")) throw new Error();
67
+ sessionRoots = parsed;
68
+ } catch {
69
+ throw new Error("--session-roots must be a JSON array of strings.");
70
+ }
71
+ }
60
72
  const refreshRaw = values.get("--refresh-ms");
61
73
  const refreshMs = refreshRaw === undefined ? 1_500 : Number(refreshRaw);
62
74
  if (!Number.isInteger(refreshMs) || refreshMs < 250) throw new Error("--refresh-ms must be an integer >= 250.");
@@ -65,6 +77,7 @@ function parseArgs(argv: string[]): RunnerOptions {
65
77
  runId,
66
78
  ...(childIndex !== undefined ? { index: childIndex } : {}),
67
79
  ...(values.get("--mission-path") ? { missionPath: path.resolve(values.get("--mission-path")!) } : {}),
80
+ sessionRoots,
68
81
  refreshMs,
69
82
  allowSteer: values.get("--allow-steer") !== "false",
70
83
  allowStop: values.get("--allow-stop") !== "false",
@@ -115,7 +128,7 @@ export function runInspector(argv = process.argv.slice(2)): void {
115
128
  process.stdout.write(`\x1b[2J\x1b[Hpi-subagents inspector\n\nLifecycle status for ${options.runId} is unavailable.\n`);
116
129
  return;
117
130
  }
118
- process.stdout.write(`\x1b[2J\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop })}${notice ? `\n\n${notice}` : ""}\n> `);
131
+ process.stdout.write(`\x1b[2J\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop, sessionRoots: options.sessionRoots })}${notice ? `\n\n${notice}` : ""}\n> `);
119
132
  if (isTerminal(status) && timer) {
120
133
  clearInterval(timer);
121
134
  timer = undefined;
@@ -51,6 +51,8 @@ export interface IntercomBridgeDiagnostic {
51
51
 
52
52
  interface ResolveIntercomBridgeInput {
53
53
  config: ExtensionConfig["intercomBridge"];
54
+ /** Per-run config replaces the global config when supplied. */
55
+ override?: IntercomBridgeConfig;
54
56
  context: "fresh" | "fork" | undefined;
55
57
  orchestratorTarget?: string;
56
58
  cwd?: string;
@@ -63,7 +65,10 @@ export function resolveIntercomSessionTarget(sessionName: string | undefined, se
63
65
  if (trimmedName) return trimmedName;
64
66
  const fallbackSessionId = intercomSessionId?.trim() || sessionId;
65
67
  const normalizedSessionId = fallbackSessionId.startsWith("session-") ? fallbackSessionId.slice("session-".length) : fallbackSessionId;
66
- return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
68
+ // NOTE: keep slice length in sync with pi-intercom's resolveIntercomPresenceName
69
+ // (index.ts: DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX + slice(0, 18)); mismatched lengths
70
+ // make fallback orchestrator targets unresolvable ("Session not found").
71
+ return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 18)}`;
67
72
  }
68
73
 
69
74
  function sanitizeIntercomTargetPart(value: string): string {
@@ -140,7 +145,7 @@ export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): Inter
140
145
  }
141
146
 
142
147
  export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
143
- const config = resolveIntercomBridgeConfig(input.config);
148
+ const config = resolveIntercomBridgeConfig(input.override !== undefined ? input.override : input.config);
144
149
  const mode = config.mode;
145
150
  const orchestratorTarget = input.orchestratorTarget?.trim();
146
151
  const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
5
+ import { PROMPT_REDACTED } from "../shared/utils.ts";
5
6
  import type { Details, SubagentRunMode } from "../shared/types.ts";
6
7
  import { validateMissionLaunch } from "./actions.ts";
7
8
  import type { MissionArtifact, MissionRecord, MissionRunLink, MissionRunMode, MissionStatus, MissionStoreConfig, MissionStoreLocation } from "./types.ts";
@@ -47,11 +48,6 @@ function workflowObjective(params: MissionLaunchParams): string | undefined {
47
48
  return undefined;
48
49
  }
49
50
 
50
- function conciseTitle(objective: string): string {
51
- const firstLine = objective.split(/\r?\n/, 1)[0]?.trim() || objective.trim();
52
- return firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
53
- }
54
-
55
51
  export function prepareMissionLaunch(input: {
56
52
  params: MissionLaunchParams;
57
53
  projectRoot: string;
@@ -73,10 +69,11 @@ export function prepareMissionLaunch(input: {
73
69
  return { missionId, location, autoCreated: false, announceInContent: true };
74
70
  }
75
71
  const mission = input.params.mission !== undefined ? validateMissionLaunch(input.params.mission) : undefined;
76
- const title = mission?.title || conciseTitle(objective!);
72
+ const promptDerivedObjective = mission?.objective ?? (mission ? mission.title : objective ? PROMPT_REDACTED : undefined);
73
+ const title = mission?.title || PROMPT_REDACTED;
77
74
  const record = createMission(location, {
78
75
  title,
79
- objective: mission?.objective || objective || title,
76
+ objective: promptDerivedObjective || title,
80
77
  ...(mission?.goal === true ? { goal: true as const } : {}),
81
78
  ...(mission?.budget ? { budget: mission.budget } : {}),
82
79
  status: "active",