@aeondave/pi-persona 1.17.3 → 1.17.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/AGENTS.md +44 -36
  2. package/CHANGELOG.md +76 -1
  3. package/README.md +23 -13
  4. package/docs/ARCHITECTURE.md +97 -37
  5. package/docs/COLLABORATION.md +110 -0
  6. package/docs/EXPERIENCE.md +33 -8
  7. package/docs/README.md +12 -0
  8. package/docs/REFERENCE.md +65 -14
  9. package/package.json +12 -6
  10. package/src/agents/agent.ts +6 -0
  11. package/src/bridge.ts +25 -8
  12. package/src/bus/broker/client.ts +119 -21
  13. package/src/bus/broker/host.ts +123 -8
  14. package/src/bus/broker/messages.ts +31 -4
  15. package/src/bus/contact.ts +113 -38
  16. package/src/bus/peers.ts +15 -0
  17. package/src/bus/result-reader.ts +88 -0
  18. package/src/core/brief.ts +14 -4
  19. package/src/core/capabilities.ts +1 -1
  20. package/src/core/leg-thinking.ts +57 -0
  21. package/src/core/ownership.ts +1 -2
  22. package/src/core/pi-compat.ts +5 -12
  23. package/src/core/session-runs.ts +1 -1
  24. package/src/core/types.ts +1 -1
  25. package/src/engine/adapter.ts +83 -19
  26. package/src/engine/async.ts +49 -10
  27. package/src/engine/child.ts +10 -2
  28. package/src/engine/inproc.ts +87 -46
  29. package/src/engine/result-grant.ts +62 -0
  30. package/src/engine/session-tools.ts +6 -0
  31. package/src/engine/stream.ts +6 -0
  32. package/src/engine/worktree.ts +17 -7
  33. package/src/exocom/collaboration-delivery.ts +29 -0
  34. package/src/exocom/collaboration.ts +148 -0
  35. package/src/exocom/envelope.ts +43 -4
  36. package/src/exocom/gate.ts +7 -2
  37. package/src/exocom/install.ts +45 -17
  38. package/src/exocom/ledger.ts +58 -8
  39. package/src/exocom/plane.ts +32 -1
  40. package/src/exocom/profile.ts +40 -0
  41. package/src/exocom/registry.ts +5 -0
  42. package/src/exocom/status.ts +11 -1
  43. package/src/exocom/write-guard.ts +7 -0
  44. package/src/extension/broker-host.ts +61 -9
  45. package/src/extension/engine.ts +20 -21
  46. package/src/extension/hooks.ts +952 -938
  47. package/src/extension/identity.ts +41 -75
  48. package/src/extension/results.ts +136 -0
  49. package/src/extension/shared.ts +17 -0
  50. package/src/extension.ts +2963 -2873
  51. package/src/loader.ts +11 -3
  52. package/src/orchestration/flow-journal.ts +7 -1
  53. package/src/orchestration/flow-run.ts +122 -19
  54. package/src/orchestration/flow.ts +11 -0
  55. package/src/orchestration/roster.ts +71 -23
  56. package/src/orchestration/sdk.ts +44 -6
  57. package/src/orchestration/types.ts +10 -0
  58. package/src/persona/model-participants.ts +20 -7
  59. package/src/persona/orchestrate.ts +2 -0
  60. package/src/persona/persona.ts +1 -1
  61. package/src/results/archive.ts +281 -0
  62. package/src/results/record.ts +166 -0
  63. package/src/results/retrieval.ts +142 -0
  64. package/src/results/types.ts +128 -0
  65. package/src/telemetry/contract.ts +1 -1
  66. package/src/tools/council.ts +40 -23
  67. package/src/tools/delegate-tool.ts +50 -14
  68. package/src/tools/delegate.ts +13 -6
  69. package/src/tools/exocom-collaboration-input.ts +168 -0
  70. package/src/tools/exocom-collaboration.ts +315 -0
  71. package/src/tools/exocom-profile.ts +102 -0
  72. package/src/tools/exocom.ts +20 -3
  73. package/src/tools/flow.ts +24 -8
  74. package/src/tools/intercom-tool.ts +98 -14
  75. package/src/tools/models.ts +15 -0
  76. package/src/tools/monitor.ts +20 -1
  77. package/src/tools/read-result.ts +52 -0
  78. package/src/tools/timer.ts +23 -2
  79. package/src/ui/agent-overlay.ts +11 -3
  80. package/src/ui/agent-tree.ts +43 -1
  81. package/src/ui/literal-tool-result.ts +1 -1
  82. package/src/ui/model-picker.ts +1 -1
  83. package/src/ui/orchestration-launch.ts +49 -0
  84. package/src/ui/session-cards.ts +191 -14
@@ -10,6 +10,8 @@ import { randomUUID } from "node:crypto";
10
10
  import type { AgentConfig } from "../agents/agent.ts";
11
11
  import { type ContractDef, contractInstructions, parseAndValidate, pinContract, type PinnedContract } from "../core/contract.ts";
12
12
  import { sanitizeDisplayLabel } from "../core/display-label.ts";
13
+ import { resolveLegThinking } from "../core/leg-thinking.ts";
14
+ import type { ThinkingLevel } from "../core/types.ts";
13
15
  import { assignedIdentityPrompt } from "../core/naming.ts";
14
16
  import { roleHint } from "../orchestration/roster.ts";
15
17
  import { type AgentProgress, type AgentRunSpec, isPositiveFiniteMs, type SteerFn, type StrategyEngine } from "../orchestration/sdk.ts";
@@ -18,8 +20,13 @@ import { type ChildEngineOptions, type ChildRunSpec, runChildAgent } from "./chi
18
20
  import { looksLikeProviderError } from "./errors.ts";
19
21
  import { piHostModuleHint } from "./host-module.ts";
20
22
  import { nextChildHandle } from "./handles.ts";
23
+ import { admitResultReadTool, referenceHandoffFailure } from "./result-grant.ts";
24
+ import { makeScopedResultReader, type ResultArchiveView } from "../results/retrieval.ts";
25
+ import type { PreparedResultReader, ScopedReader } from "../bus/result-reader.ts";
21
26
  import { combineSignals } from "./signals.ts";
27
+ import { emptyUsage } from "./stream.ts";
22
28
  import { toolModifierFailure, unknownAgentFailure, unknownContractFailure } from "./spec-preflight.ts";
29
+ import { mergeSessionToolAllowlist } from "./session-tools.ts";
23
30
 
24
31
  /** The engine-side face of the cross-process broker (spec B3/B4/B7) — a child-engine
25
32
  * spawn's connection to the supervisor-hosted relay. The concrete implementation
@@ -40,6 +47,10 @@ export interface EngineAdapterBroker {
40
47
  * parent-side idle/startup watchdogs consult this before killing a silent leg (R9): a leg
41
48
  * blocked on the supervisor is re-armed, not killed. */
42
49
  hasPendingAskFrom(handle: string): boolean;
50
+ /** Strict, AWAITED result-reader preflight for a reference-handoff leg (the broker host binds
51
+ * a single-use token to this handle). Must complete BEFORE `register` and the spawn — a
52
+ * missing broker/binding fails the leg closed with zero usage instead of degrading bus-less. */
53
+ prepareResultReader(handle: string, reader: ScopedReader, signal?: AbortSignal): Promise<PreparedResultReader>;
43
54
  }
44
55
 
45
56
  export interface EngineAdapterDeps {
@@ -55,8 +66,7 @@ export interface EngineAdapterDeps {
55
66
  * the on-the-fly role this run carries, when it has one, so a member's own assignment key
56
67
  * (`agent#hash`) resolves ahead of the bare agent's. */
57
68
  modelFor?: (agent: string, role?: string) => string | undefined;
58
- /** Explicit thinking level appended to the child model (`model:level`) so it can't
59
- * fall into a model's default "adaptive" mode, which some models reject. */
69
+ /** Configured/inherited child effort, below explicit leg and agent selections. */
60
70
  childThinking?: string;
61
71
  /** The shared behavioral layer (docs/SPINE.md), prepended to every leg's prompt. This is the
62
72
  * WORKER variant (`prompts/spine.worker.md`), not the supervisor text — a leg runs headless
@@ -80,6 +90,11 @@ export interface EngineAdapterDeps {
80
90
  * blocking would deadlock: default false, mirroring `inproc.ts`'s `allowBlocking`
81
91
  * default. Forwarded to the child as `PI_PERSONA_ALLOW_BLOCKING` (bridge.ts). */
82
92
  allowBlocking?: boolean;
93
+ /** The owning session's terminal archive (a GETTER — the owner can change across sessions).
94
+ * Required by any leg carrying `resultRefs` (flow reference handoff): the scoped reader
95
+ * handed to the broker pages THIS archive, after a zero-usage preflight verified every
96
+ * granted ref is durably stored. Absent ⇒ such legs fail closed before any spawn. */
97
+ resultArchive?: () => ResultArchiveView | undefined;
83
98
  }
84
99
 
85
100
  export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
@@ -113,10 +128,38 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
113
128
  ): Promise<AgentResult> {
114
129
  const cfg = deps.resolveAgent(spec.agent);
115
130
  if (!cfg) return unknownAgentFailure(spec.agent, deps.listAgents?.() ?? []);
131
+ let thinkingRequested: ThinkingLevel | undefined;
132
+ try {
133
+ thinkingRequested = resolveLegThinking(spec, cfg, deps.childThinking);
134
+ } catch (error) {
135
+ return { agent: spec.agent, output: "", usage: emptyUsage(), ok: false, failureKind: "contract", error: `[${spec.agent}] ${error instanceof Error ? error.message : String(error)}` };
136
+ }
116
137
  const tools = spec.tools ?? cfg.tools;
117
138
  const invalidTools = toolModifierFailure(spec.agent, tools);
118
139
  if (invalidTools) return invalidTools;
119
140
 
141
+ // ── reference handoff preflight (contract 3/4) — BEFORE any broker work or spawn ──
142
+ // Admission uses the SAME shared rule as the in-process engine; storage is verified
143
+ // durable (session-entry); the broker binding is AWAITED before registration/spawn.
144
+ // Any miss is a zero-usage contract failure and the child is never invoked.
145
+ let admittedTools = tools;
146
+ let resultReader: ScopedReader | undefined;
147
+ if (spec.resultRefs !== undefined && spec.resultRefs.length > 0) {
148
+ const grant = admitResultReadTool({
149
+ specTools: spec.tools, cfgTools: cfg.tools, excludeTools: cfg.excludeTools,
150
+ canUseBus: deps.canUseBus ?? true,
151
+ });
152
+ if (!grant.ok) return referenceHandoffFailure(spec.agent, grant.error);
153
+ const archive = deps.resultArchive?.();
154
+ if (archive === undefined) return referenceHandoffFailure(spec.agent, "terminal result archive unavailable for reference handoff");
155
+ const scoped = makeScopedResultReader(archive, spec.resultRefs);
156
+ const { missing } = scoped.preflight();
157
+ if (missing.length > 0) return referenceHandoffFailure(spec.agent, `archived results unavailable: ${missing.join(", ")}`);
158
+ if (deps.broker === undefined) return referenceHandoffFailure(spec.agent, "cross-process broker reader unavailable for reference handoff");
159
+ resultReader = scoped;
160
+ admittedTools = grant.tools;
161
+ }
162
+
120
163
  // Resolved (and pinned) up front: the SAME def both instructs the member and
121
164
  // validates its output — an instruction/validation drift is impossible. A named
122
165
  // contract that cannot be resolved is a hard configuration error: running the leg
@@ -130,12 +173,10 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
130
173
  : spec.task;
131
174
  const task = contractDef ? `${withSkills}\n\n${contractInstructions(contractDef)}` : withSkills;
132
175
  const childSpec: ChildRunSpec = { task };
133
- let model = spec.model ?? deps.modelFor?.(spec.agent, spec.role) ?? cfg.model;
134
- // Append an explicit thinking level (model:level) unless one is already present —
135
- // a child without it defaults to "adaptive", which some models reject.
136
- if (model && deps.childThinking && !model.includes(":")) model = `${model}:${deps.childThinking}`;
176
+ const model = spec.model ?? deps.modelFor?.(spec.agent, spec.role) ?? cfg.model;
137
177
  if (model) childSpec.model = model;
138
- if (tools !== undefined) childSpec.tools = tools;
178
+ if (thinkingRequested !== undefined) childSpec.thinking = thinkingRequested;
179
+ if (admittedTools !== undefined) childSpec.tools = admittedTools;
139
180
  if (cfg.excludeTools && cfg.excludeTools.length > 0) childSpec.excludeTools = cfg.excludeTools;
140
181
  // The shared behavioral layer (docs/SPINE.md) + the agent's own persona + any
141
182
  // on-the-fly `role` specialisation from the spec. All three go out as
@@ -160,27 +201,46 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
160
201
 
161
202
  // Cross-process comm plane (spec B1-B7): absent broker ⇒ none of this runs, so
162
203
  // the spawn env + behaviour stay byte-identical to pre-broker pi-persona.
163
- // Present ⇒ mint a handle, register it with the host BEFORE
164
- // spawn, wire the child's env, and give the caller a steer function.
204
+ // Present ⇒ mint a handle, (for a reference-handoff leg) AWAIT the strict reader
205
+ // binding BEFORE registration, register the handle with the host BEFORE spawn, wire
206
+ // the child's env, and give the caller a steer function.
165
207
  let handle: string | undefined;
208
+ let preparedReader: PreparedResultReader | undefined;
166
209
  if (deps.broker) {
167
- handle = nextChildHandle(spec.agent);
210
+ const broker = deps.broker;
211
+ handle = nextChildHandle(spec.agent);
168
212
  const wantsPeers = spec.peers === true && (deps.canUseBus ?? true);
169
- const label = spec.name?.trim()
213
+ const busTools = (deps.canUseBus ?? true) ? ["contact_supervisor", ...(wantsPeers ? ["contact_peer"] : [])] : [];
214
+ const mergedTools = mergeSessionToolAllowlist(admittedTools, busTools);
215
+ if (mergedTools !== undefined) childSpec.tools = mergedTools;
216
+ const label = spec.name?.trim()
170
217
  ? sanitizeDisplayLabel(spec.name, handle)
171
218
  : spec.role ? `${handle} (${roleHint(spec.role)})` : handle;
172
- deps.broker.register({ handle, label, group: peerGroup, ...(wantsPeers ? { peers: true } : {}) });
219
+ if (resultReader !== undefined) {
220
+ try {
221
+ preparedReader = await broker.prepareResultReader(handle, resultReader, signal);
222
+ } catch (error) {
223
+ return referenceHandoffFailure(spec.agent, `broker result reader preparation failed: ${error instanceof Error ? error.message : String(error)}`);
224
+ }
225
+ childOptions.env = {
226
+ ...childOptions.env,
227
+ // Private spawn credential for the child's bridge (bridge.ts installs read_result
228
+ // only when this is set); scrubbed from the inherited env inside child.ts.
229
+ PI_PERSONA_RESULT_READER_TOKEN: preparedReader.token,
230
+ };
231
+ }
232
+ broker.register({ handle, label, group: peerGroup, ...(wantsPeers ? { peers: true } : {}) });
173
233
  childOptions.env = {
174
234
  ...childOptions.env,
175
- PI_PERSONA_BUS: deps.broker.endpoint,
235
+ PI_PERSONA_BUS: broker.endpoint,
176
236
  PI_PERSONA_HANDLE: handle,
237
+ PI_PERSONA_BUS_TOOLS: (deps.canUseBus ?? true) ? "on" : "off",
177
238
  ...(wantsPeers ? { PI_PERSONA_PEERS: "1" } : {}),
178
239
  ...(deps.allowBlocking ? { PI_PERSONA_ALLOW_BLOCKING: "1" } : {}),
179
240
  };
180
241
  // The parent-side watchdogs must not kill a leg that is genuinely waiting on the
181
242
  // supervisor's answer (R9, inproc parity) — ask the bus, don't guess from a static flag.
182
- childOptions.isBlocked = () => deps.broker!.hasPendingAskFrom(handle!);
183
- const broker = deps.broker;
243
+ childOptions.isBlocked = () => broker.hasPendingAskFrom(handle!);
184
244
  const h = handle;
185
245
  const steer: SteerFn = Object.assign((text: string) => broker.steerFrame(h, text), {
186
246
  isWaitingForSupervisor: () => broker.hasPendingAskFrom(h),
@@ -192,17 +252,21 @@ export function makeEngine(deps: EngineAdapterDeps): StrategyEngine {
192
252
  try {
193
253
  child = await runChildAgent(childSpec, signal, childOptions);
194
254
  } finally {
195
- // Unregister on EVERY settle path (success, failure, or an unexpected throw) —
196
- // a leaked handle would otherwise linger in the host's registries forever.
255
+ // Revoke the reader grant and unregister on EVERY settle path (success, failure, or
256
+ // an unexpected throw) — a leaked grant would outlive its leg; a leaked handle would
257
+ // otherwise linger in the host's registries forever.
258
+ preparedReader?.revoke();
197
259
  if (deps.broker && handle) deps.broker.unregister(handle);
198
260
  }
199
261
 
200
262
  const result: AgentResult = { agent: spec.agent, output: child.output, usage: child.usage, ok: child.ok };
201
- // The provider/id the child ran on (drop any `:thinking` suffix) — for the UI and
263
+ if (thinkingRequested !== undefined) result.thinkingRequested = thinkingRequested;
264
+ if (child.thinkingEffective !== undefined) result.thinkingEffective = child.thinkingEffective;
265
+ // The opaque provider/id the child ran on — for the UI and
202
266
  // as the seed of the provider-fallback chain on a provider failure. The child's
203
267
  // STREAM-REPORTED model wins over the requested spec: a resolved alias or a
204
268
  // provider-side redirect must seed the fallback chain with what actually ran.
205
- const modelUsed = (child.model ?? childSpec.model ?? spec.model ?? cfg.model)?.split(":")[0];
269
+ const modelUsed = child.model ?? childSpec.model ?? spec.model ?? cfg.model;
206
270
  if (modelUsed) result.modelUsed = modelUsed;
207
271
  // Diagnostic tag: agent · model ref · dynamic overrides. Same shape as inproc engine,
208
272
  // so failed follow-ups always say WHICH agent+model died and WHY.
@@ -11,13 +11,13 @@
11
11
 
12
12
  import type { ProgressSnapshot, ToolEvent } from "./stream.ts";
13
13
  import type { AgentResult } from "../orchestration/types.ts";
14
+ import { resultReceiptHint, type ResultReceipt } from "../results/retrieval.ts";
14
15
  import { DEFAULT_ASYNC_RETAIN } from "../core/config.ts";
15
16
  import { MAX_DISPLAY_LABEL_CHARS, sanitizeDisplayLabel, sanitizeStatusLabel } from "../core/display-label.ts";
16
17
  import { compactTokens } from "../core/format.ts";
18
+ import { isThinkingLevel } from "../core/types.ts";
17
19
  import { formatDuration } from "../core/time.ts";
18
20
 
19
- export { MAX_DISPLAY_LABEL_CHARS } from "../core/display-label.ts";
20
-
21
21
  /** "possibly stuck" — the soft stall signal shared by the operator card, the peek digest and the UI
22
22
  * badge. It is deliberately patient: a long scan, a big generation, or a blocking command shows no
23
23
  * visible progress yet is perfectly healthy, so every surface flags a leg only after a genuinely long quiet
@@ -33,6 +33,7 @@ export interface AsyncRun {
33
33
  status: "running" | "done" | "failed" | "stopped";
34
34
  progress: ProgressSnapshot;
35
35
  result?: AgentResult;
36
+ resultReceipt?: ResultReceipt;
36
37
  error?: string;
37
38
  /** The codename the launcher gave this run (e.g. "atlas-static") — the SAME name shown on the
38
39
  * agent-tree node for this run. When set, it's the canonical display name (over the bare
@@ -115,6 +116,7 @@ export class AsyncRunTracker {
115
116
  /** Ids whose payload was evicted while STILL UNCOLLECTED, oldest first (insertion-ordered Set). */
116
117
  private readonly droppedIds = new Set<string>();
117
118
  private readonly completeListeners: Array<(run: AsyncRun) => void> = [];
119
+ private readonly createListeners: Array<(run: AsyncRun) => void> = [];
118
120
  private seq = 0;
119
121
  private readonly maxRetained: number;
120
122
  private readonly now: () => number;
@@ -159,6 +161,9 @@ export class AsyncRunTracker {
159
161
  if (meta.label !== undefined) entry.label = sanitizeDisplayLabel(meta.label);
160
162
  if (meta.model !== undefined) entry.model = sanitizeDisplayLabel(meta.model, "model");
161
163
  this.runs.set(id, entry);
164
+ for (const cb of [...this.createListeners]) {
165
+ try { cb(entry); } catch { /* archival observation cannot prevent launch */ }
166
+ }
162
167
 
163
168
  const settle = (): void => this.settleOnce(entry);
164
169
  // `Promise.resolve(run(...))` routes BOTH a returned rejection AND a SYNCHRONOUS throw from
@@ -176,7 +181,9 @@ export class AsyncRunTracker {
176
181
  .then((result) => {
177
182
  if (entry.settled) return; // force-settled by the supervisor — drop the late natural result
178
183
  entry.status = result.ok ? "done" : result.failureKind === "abort" ? "stopped" : "failed";
179
- entry.result = result;
184
+ // Retain scalar metadata by value: an engine reusing/mutating its result object
185
+ // after settlement must not rewrite this run's requested/observed effort.
186
+ entry.result = { ...result, usage: { ...result.usage }, ...(result.resultReceipt ? { resultReceipt: { ...result.resultReceipt } } : {}) };
180
187
  if (!result.ok && result.error) entry.error = result.error;
181
188
  })
182
189
  .catch((err: unknown) => {
@@ -347,6 +354,14 @@ export class AsyncRunTracker {
347
354
  return batch === undefined || !this.running().some((r) => r.batch === batch);
348
355
  }
349
356
 
357
+ onCreate(cb: (run: AsyncRun) => void): () => void {
358
+ this.createListeners.push(cb);
359
+ return () => {
360
+ const i = this.createListeners.indexOf(cb);
361
+ if (i >= 0) this.createListeners.splice(i, 1);
362
+ };
363
+ }
364
+
350
365
  onComplete(cb: (run: AsyncRun) => void): () => void {
351
366
  this.completeListeners.push(cb);
352
367
  return () => {
@@ -363,10 +378,18 @@ export function dedupeRunsById(runs: AsyncRun[]): AsyncRun[] {
363
378
  return [...byId.values()];
364
379
  }
365
380
 
381
+ /** A terminal engine observation supersedes the launch choice; running/launch rows retain their snapshot. */
382
+ export function observedRunModel(run: Partial<Pick<AsyncRun, "status" | "result">>): string | undefined {
383
+ const model = run.status !== undefined && run.status !== "running" ? run.result?.modelUsed : undefined;
384
+ return model?.trim() ? model.split("/").map((part) => sanitizeDisplayLabel(part, "model", 256)).join("/").slice(0, 256) : undefined;
385
+ }
386
+
366
387
  /** One identity everywhere a tracked run is rendered: the launcher's codename when present,
367
- * otherwise the agent type, with the already-shortened model appended once. */
368
- export function runDisplayName(run: Pick<AsyncRun, "agent" | "label" | "model">): string {
369
- const model = run.model === undefined ? undefined : sanitizeDisplayLabel(run.model, "model", 24);
388
+ * otherwise the agent type, with the observed terminal model or launch model appended once. */
389
+ export function runDisplayName(run: Pick<AsyncRun, "agent" | "label" | "model"> & Partial<Pick<AsyncRun, "status" | "result">>): string {
390
+ const observed = observedRunModel(run);
391
+ const selected = observed ? (observed.split("/").pop() ?? observed).replace(/^claude-/, "") : run.model;
392
+ const model = selected === undefined ? undefined : sanitizeDisplayLabel(selected, "model", 24);
370
393
  if (model === undefined) return sanitizeDisplayLabel(run.label ?? run.agent);
371
394
  const suffix = ` · ${model}`;
372
395
  const nameLimit = Math.max(1, MAX_DISPLAY_LABEL_CHARS - suffix.length);
@@ -618,6 +641,25 @@ export function buildCompletionReport(runs: AsyncRun[], fence: (text: string) =>
618
641
  const stoppedCount = stopped.length > 0 ? `, ${stopped.length} stopped` : "";
619
642
  const head = `[pi-persona] ${runs.length} async run${runs.length === 1 ? "" : "s"} settled — ${done.length} done, ${failed.length} failed${stoppedCount}`;
620
643
  const blocks: string[] = [head];
644
+ const models = runs.flatMap((run) => {
645
+ const model = observedRunModel(run);
646
+ return model ? [`${run.id}: Observed model: ${model}`] : [];
647
+ });
648
+ if (models.length > 0) blocks.push(`\n${models.join("\n")}`);
649
+ const receipts = runs.slice(0, MAX_ASYNC_STATUS_ROWS).flatMap((run) => {
650
+ const hint = resultReceiptHint(run.resultReceipt);
651
+ return hint ? [`${run.id}: ${hint}`] : [];
652
+ });
653
+ if (receipts.length > 0) blocks.push(`\nTerminal reports (intercom results lists all retained references):\n${receipts.join("\n")}`);
654
+ // Common result metadata, outside untrusted worker prose, on inline and background paths.
655
+ // A host effort observation is a Pi request setting, not a measure of provider cognition.
656
+ const efforts = runs.flatMap((r) => {
657
+ const requested = r.result?.thinkingRequested;
658
+ const observed = r.result?.thinkingEffective;
659
+ if (!isThinkingLevel(requested) && !isThinkingLevel(observed)) return [];
660
+ return [`${r.id}: requested ${isThinkingLevel(requested) ? requested : "unknown"}; observed Pi effort ${isThinkingLevel(observed) ? observed : "unknown"}`];
661
+ });
662
+ if (efforts.length > 0) blocks.push(`\nThinking (not provider cognition):\n${efforts.join("\n")}`);
621
663
  for (const r of done) {
622
664
  // Only fence REAL output — an empty/whitespace result gets a plain "(no output)", never an
623
665
  // empty <fence></fence> shell (there's nothing untrusted to guard, and the empty block reads
@@ -660,7 +702,7 @@ export function buildCompletionReport(runs: AsyncRun[], fence: (text: string) =>
660
702
  // The cap cuts whole legs from the middle of a wide fan-out. Name every run in the tail, which the
661
703
  // cut keeps, so a cut leg is still fetchable by id instead of silently missing.
662
704
  const named = report.length > MAX_COMPLETION_REPORT_CHARS
663
- ? `${report}\n\nRun ids in this report: ${runs.slice(0, MAX_ASYNC_STATUS_ROWS).map((r) => r.id).join(", ")}`
705
+ ? `${report}\n\nRun ids in this report: ${runs.slice(0, MAX_ASYNC_STATUS_ROWS).map((r) => r.id).join(", ")}. Saved reports: intercom { action:"results" }; use result { ref, offset, limit } even if live handles were dropped.`
664
706
  : report;
665
707
  return boundCompletionSurface(named);
666
708
  }
@@ -697,9 +739,6 @@ export const MAX_EMPTY_RENDER_RETRIES = 3;
697
739
  export interface IdleNotifierDeps<T> {
698
740
  /** Whether the supervisor is idle (not streaming a turn). */
699
741
  isIdle: () => boolean;
700
- /** Legacy host-queue probe. Retained for source compatibility, but deliberately ignored: when
701
- * the supervisor is idle, starting a turn is what drains any orphaned host follow-ups. */
702
- hasPending?: () => boolean;
703
742
  /** Deliver the coalesced message. Async rejection is treated exactly like a synchronous throw:
704
743
  * the batch is restored at the front of the queue and retried. */
705
744
  deliver: (message: string) => void | Promise<void>;
@@ -10,11 +10,13 @@ import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
 
13
+ import type { ThinkingLevel } from "../core/types.ts";
13
14
  import { applyEvent, type ChildUsage, createStreamState, feedLines, type ProgressSnapshot, snapshot } from "./stream.ts";
14
15
 
15
16
  export interface ChildRunSpec {
16
17
  task: string;
17
18
  model?: string;
19
+ thinking?: ThinkingLevel;
18
20
  tools?: string[];
19
21
  excludeTools?: string[];
20
22
  systemPrompt?: string;
@@ -32,6 +34,7 @@ export interface ChildRunResult {
32
34
  /** True when the idle watchdog OR the hard wall-clock cap fired. Disjoint from `aborted`. */
33
35
  timedOut: boolean;
34
36
  model?: string;
37
+ thinkingEffective?: ThinkingLevel;
35
38
  stopReason?: string;
36
39
  errorMessage?: string;
37
40
  }
@@ -166,6 +169,7 @@ export async function runChildAgent(
166
169
 
167
170
  const args: string[] = ["--mode", "json", "-p", "--no-session"];
168
171
  if (spec.model) args.push("--model", spec.model);
172
+ if (spec.thinking !== undefined) args.push("--thinking", spec.thinking);
169
173
  if (spec.tools !== undefined) {
170
174
  if (spec.tools.length > 0) args.push("--tools", spec.tools.join(","));
171
175
  else args.push("--no-tools");
@@ -201,13 +205,16 @@ export async function runChildAgent(
201
205
  const inv = resolveInvocation(args);
202
206
  // A stale broker wiring in the SUPERVISOR's own environment must not leak into a
203
207
  // plain child: PI_PERSONA_BUS would boot the child's bridge against a dead (or
204
- // foreign) endpoint. The adapter re-adds these deliberately via opts.env when a
205
- // broker is actually live for THIS run.
208
+ // foreign) endpoint, and a stale reader token would arm a read_result tool nobody
209
+ // granted. The adapter re-adds these deliberately via opts.env when a broker (and a
210
+ // result-reader grant) is actually live for THIS run.
206
211
  const inherited = { ...process.env };
207
212
  delete inherited.PI_PERSONA_BUS;
208
213
  delete inherited.PI_PERSONA_HANDLE;
209
214
  delete inherited.PI_PERSONA_PEERS;
210
215
  delete inherited.PI_PERSONA_ALLOW_BLOCKING;
216
+ delete inherited.PI_PERSONA_BUS_TOOLS;
217
+ delete inherited.PI_PERSONA_RESULT_READER_TOKEN;
211
218
  const proc = spawn(inv.command, inv.args, {
212
219
  cwd: spec.cwd ?? process.cwd(),
213
220
  shell: false,
@@ -496,6 +503,7 @@ export async function runChildAgent(
496
503
  timedOut: timedOut || hardTimedOut || startupTimedOut,
497
504
  };
498
505
  if (state.model !== undefined) result.model = state.model;
506
+ if (state.thinkingEffective !== undefined) result.thinkingEffective = state.thinkingEffective;
499
507
  if (state.stopReason !== undefined) result.stopReason = state.stopReason;
500
508
  // errorMessage precedence: a timeout/abort is the *cause of death*, so label
501
509
  // it explicitly and fold in any stream-level error rather than letting the