@signalridge/pi-subagents 1.10.0 → 1.10.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.10.2
4
+ ### Patch Changes
5
+
6
+ - 65e2481: Show supervisor questions in subagent input dialogs and identify the asking agent by its actual name.
7
+
8
+ ## 1.10.1
9
+ ### Patch Changes
10
+
11
+ - 1f586f8: Preserve resumable cancellation when a workflow pauses during successful timeout cleanup, and retain finalized subagent output alongside observer failures. Avoid retaining descendant-owned mouse gestures in bordered components. Track successful writes using final rewritten paths, and distinguish uncertain changes observed during failed calls from confirmed agent modifications while keeping both discoverable in file browsing. Preserve exact filesystem paths and lazy-directory scan state when discovering session-observed changes, including changes injected during an active scan.
12
+ - 1f586f8: - Bound workflow cancellation drains and durably fence unconfirmed cleanup, aborting sibling agents and releasing foreground callers.
13
+ - Await managed timeout cleanup before script continuation and terminal publication.
14
+ - Keep failed retry text and length responses discarded by overflow-compaction retry out of successful subagent fallbacks, while retaining valid pre-compaction and non-retried length output.
15
+ - Account for native prompts spanning reporter activation without dropping out-of-order wait debt.
16
+ - Reconcile settlement after observer-triggered manual compaction with run/session ownership.
17
+ - Preserve clipped overlay hit ranges and native container ancestry for editor autocomplete focus.
18
+ - Reconcile errored writes and edits against pre/post mutation evidence, retaining invocation cwd and releasing evidence when blocked or aborted preflight bypasses tool results.
19
+ - d9219d4: Correct shipped documentation that contradicted the code or the manifest.
20
+
21
+ - `pi-subagents`: the summary line still advertised protocol-v3 spawning while
22
+ the feature list and the managed-spawn section described v4.
23
+ - `pi-worktree`: the workspace-switching section named `/tree`; the registered
24
+ command is `/worktree`.
25
+ - `pi-workflows`: removed an orphaned sentence fragment left behind by an edit
26
+ to the saved-workflow paragraph.
27
+ - `pi-plan-mode`, `pi-goal`, `pi-usage-extension`: the stated Pi floor
28
+ (`0.80.6`, `0.42.4+`) contradicted the declared `^0.84.0` peer range, which
29
+ resolves to `0.84.x`. `pi-usage-extension` also carried a "Last updated"
30
+ stamp from a version that predates the workspace baseline.
31
+ - `pi-welcome`: the example card was a real session capture. It is now a
32
+ generic sample, and Install moved above the configuration sections instead of
33
+ sitting below them.
34
+ - 1f586f8: Expand Pi peer support to `^0.84.0 || ^0.85.0`, retaining 0.84 compatibility while admitting 0.85 releases. The previous zero-major caret range excluded Pi 0.85.1. Update existing Pi development dependency pins to 0.85.1.
35
+ - d9219d4: Converge every README on one house style: plain sentence-case headings, no
36
+ decorative emoji.
37
+
38
+ Ten packages carried an emoji heading scheme inherited from their upstream
39
+ forks while the other nineteen used plain headings, so the same monorepo
40
+ rendered as two unrelated projects on npmjs.com. Headings are now emoji-free
41
+ and titles are sentence case.
42
+
43
+ Also removes the `Keywords` section from those ten. It duplicated each
44
+ package's `package.json` `keywords` field, which is what npm actually indexes,
45
+ and no plain-style README carried one. `Installation` is now `Install`
46
+ everywhere.
47
+
48
+ Headings that were Title Case are sentence case too, so one convention now
49
+ covers the whole monorepo. Existing in-page anchor links are unaffected:
50
+ GitHub lowercases heading slugs already.
51
+
52
+ Documentation only — no runtime change.
53
+ - 1f586f8: Keep goal iteration and continuation numbering correct when Pi compacts between tool results and the next response within one run. Preserve existing response limits and manual/post-run compaction handling.
54
+
55
+ Capture subagent invocation results from finalized assistant events with bounded message retention so compaction cannot hide a final error or mix in historical output. Recognize explicitly selected PowerShell when the host provides it, without requiring its export on older Pi versions or expanding default wildcard tools.
56
+
57
+ Use real session contexts for workflow replay and live TUI progress, release widget bindings on shutdown, and allow foreground checkpoint dialogs in RPC mode while retaining print-mode defaults. Cancel unanswered dialogs with their owning execution on tool abort, pause, stop, disposal, or fatal failure without journaling cancelled answers.
58
+ - Updated dependencies [1f586f8]
59
+ - Updated dependencies [1f586f8]
60
+ - Updated dependencies [1f586f8]
61
+ - Updated dependencies [1f586f8]
62
+ - @signalridge/pi-ui@1.3.1
63
+
3
64
  ## 1.10.0
4
65
  ### Minor Changes
5
66
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @signalridge/pi-subagents
2
2
 
3
- Signalridge's managed subagent package for Pi, including protocol-v3 spawning, lifecycle isolation, Fleet UI, persistence, and recovery.
3
+ Signalridge's managed subagent package for Pi, including protocol-v4 spawning, lifecycle isolation, Fleet UI, persistence, and recovery.
4
4
 
5
5
  A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Run them in foreground or background, steer them mid-run, resume completed sessions, and define your own custom agent types.
6
6
 
@@ -50,7 +50,7 @@ pi -e ./packages/pi-subagents
50
50
 
51
51
  Formatter checks remain disabled for this package, while legacy `noExplicitAny`, control-character regex, and empty-interface rules remain off. Signalridge additions follow the package AGENTS rules and the strict `--error-on-warnings` lint gate.
52
52
 
53
- ## Quick Start
53
+ ## Quick start
54
54
 
55
55
  The parent agent spawns sub-agents using the `Agent` tool:
56
56
 
@@ -161,7 +161,7 @@ Find auth files · completed
161
161
 
162
162
  Group completions render each agent as a separate block. The LLM receives structured `<task-notification>` XML for parsing, while the user sees the themed visual.
163
163
 
164
- ## Default Agent Types
164
+ ## Default agent types
165
165
 
166
166
  | Type | Tools | Model | Prompt Mode | Description |
167
167
  |------|-------|-------|-------------|-------------|
@@ -173,7 +173,7 @@ The `general-purpose` agent is a **parent twin** — it receives the parent's en
173
173
 
174
174
  Default agents can be **ejected** (`/agents` → select agent → Eject) to export them as `.md` files for customization, **overridden** by creating a `.md` file with the same name (e.g. `.pi/agents/general-purpose.md`), or **disabled** per-project with `enabled: false` frontmatter.
175
175
 
176
- ## Custom Agents
176
+ ## Custom agents
177
177
 
178
178
  Define custom agent types by creating `.md` files. The filename becomes the agent type name. Any name is allowed — using a default agent's name overrides it.
179
179
 
@@ -216,7 +216,7 @@ Then spawn it like any built-in type:
216
216
  Agent({ subagent_type: "auditor", prompt: "Review the auth module", description: "Security audit" })
217
217
  ```
218
218
 
219
- ### Frontmatter Fields
219
+ ### Frontmatter fields
220
220
 
221
221
  All fields are optional — sensible defaults for everything.
222
222
 
@@ -376,7 +376,7 @@ Settings
376
376
  - **Model tiers** — create, edit and delete the [tier](#model-tiers) profiles. Each row shows what the tier resolves to on this machine (`small — claude-haiku-4-5 · thinking max`), and a tier dropped as malformed is listed as `blocked` so it can be fixed rather than staying invisible. The model picker offers this machine's available models (narrowed to your scope when **Scope models** is on) plus `inherit` and a typed escape hatch; the thinking picker offers only the levels the chosen model actually supports, since the rest would be silently clamped at spawn. Deleting the tier that `defaultTier` names clears the default in the same step
377
377
  - **Settings** — configure max concurrency, default max turns, grace turns, default model, default tier, and join mode at runtime
378
378
 
379
- ## Graceful Max Turns
379
+ ## Graceful max turns
380
380
 
381
381
  Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
382
382
 
@@ -397,7 +397,7 @@ Background agents are subject to a configurable concurrency limit (default: 4).
397
397
 
398
398
  Foreground agents bypass the queue — they block the parent anyway.
399
399
 
400
- ## Join Strategies
400
+ ## Join strategies
401
401
 
402
402
  When background agents complete, they notify the main agent. The **join mode** controls how these notifications are delivered. It applies only to background agents.
403
403
 
@@ -640,7 +640,7 @@ then `defaultModel`, or the parent's model when none is set.
640
640
  Programmatic callers and the legacy RPC may still pass `model`/`thinking`
641
641
  directly. That is the escape hatch for code, not a way to configure an agent.
642
642
 
643
- ## Model Scope
643
+ ## Model scope
644
644
 
645
645
  **Opt-in:** off by default. Enable via `/agents → Settings → Scope models`.
646
646
 
@@ -662,7 +662,7 @@ When on, each subagent spawn's effective model is validated against pi's own `en
662
662
 
663
663
  **No-op safety:** if `enabledModels` is missing or empty in pi's settings, scope check skips entirely — no false positives, no spurious errors.
664
664
 
665
- ## Persistent Settings
665
+ ## Persistent settings
666
666
 
667
667
  Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, nested depth, fallback agent, default model, default tier, default join mode, scheduling on/off, scope models on/off, strict agent files on/off, disable defaults on/off, output transcript on/off, tool description full/compact/custom, fleet view on/off) persist across pi restarts. Two files, merged on load:
668
668
 
@@ -839,7 +839,7 @@ pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });
839
839
  Reply channels are scoped per `requestId`, so concurrent requests don't interfere.
840
840
  Workflow callers should pass a foreground signal only to their own wait. Managed child cancellation is explicit through the owner-scoped lifecycle RPC; aborting an RPC wait does not abort the child.
841
841
 
842
- ## Persistent Agent Memory
842
+ ## Persistent agent memory
843
843
 
844
844
  Agents can have persistent memory across sessions. Set `memory` in frontmatter to enable:
845
845
 
@@ -861,7 +861,7 @@ Memory uses a `MEMORY.md` index file and individual memory files with frontmatte
861
861
 
862
862
  The `disallowed_tools` field is respected when determining write capability — an agent with `tools: write` + `disallowed_tools: write` correctly gets read-only memory.
863
863
 
864
- ## Worktree Isolation
864
+ ## Worktree isolation
865
865
 
866
866
  Set `isolation: worktree` to run an agent in a temporary git worktree:
867
867
 
@@ -881,7 +881,7 @@ If cleanup cannot be confirmed, the result keeps the worktree path and includes
881
881
 
882
882
  The child prompt also states that the temporary worktree is the only writable checkout and names the original base repository as off-limits. This prevents inherited parent instructions from sending the agent back into the shared tree.
883
883
 
884
- ## Skill Preloading
884
+ ## Skill preloading
885
885
 
886
886
  Skills can be preloaded by name and injected into the agent's system prompt:
887
887
 
@@ -911,7 +911,7 @@ Recursion skips dotfile directories and `node_modules`. A directory that itself
911
911
 
912
912
  **Security:** symlinks are rejected at every layer (root, flat file, skill directory, `SKILL.md` inside a skill directory) — intentional deviation from Pi, which follows symlinks. Skill names with path-traversal characters (`..`, `/`, `\`, spaces, leading dot, >128 chars) are rejected.
913
913
 
914
- ## Tool Denylist
914
+ ## Tool denylist
915
915
 
916
916
  Block specific tools from an agent even if extensions provide them:
917
917
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalridge/pi-subagents",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "Signalridge's managed subagent runtime with workflow-owned orchestration RPC.",
5
5
  "author": "tintinweb and signalridge contributors",
6
6
  "license": "MIT",
@@ -25,15 +25,15 @@
25
25
  "autonomous"
26
26
  ],
27
27
  "peerDependencies": {
28
- "@earendil-works/pi-ai": "^0.84.0",
29
- "@earendil-works/pi-coding-agent": "^0.84.0",
30
- "@earendil-works/pi-tui": "^0.84.0"
28
+ "@earendil-works/pi-ai": "^0.84.0 || ^0.85.0",
29
+ "@earendil-works/pi-coding-agent": "^0.84.0 || ^0.85.0",
30
+ "@earendil-works/pi-tui": "^0.84.0 || ^0.85.0"
31
31
  },
32
32
  "dependencies": {
33
33
  "@sinclair/typebox": "^0.34.50",
34
34
  "croner": "^10.0.1",
35
35
  "@signalridge/pi-subagents-protocol": "^1.5.0",
36
- "@signalridge/pi-ui": "^1.3.0",
36
+ "@signalridge/pi-ui": "^1.3.1",
37
37
  "nanoid": "^5.0.0"
38
38
  },
39
39
  "scripts": {
@@ -7,6 +7,7 @@ import { homedir } from "node:os";
7
7
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
8
8
  import type { Model } from "@earendil-works/pi-ai";
9
9
  import type { ExtensionContext, LoadExtensionsResult } from "@earendil-works/pi-coding-agent";
10
+ import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
10
11
  import {
11
12
  type AgentSession,
12
13
  type AgentSessionEvent,
@@ -641,6 +642,11 @@ export interface RunResult {
641
642
  */
642
643
  function collectResponseText(session: AgentSession) {
643
644
  let text = "";
645
+ // Invocation-owned events survive compaction replacing session history.
646
+ // Retain only the final message and latest nonempty text, not a transcript.
647
+ let lastMessage: AgentSession["messages"][number] | undefined;
648
+ let lastNonemptyText = "";
649
+ let beforeLastMessageText = "";
644
650
  const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
645
651
  // message_start also fires for user and toolResult messages — resetting on
646
652
  // those would wipe assistant text already collected. Reset only when a new
@@ -651,25 +657,33 @@ function collectResponseText(session: AgentSession) {
651
657
  if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
652
658
  text += event.assistantMessageEvent.delta;
653
659
  }
660
+ if (event.type === "message_end" && event.message.role === "assistant") {
661
+ beforeLastMessageText = lastNonemptyText;
662
+ lastMessage = event.message;
663
+ text = extractText(event.message.content);
664
+ // Failed attempts remain visible as the current result, but must never
665
+ // replace the valid fallback: Pi removes them when retrying. Compaction
666
+ // may already have removed the earlier valid message from history.
667
+ if (text.trim() && event.message.stopReason !== "error" && event.message.stopReason !== "aborted") {
668
+ lastNonemptyText = text.trim();
669
+ }
670
+ }
671
+ if (event.type === "compaction_end" && event.reason === "overflow" && !event.aborted && event.willRetry &&
672
+ lastMessage?.role === "assistant" && lastMessage.stopReason === "length") {
673
+ // A length result is legitimate until the SDK actually retries it. On
674
+ // overflow recovery Pi removes this candidate, including any occurrence
675
+ // restored by compaction AFTER this notification. Retract its fallback
676
+ // contribution from invocation state rather than rescanning that history.
677
+ lastNonemptyText = beforeLastMessageText;
678
+ text = "";
679
+ lastMessage = undefined;
680
+ }
654
681
  });
655
- return { getText: () => text, unsubscribe };
656
- }
657
-
658
- /**
659
- * Get the last non-empty assistant text produced during THIS invocation.
660
- * `startIndex` is the message count captured before the prompt, so the walk-back
661
- * never crosses into a previous turn: on a resume whose new turn failed empty,
662
- * this returns "" instead of the prior turn's answer (#144). Defaults to 0 (a
663
- * fresh spawn, where the whole history belongs to this run).
664
- */
665
- function getLastAssistantText(session: AgentSession, startIndex = 0): string {
666
- for (let i = session.messages.length - 1; i >= startIndex; i--) {
667
- const msg = session.messages[i];
668
- if (msg.role !== "assistant") continue;
669
- const text = extractText(msg.content).trim();
670
- if (text) return text;
671
- }
672
- return "";
682
+ return {
683
+ getText: () => text.trim() || lastNonemptyText,
684
+ getFailure: () => finalTurnError(lastMessage ? [lastMessage] : []),
685
+ unsubscribe,
686
+ };
673
687
  }
674
688
 
675
689
  /**
@@ -683,12 +697,11 @@ function getLastAssistantText(session: AgentSession, startIndex = 0): string {
683
697
  * Everything else completes: a clean "stop"/"toolUse" final, and — crucially — a
684
698
  * "length" stop that DID produce text (a legitimate truncated-but-useful answer).
685
699
  * "aborted" is handled by the manager's abort flag / "stopped" guard, not here.
686
- * Bounded by `startIndex` (like the text fallback) so a resume that produced no
687
- * assistant message of its own never inherits a PRIOR turn's stop reason.
700
+ * Invocation-owned messages exclude historical failures, even after compaction.
688
701
  */
689
- function finalTurnError(session: AgentSession, startIndex = 0): string | undefined {
690
- for (let i = session.messages.length - 1; i >= startIndex; i--) {
691
- const msg = session.messages[i];
702
+ function finalTurnError(messages: AgentSession["messages"]): string | undefined {
703
+ for (let i = messages.length - 1; i >= 0; i--) {
704
+ const msg = messages[i];
692
705
  if (msg.role !== "assistant") continue;
693
706
  if (msg.stopReason === "error") {
694
707
  return (msg as { errorMessage?: string }).errorMessage?.trim() || "provider error with no output";
@@ -984,6 +997,10 @@ export async function runAgent(
984
997
  // into the allowlist, then dropped it at registration with no signal back.
985
998
  if (agentConfig?.builtinToolNames?.length) {
986
999
  const knownBuiltins = new Set(BUILTIN_TOOL_NAMES);
1000
+ // Optional public capability: do not require a new named export on Pi 0.84.
1001
+ if ("createPowerShellTool" in PiCodingAgent && typeof Reflect.get(PiCodingAgent, "createPowerShellTool") === "function") {
1002
+ knownBuiltins.add("powershell");
1003
+ }
987
1004
  for (const name of agentConfig.builtinToolNames) {
988
1005
  if (!knownBuiltins.has(name)) {
989
1006
  options.onToolActivity?.({
@@ -1109,7 +1126,9 @@ export async function runAgent(
1109
1126
  const supervisorTools =
1110
1127
  ctx.hasUI && !isolated && options.supervisorQuestions !== false
1111
1128
  ? createSupervisorTool({
1112
- agentLabel: agentConfig?.displayName ?? type,
1129
+ // Use the canonical agent type, not display_name: the question must
1130
+ // identify the actual subagent that requested the decision.
1131
+ agentLabel: agentConfig?.name ?? type,
1113
1132
  ask: {
1114
1133
  input: (title, placeholder) => ctx.ui.input(title, placeholder),
1115
1134
  select: (title, choices) => ctx.ui.select(title, choices),
@@ -1167,6 +1186,10 @@ export async function runAgent(
1167
1186
  const denyTools = new Set<string>(
1168
1187
  EXCLUDED_TOOL_NAMES.filter((t) => !nestedToolNames.has(t)),
1169
1188
  );
1189
+ // Keep the historical wildcard defaults; PowerShell is explicit opt-in.
1190
+ if ("createPowerShellTool" in PiCodingAgent && typeof Reflect.get(PiCodingAgent, "createPowerShellTool") === "function" && !builtinToolNameSet.has("powershell")) {
1191
+ denyTools.add("powershell");
1192
+ }
1170
1193
  // Keep only the built-ins the agent asked for — deny the rest.
1171
1194
  for (const name of BUILTIN_TOOL_NAMES) {
1172
1195
  if (!builtinToolNameSet.has(name)) denyTools.add(name);
@@ -1392,6 +1415,9 @@ export async function runAgent(
1392
1415
  handle.unref?.();
1393
1416
  pendingToolTimeouts.set(toolCallId, handle);
1394
1417
  };
1418
+ // Own finalized output before observer dispatch: a synchronous callback
1419
+ // failure must remain visible without erasing the answer it interrupted.
1420
+ const collector = collectResponseText(session);
1395
1421
  const unsubTurns = session.subscribe((event: AgentSessionEvent) => {
1396
1422
  if (event.type === "turn_end") {
1397
1423
  turnCount++;
@@ -1442,8 +1468,6 @@ export async function runAgent(
1442
1468
  }
1443
1469
  });
1444
1470
 
1445
- const collector = collectResponseText(session);
1446
-
1447
1471
  // Build the effective prompt: optionally prepend parent context
1448
1472
  let effectivePrompt = prompt;
1449
1473
  const inheritContext = internalOverride?.inheritContext ?? options.inheritContext;
@@ -1454,9 +1478,6 @@ export async function runAgent(
1454
1478
  }
1455
1479
  }
1456
1480
 
1457
- // Boundary for the history fallback: only assistant text produced from here
1458
- // on counts as this run's output (a fresh session, so usually 0).
1459
- const startLen = session.messages.length;
1460
1481
  try {
1461
1482
  throwIfAborted(options.signal);
1462
1483
  await session.prompt(effectivePrompt);
@@ -1467,7 +1488,7 @@ export async function runAgent(
1467
1488
  pendingToolTimeouts.clear();
1468
1489
  }
1469
1490
 
1470
- const baseText = collector.getText().trim() || getLastAssistantText(session, startLen);
1491
+ const baseText = collector.getText();
1471
1492
  // The acceptance gate runs AFTER the agent is done and its verdict is
1472
1493
  // appended to the result, so the parent reads the check and the claim it is
1473
1494
  // checking side by side. It deliberately does not steer the agent to fix
@@ -1476,7 +1497,7 @@ export async function runAgent(
1476
1497
  ? `${baseText}${await runConfiguredGate(agentConfig.gate, effectiveCwd, options.pi)}`
1477
1498
  : baseText;
1478
1499
  completed = true;
1479
- return { responseText, session, aborted, steered: softLimitReached, failure: finalTurnError(session, startLen) };
1500
+ return { responseText, session, aborted, steered: softLimitReached, failure: collector.getFailure() };
1480
1501
  } finally {
1481
1502
  cleanupAbort();
1482
1503
  // The manager takes ownership only after onSessionCreated is invoked. If
@@ -1505,11 +1526,7 @@ export async function resumeAgent(
1505
1526
  signal?: AbortSignal;
1506
1527
  } = {},
1507
1528
  ): Promise<{ text: string; failure?: string }> {
1508
- // Boundary for the history fallback: the session already holds prior turns,
1509
- // so only assistant text produced by THIS resume prompt counts as its output
1510
- // — a failed resume must not surface the previous turn's answer (#144).
1511
1529
  throwIfAborted(options.signal);
1512
- const startLen = session.messages.length;
1513
1530
  const collector = collectResponseText(session);
1514
1531
  const cleanupAbort = forwardAbortSignal(session, options.signal);
1515
1532
 
@@ -1574,8 +1591,8 @@ export async function resumeAgent(
1574
1591
  }
1575
1592
 
1576
1593
  return {
1577
- text: collector.getText().trim() || getLastAssistantText(session, startLen),
1578
- failure: finalTurnError(session, startLen),
1594
+ text: collector.getText(),
1595
+ failure: collector.getFailure(),
1579
1596
  };
1580
1597
  }
1581
1598
 
package/src/supervisor.ts CHANGED
@@ -36,7 +36,7 @@ export interface SupervisorAsk {
36
36
  export interface SupervisorToolContext {
37
37
  /** The parent's UI. Omitted when there is no human to ask. */
38
38
  ask?: SupervisorAsk;
39
- /** Display name of the asking agent, for the prompt title. */
39
+ /** Actual type/name of the asking agent, for the prompt title. */
40
40
  agentLabel: string;
41
41
  }
42
42
 
@@ -82,7 +82,10 @@ export function createSupervisorTool(context: SupervisorToolContext) {
82
82
  const question = truncateCodePoints(sanitizeDisplayText(params.question), MAX_QUESTION, "…").trim();
83
83
  if (!question) return textResult("Ask a non-empty question.", true);
84
84
 
85
- const title = `${context.agentLabel} asks`;
85
+ const title = `${sanitizeDisplayText(context.agentLabel)} asks`;
86
+ // Pi 0.85.1 ignores ui.input()'s placeholder argument. Keep the
87
+ // question in the title so the human sees it in both old and new hosts.
88
+ const promptTitle = `${title}: ${question}`;
86
89
  const options = (params.options ?? [])
87
90
  .map((option) => truncateCodePoints(sanitizeDisplayText(option), MAX_OPTION, "…").trim())
88
91
  .filter((option) => option.length > 0)
@@ -92,8 +95,8 @@ export function createSupervisorTool(context: SupervisorToolContext) {
92
95
  try {
93
96
  answer =
94
97
  options.length > 0
95
- ? await ask.select(`${title}: ${question}`, options)
96
- : await ask.input(title, question);
98
+ ? await ask.select(promptTitle, options)
99
+ : await ask.input(promptTitle);
97
100
  } catch (error: unknown) {
98
101
  // A dialog that cannot open must not fail the child's whole run; it
99
102
  // is told nobody answered and carries on under its own judgement.