@zachwill/pi-orchestrate 0.15.0 → 0.16.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pi Orchestrate
2
2
 
3
- [`@zachwill/pi-orchestrate`](https://www.npmjs.com/package/@zachwill/pi-orchestrate) lets a Pi session delegate work to direct child sessions.
3
+ [`@zachwill/pi-orchestrate`](https://www.npmjs.com/package/@zachwill/pi-orchestrate) lets a Pi session delegate work to direct child sessions. It requires Pi 0.85.0 or newer.
4
4
 
5
5
  - Each worker gets a focused brief and a separate conversation.
6
6
  - Workers run independently and return their results to the parent.
@@ -23,6 +23,14 @@ A **worker ID** identifies a worker session. A **run ID** identifies one generat
23
23
 
24
24
  Interactive workers remain available across session switches and extension reloads within the same Pi process. Closing one releases its retained session; process shutdown releases any that remain.
25
25
 
26
+ ## Compaction
27
+
28
+ Parent compaction does not stop workers. Results that settle during compaction remain queued for the owning session and resume automatic delivery when that session becomes idle, including after failed or cancelled compaction. Grouped work still triggers synthesis only after the group settles.
29
+
30
+ Before each parent model request, the extension adds a fresh, owner-scoped snapshot of active workers and ready interactive sessions. This transient context survives compaction by being rebuilt from live state; it is not appended to the transcript. The snapshot is capped at 12 KiB and reports omitted workers or truncated assignments, with `worker_status` available for recovery rather than polling.
31
+
32
+ This state belongs to the running Pi process. Compaction does not require restarting workers, and persisted transcripts do not restore running workers after a process restart.
33
+
26
34
  ## Agent interface
27
35
 
28
36
  Pi Orchestrate gives the parent five model-facing tools:
@@ -1,15 +1,13 @@
1
1
  ---
2
2
  name: investigator
3
- description: Investigates cross-file questions and synthesizes grounded evidence.
3
+ description: Investigates cross-file questions through read-only inspection and evidence-based synthesis.
4
4
  thinking: medium
5
5
  tools: read, grep, find, ls, bash
6
6
  lifecycle: one-shot
7
7
  ---
8
8
 
9
- Investigate the assigned cross-file question through read-only inspection, comparison, and evidence synthesis.
9
+ Investigate the assigned question, tracing relevant relationships and comparing evidence across files. Stay within scope and stop when the evidence supports an answer; do not keep exploring for completeness.
10
10
 
11
- Do not modify files or run builds, tests, or commands that mutate state. Use bash only for read-only commands.
11
+ Do not modify files or run builds, tests, or other state-changing commands. Use bash only for read-only inspection.
12
12
 
13
- Ground each finding in file paths, line ranges, or symbols. Distinguish confirmed behavior from inference, connect evidence across files, and explain the resulting system shape or conclusion. Provide grounded recommendations when the assignment requests them.
14
-
15
- Return concise **Findings** and **Synthesis** sections. Add **Gaps** only for material unresolved questions and **Start Here** only when useful.
13
+ Lead with the answer. Support material findings and requested recommendations with exact paths, line ranges, or symbols. Explain how the evidence supports the conclusion, distinguish facts from inference, and identify unresolved gaps that could change the answer.
@@ -1,15 +1,13 @@
1
1
  ---
2
2
  name: scout
3
- description: Answers one small factual repository question with read-only evidence.
4
- thinking: medium
3
+ description: Answers a small factual repository question through shallow, read-only inspection.
4
+ thinking: low
5
5
  tools: read, grep, find, ls, bash
6
6
  lifecycle: one-shot
7
7
  ---
8
8
 
9
- Answer one small factual probe through fast, shallow, read-only repository inspection.
9
+ Answer the assigned factual question using direct repository evidence. Keep the inspection shallow and stop as soon as the question is answerable. If it requires deeper investigation, return what you found and explain what remains rather than expanding the task.
10
10
 
11
- Do not modify files or run builds, tests, or commands that mutate state. Use bash only for read-only commands.
11
+ Do not modify files or run builds, tests, or other state-changing commands. Use bash only for read-only inspection.
12
12
 
13
- Accept one path, symbol, command output, short inventory, direct comparison, or existence check. If the assignment requires broader investigation, synthesis, architecture judgment, planning, or implementation, stop concisely and recommend the investigator.
14
-
15
- Return a short **Answer** and **Evidence** grounded in paths, line ranges, symbols, or command output. Add **Gaps** only when material.
13
+ Lead with a short answer and cite the paths, line ranges, symbols, or command output needed to support it. Distinguish confirmed facts from inference and identify missing evidence that affects the answer.
@@ -8,19 +8,13 @@ skills: []
8
8
  lifecycle: one-shot
9
9
  ---
10
10
 
11
- You are a web research worker. Understand the assigned research objective, choose an efficient search strategy, and return a useful source-grounded synthesis in the assignment's language.
11
+ Answer the assigned question using public web sources.
12
12
 
13
13
  Use the installed, authenticated `codex` CLI as your web-search backend. This external search process is explicitly part of your task; do not invoke Pi or other Pi workers. Do not modify project files or install anything. Use fresh temporary directories and clean them up.
14
14
 
15
15
  ## Strategy
16
16
 
17
- Use your judgment:
18
-
19
- - For a narrow lookup, run one focused Codex search.
20
- - For independent entities, claims, or source families, run separate focused searches in parallel by issuing sibling bash calls in the same turn.
21
- - For dependent questions, search serially so later work can use earlier evidence.
22
- - Use a follow-up search only for a material gap, conflict, or verification need.
23
- - Stop when the objective is adequately answered. Do not multiply searches for cosmetic coverage.
17
+ Use one focused search for a narrow lookup. Search independent angles in parallel and dependent questions serially. Follow up only to resolve a material gap, conflict, or verification need. Stop when the question is adequately answered.
24
18
 
25
19
  Tell each Codex process to use at most four actual web searches unless the assignment justifies a different bound. Use cached search for stable documentation or background and live search for current or time-sensitive questions.
26
20
 
@@ -74,11 +68,6 @@ Never use `--dangerously-bypass-approvals-and-sandbox`. Retry only when diagnost
74
68
 
75
69
  ## Response
76
70
 
77
- Return a concise synthesis that directly serves the assignment. Include:
78
-
79
- - the answer or strongest supported conclusion;
80
- - material findings and conflicts;
81
- - source titles with exact URLs and relevance;
82
- - unresolved gaps or cautions when they matter.
71
+ Lead with the strongest supported answer. Cite exact source URLs alongside material factual claims. Identify conflicts, uncertainty, and freshness limits that affect the conclusion.
83
72
 
84
73
  Do not dump search transcripts or raw temporary paths. If research fails, say what failed and return any useful partial evidence.
@@ -1,18 +1,17 @@
1
1
  ---
2
2
  name: worker
3
- description: Implements a bounded change within explicit scope and acceptance criteria.
3
+ description: Implements bounded code changes, fixes, and refactors.
4
4
  thinking: medium
5
5
  tools: read, bash, edit, write, grep, find, ls
6
6
  lifecycle: one-shot
7
7
  ---
8
8
 
9
- Implement the assigned change within its stated scope.
9
+ Complete the assigned change within its scope. Follow project instructions, inspect nearby code, and preserve changes you do not own. Report relevant out-of-scope findings rather than fixing them.
10
10
 
11
- - Follow loaded project conventions. Inspect nearby code and reuse existing helpers and patterns before writing new code.
12
- - Change only what the assignment requires. Do not fix, refactor, or investigate unrelated work.
13
- - Do not commit, push, or perform destructive actions unless assigned.
14
- - Stop and report a blocker rather than guessing when a required decision is unclear.
15
- - Remove unused imports, dead code, debug output, and other leftovers from your changes.
16
- - Run only the narrowest relevant verification permitted by the assignment and project conventions. Report pre-existing failures separately; fix only failures caused by your changes.
11
+ Resolve uncertainty through inspection where possible. Make reasonable, reversible decisions within the assignment; report a blocker when progress requires missing authority or a material decision the assignment does not resolve.
17
12
 
18
- Return concise sections for **Completed**, **Files Changed**, and **Verification**. Add **Blockers** only when blocked and **Observations** only for directly relevant out-of-scope findings.
13
+ Run checks that establish whether the change works, following the assignment and project requirements. Fix failures introduced by your work and remove leftovers from your changes. Report pre-existing failures separately.
14
+
15
+ Lead with the result. Include changed paths, verification performed, and anything unresolved. Use the structure the handoff needs rather than a fixed template.
16
+
17
+ Do not commit, push, deploy, or take destructive action unless explicitly authorized.
@@ -9,6 +9,10 @@ import {
9
9
  } from "./catalog/discovery.ts";
10
10
  import type { WorkerCatalog } from "./catalog/definition.ts";
11
11
  import { applyOrchestratorContract } from "./parent/contract.ts";
12
+ import {
13
+ projectLiveWorkerContext,
14
+ replaceLiveWorkerContext,
15
+ } from "./parent/worker-context.ts";
12
16
  import {
13
17
  attachProcessHost,
14
18
  createProcessHost,
@@ -110,6 +114,52 @@ export function createOrchestrationExtension(
110
114
  };
111
115
  });
112
116
 
117
+ pi.on("context", async (event, ctx) => {
118
+ const messages = replaceLiveWorkerContext(event.messages, undefined);
119
+ const binding = activeBinding;
120
+ const boundHost = host;
121
+ const attachment = hostAttachment;
122
+
123
+ if (!binding || !boundHost || !attachment) return { messages };
124
+
125
+ try {
126
+ if (
127
+ attachment.host !== boundHost ||
128
+ ctx.sessionManager.getSessionId() !== binding.ownerSessionId
129
+ ) {
130
+ return { messages };
131
+ }
132
+
133
+ const snapshot = await boundHost.orchestration.snapshot(
134
+ binding.ownerSessionId,
135
+ );
136
+
137
+ // Session replacement and reload can race the awaited snapshot. Only the
138
+ // exact attachment generation that requested it may add parent context.
139
+ if (
140
+ activeBinding !== binding ||
141
+ host !== boundHost ||
142
+ hostAttachment !== attachment ||
143
+ attachment.host !== boundHost ||
144
+ ctx.sessionManager.getSessionId() !== binding.ownerSessionId
145
+ ) {
146
+ return { messages };
147
+ }
148
+
149
+ const context = projectLiveWorkerContext(snapshot, {
150
+ ownerSessionId: binding.ownerSessionId,
151
+ pendingResultCount: boundHost.delivery.pendingCount(
152
+ binding.ownerSessionId,
153
+ ),
154
+ });
155
+ return { messages: replaceLiveWorkerContext(messages, context) };
156
+ } catch {
157
+ // A context projection is advisory. On snapshot or stale-context failure,
158
+ // omit it rather than retaining an older authority-bearing projection.
159
+ return { messages };
160
+ }
161
+ });
162
+
113
163
  pi.on("message_end", (event) => {
114
164
  if (event.message.role !== "assistant") return;
115
165
  const toolCalls = event.message.content.filter(
@@ -125,6 +125,7 @@ You are responsible for delivering the user’s requested outcome. Work directly
125
125
  ### Completion
126
126
 
127
127
  - After dispatching, wait for automatic result delivery instead of polling \`worker_status\`. Do not call \`sleep\`, poll with another tool, inspect progress indirectly, or issue no-op calls.
128
+ - Compaction does not stop workers. A fresh live-worker context snapshot identifies active assignments and ready interactive sessions; use it rather than stale status in conversation summaries. Do not redispatch work because its dispatch was compacted away. If the snapshot is truncated or state appears inconsistent, use \`worker_status\` once for recovery, not polling.
128
129
  - While waiting, perform only already-admitted independent work from the current change; otherwise end the turn.
129
130
  - Classify findings before acting: fix or remove defects introduced by the current change, complete unfinished requirements inside its boundary, and record adjacent or pre-existing concerns without admitting them.
130
131
  - Dispatch another wave only for admitted work inside the current change. Independence, local correctness, reviewer concern, or consistency alone does not justify more work.
@@ -10,6 +10,7 @@ export const DELIVERY_PARENT_INSTRUCTIONS =
10
10
  "Parent: Synthesize all results, resolve conflicts, review changes and evidence, run integration checks, and continue the user's task. Do not merely forward worker reports.";
11
11
 
12
12
  export type ParentBindingGeneration = string | number | symbol;
13
+ export type ScheduleIdleRecheck = (recheck: () => void) => () => void;
13
14
 
14
15
  export interface WorkerDeliveryMessage {
15
16
  readonly customType: "pi-orchestrate-worker-result";
@@ -34,6 +35,11 @@ interface BoundParent {
34
35
  agentRunning: boolean;
35
36
  }
36
37
 
38
+ interface ScheduledIdleRecheck {
39
+ readonly generation: ParentBindingGeneration;
40
+ cancel(): void;
41
+ }
42
+
37
43
  interface SynthesisGroupState {
38
44
  expected: number;
39
45
  readonly acceptedEventIds: string[];
@@ -63,21 +69,30 @@ export class DeliveryCoordinator implements DeliveryService {
63
69
  private readonly boundParents = new Map<string, BoundParent>();
64
70
  private readonly pendingSettlements: WorkerSettlement[] = [];
65
71
  private readonly flushingOwners = new Set<string>();
72
+ private readonly idleRechecks = new Map<string, ScheduledIdleRecheck>();
66
73
  private readonly synthesisGroups = new Map<string, SynthesisGroupState>();
67
74
  // Orchestration settlement sequences are process-scoped and monotonic across
68
75
  // owners, so one watermark is valid.
69
76
  private highestAcceptedSequence = 0;
70
77
 
78
+ constructor(
79
+ private readonly scheduleIdleRecheck: ScheduleIdleRecheck = scheduleDeliveryIdleRecheck,
80
+ ) {}
81
+
71
82
  bind(binding: ParentBinding): void {
83
+ this.cancelIdleRecheck(binding.ownerSessionId);
72
84
  this.boundParents.set(binding.ownerSessionId, {
73
85
  binding,
74
- agentRunning: !binding.isIdle(),
86
+ // Non-idle also covers manual compaction. Agent lifecycle events, rather
87
+ // than the broader idle flag, own this state.
88
+ agentRunning: false,
75
89
  });
76
90
  this.flush(binding.ownerSessionId, binding.generation);
77
91
  }
78
92
 
79
93
  unbind(ownerSessionId: string, generation: ParentBindingGeneration): void {
80
94
  if (!this.matchesBinding(ownerSessionId, generation)) return;
95
+ this.cancelIdleRecheck(ownerSessionId, generation);
81
96
  this.boundParents.delete(ownerSessionId);
82
97
  }
83
98
 
@@ -85,6 +100,7 @@ export class DeliveryCoordinator implements DeliveryService {
85
100
  const parent = this.boundParents.get(ownerSessionId);
86
101
  if (parent?.binding.generation !== generation) return;
87
102
  parent.agentRunning = true;
103
+ this.cancelIdleRecheck(ownerSessionId, generation);
88
104
  }
89
105
 
90
106
  markAgentSettled(ownerSessionId: string, generation: ParentBindingGeneration): void {
@@ -130,6 +146,8 @@ export class DeliveryCoordinator implements DeliveryService {
130
146
  }
131
147
 
132
148
  clear(): void {
149
+ for (const recheck of this.idleRechecks.values()) recheck.cancel();
150
+ this.idleRechecks.clear();
133
151
  this.boundParents.clear();
134
152
  this.pendingSettlements.length = 0;
135
153
  this.flushingOwners.clear();
@@ -146,18 +164,28 @@ export class DeliveryCoordinator implements DeliveryService {
146
164
 
147
165
  private canDeliver(ownerSessionId: string, generation: ParentBindingGeneration): boolean {
148
166
  const parent = this.boundParents.get(ownerSessionId);
149
- return (
150
- parent !== undefined &&
151
- parent.binding.generation === generation &&
152
- !parent.agentRunning &&
153
- parent.binding.isIdle()
154
- );
167
+ if (
168
+ parent === undefined ||
169
+ parent.binding.generation !== generation ||
170
+ parent.agentRunning
171
+ ) {
172
+ return false;
173
+ }
174
+ if (!parent.binding.isIdle()) {
175
+ this.ensureIdleRecheck(ownerSessionId, generation);
176
+ return false;
177
+ }
178
+ this.cancelIdleRecheck(ownerSessionId, generation);
179
+ return true;
155
180
  }
156
181
 
157
182
  private flush(ownerSessionId: string, generation: ParentBindingGeneration): void {
158
- if (this.flushingOwners.has(ownerSessionId) || !this.canDeliver(ownerSessionId, generation)) {
183
+ if (this.flushingOwners.has(ownerSessionId)) return;
184
+ if (this.pendingCount(ownerSessionId) === 0) {
185
+ this.cancelIdleRecheck(ownerSessionId, generation);
159
186
  return;
160
187
  }
188
+ if (!this.canDeliver(ownerSessionId, generation)) return;
161
189
 
162
190
  this.flushingOwners.add(ownerSessionId);
163
191
  try {
@@ -216,6 +244,36 @@ export class DeliveryCoordinator implements DeliveryService {
216
244
  }
217
245
  }
218
246
 
247
+ private ensureIdleRecheck(
248
+ ownerSessionId: string,
249
+ generation: ParentBindingGeneration,
250
+ ): void {
251
+ const current = this.idleRechecks.get(ownerSessionId);
252
+ if (current?.generation === generation) return;
253
+ current?.cancel();
254
+
255
+ const scheduled: ScheduledIdleRecheck = {
256
+ generation,
257
+ cancel: () => {},
258
+ };
259
+ this.idleRechecks.set(ownerSessionId, scheduled);
260
+ scheduled.cancel = this.scheduleIdleRecheck(() => {
261
+ if (this.idleRechecks.get(ownerSessionId) !== scheduled) return;
262
+ this.idleRechecks.delete(ownerSessionId);
263
+ this.flush(ownerSessionId, generation);
264
+ });
265
+ }
266
+
267
+ private cancelIdleRecheck(
268
+ ownerSessionId: string,
269
+ generation?: ParentBindingGeneration,
270
+ ): void {
271
+ const scheduled = this.idleRechecks.get(ownerSessionId);
272
+ if (!scheduled || (generation !== undefined && scheduled.generation !== generation)) return;
273
+ this.idleRechecks.delete(ownerSessionId);
274
+ scheduled.cancel();
275
+ }
276
+
219
277
  private renderWorkerMessage(
220
278
  settlement: WorkerSettlement,
221
279
  byteLimit: number,
@@ -302,6 +360,12 @@ export const deliveryLayer: Layer.Layer<Delivery, never, Orchestration> = Layer.
302
360
  }),
303
361
  );
304
362
 
363
+ function scheduleDeliveryIdleRecheck(recheck: () => void): () => void {
364
+ const timeout = setTimeout(recheck, 100);
365
+ timeout.unref();
366
+ return () => clearTimeout(timeout);
367
+ }
368
+
305
369
  function synthesisGroupKey(ownerSessionId: string, synthesisGroupId: string): string {
306
370
  return `${ownerSessionId}\u0000${synthesisGroupId}`;
307
371
  }
@@ -0,0 +1,196 @@
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import type {} from "@earendil-works/pi-coding-agent";
3
+ import type { WorkerRecord } from "../orchestration/model.ts";
4
+ import type { OwnerSnapshot } from "../orchestration/service.ts";
5
+
6
+ export const LIVE_WORKER_CONTEXT_TYPE = "pi-orchestrate-live-worker-context";
7
+ export const MAX_LIVE_WORKER_CONTEXT_BYTES = 12 * 1024;
8
+ export const MAX_LIVE_WORKER_ITEM_BYTES = 1024;
9
+ export const MAX_LIVE_WORKER_ASSIGNMENT_BYTES = 480;
10
+
11
+ const ACTIVE_STATUSES = new Set<WorkerRecord["status"]>([
12
+ "starting",
13
+ "running",
14
+ "stopping",
15
+ ]);
16
+ const ITEM_TRUNCATION_MARKER =
17
+ "\n [entry truncated; use worker_status once for diagnostics/recovery]";
18
+ const ASSIGNMENT_TRUNCATION_MARKER = " … [assignment excerpt truncated]";
19
+
20
+ type CustomMessage = Extract<AgentMessage, { readonly role: "custom" }>;
21
+
22
+ export interface LiveWorkerContextOptions {
23
+ readonly ownerSessionId: string;
24
+ readonly pendingResultCount: number;
25
+ readonly timestamp?: number;
26
+ }
27
+
28
+ /** Project only current actionable process state; terminal history and outcomes stay out. */
29
+ export function projectLiveWorkerContext(
30
+ snapshot: OwnerSnapshot,
31
+ options: LiveWorkerContextOptions,
32
+ ): CustomMessage | undefined {
33
+ const workers = snapshot.workers.filter((worker) =>
34
+ worker.ownerSessionId === options.ownerSessionId && isActionable(worker)
35
+ );
36
+ const pendingResultCount = normalizeCount(options.pendingResultCount);
37
+ if (workers.length === 0 && pendingResultCount === 0) return undefined;
38
+
39
+ const activeCount = workers.filter((worker) => ACTIVE_STATUSES.has(worker.status)).length;
40
+ const readyCount = workers.length - activeCount;
41
+ const items = workers.map(renderWorkerItem);
42
+ const content = renderBoundedContext(
43
+ items,
44
+ activeCount,
45
+ readyCount,
46
+ pendingResultCount,
47
+ );
48
+
49
+ return {
50
+ role: "custom",
51
+ customType: LIVE_WORKER_CONTEXT_TYPE,
52
+ content,
53
+ display: false,
54
+ timestamp: options.timestamp ?? Date.now(),
55
+ };
56
+ }
57
+
58
+ /** Replace this extension's transient projection while preserving every unrelated message. */
59
+ export function replaceLiveWorkerContext(
60
+ messages: readonly AgentMessage[],
61
+ context: CustomMessage | undefined,
62
+ ): AgentMessage[] {
63
+ const retained = messages.filter((message) =>
64
+ message.role !== "custom" || message.customType !== LIVE_WORKER_CONTEXT_TYPE
65
+ );
66
+ return context ? [...retained, context] : retained;
67
+ }
68
+
69
+ function isActionable(worker: WorkerRecord): boolean {
70
+ return ACTIVE_STATUSES.has(worker.status) ||
71
+ (worker.status === "ready" && worker.lifecycle === "interactive");
72
+ }
73
+
74
+ function renderWorkerItem(worker: WorkerRecord): string {
75
+ const metadata = [
76
+ `worker_id=${quote(worker.id)}`,
77
+ `run_id=${quote(worker.runId)}`,
78
+ `definition=${quote(normalizeText(worker.worker))}`,
79
+ `title=${quote(normalizeText(worker.title))}`,
80
+ `lifecycle=${worker.lifecycle}`,
81
+ `status=${worker.status}`,
82
+ ].join(" | ");
83
+ const assignment = capUtf8(
84
+ normalizeText(worker.instructions),
85
+ MAX_LIVE_WORKER_ASSIGNMENT_BYTES,
86
+ ASSIGNMENT_TRUNCATION_MARKER,
87
+ );
88
+ const item = `- ${metadata}\n assignment=${quote(assignment)}`;
89
+ return capUtf8(item, MAX_LIVE_WORKER_ITEM_BYTES, ITEM_TRUNCATION_MARKER);
90
+ }
91
+
92
+ function renderBoundedContext(
93
+ items: readonly string[],
94
+ activeCount: number,
95
+ readyCount: number,
96
+ pendingResultCount: number,
97
+ ): string {
98
+ for (let shown = items.length; shown >= 0; shown -= 1) {
99
+ const content = renderContext(
100
+ items.slice(0, shown),
101
+ items.length,
102
+ activeCount,
103
+ readyCount,
104
+ pendingResultCount,
105
+ );
106
+ if (utf8Bytes(content) <= MAX_LIVE_WORKER_CONTEXT_BYTES) return content;
107
+ }
108
+
109
+ // Fixed guidance is intentionally far below the global limit. Keep a defensive
110
+ // cap so future copy changes cannot violate the public bound.
111
+ return capUtf8(
112
+ renderContext([], items.length, activeCount, readyCount, pendingResultCount),
113
+ MAX_LIVE_WORKER_CONTEXT_BYTES,
114
+ "\n[Live worker context truncated; use worker_status once for recovery.]",
115
+ );
116
+ }
117
+
118
+ function renderContext(
119
+ shownItems: readonly string[],
120
+ totalWorkers: number,
121
+ activeCount: number,
122
+ readyCount: number,
123
+ pendingResultCount: number,
124
+ ): string {
125
+ const sections = [
126
+ "## Pi Orchestrate live worker context",
127
+ "Authoritative transient process snapshot for this parent provider call; it is not conversation history.",
128
+ `Relevant owned workers: ${totalWorkers} (${activeCount} active, ${readyCount} ready interactive).`,
129
+ ];
130
+
131
+ if (shownItems.length > 0) {
132
+ sections.push(shownItems.join("\n"));
133
+ }
134
+ if (shownItems.length < totalWorkers) {
135
+ const omitted = totalWorkers - shownItems.length;
136
+ sections.push(
137
+ `Snapshot overflow: showing ${shownItems.length} of ${totalWorkers}; ${omitted} worker${omitted === 1 ? "" : "s"} omitted by the ${MAX_LIVE_WORKER_CONTEXT_BYTES}-byte limit. Use worker_status once for diagnostics/recovery to inspect omitted worker IDs and current state; do not poll.`,
138
+ );
139
+ }
140
+ if (pendingResultCount > 0) {
141
+ sections.push(
142
+ `Pending delivery: ${pendingResultCount} settled worker result${pendingResultCount === 1 ? "" : "s"} await automatic delivery. Do not redispatch that work.`,
143
+ );
144
+ }
145
+
146
+ const guidance: string[] = [];
147
+ if (activeCount > 0) {
148
+ guidance.push(
149
+ "Do not duplicate active assignments. Wait for automatic result delivery; do not poll worker_status for completion.",
150
+ );
151
+ }
152
+ if (readyCount > 0) {
153
+ guidance.push(
154
+ "Ready interactive sessions are retained: use interactive_send with the worker_id for follow-up, or interactive_close when finished.",
155
+ );
156
+ }
157
+ guidance.push(
158
+ "Use worker_status only for diagnostics or recovery when this snapshot reports overflow or state appears inconsistent.",
159
+ );
160
+ sections.push(`Guidance:\n- ${guidance.join("\n- ")}`);
161
+
162
+ return sections.join("\n\n");
163
+ }
164
+
165
+ function normalizeText(value: string): string {
166
+ return value.replace(/\s+/gu, " ").trim();
167
+ }
168
+
169
+ function quote(value: string): string {
170
+ return JSON.stringify(value);
171
+ }
172
+
173
+ function normalizeCount(value: number): number {
174
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
175
+ }
176
+
177
+ function utf8Bytes(value: string): number {
178
+ return Buffer.byteLength(value, "utf8");
179
+ }
180
+
181
+ function capUtf8(value: string, byteLimit: number, marker: string): string {
182
+ if (utf8Bytes(value) <= byteLimit) return value;
183
+ const markerBytes = utf8Bytes(marker);
184
+ if (markerBytes >= byteLimit) return truncateUtf8(marker, byteLimit);
185
+ return `${truncateUtf8(value, byteLimit - markerBytes)}${marker}`;
186
+ }
187
+
188
+ function truncateUtf8(value: string, byteLimit: number): string {
189
+ if (byteLimit <= 0) return "";
190
+ const bytes = Buffer.from(value, "utf8");
191
+ if (bytes.byteLength <= byteLimit) return value;
192
+
193
+ let end = byteLimit;
194
+ while (end > 0 && (bytes[end] ?? 0) >> 6 === 0b10) end -= 1;
195
+ return bytes.subarray(0, end).toString("utf8");
196
+ }
@@ -674,7 +674,14 @@ const prepareChildModelRuntime = Effect.fn("WorkerSession.prepareChildModelRunti
674
674
  yield* Effect.tryPromise({
675
675
  try: async () => {
676
676
  if (auth.headers) {
677
- modelRuntime.registerProvider(selected.provider, { headers: { ...auth.headers } });
677
+ // Provider config accepts resolved values, not request-only null suppressions;
678
+ // the child runtime derives those again from the provider's auth implementation.
679
+ const headers = Object.fromEntries(
680
+ Object.entries(auth.headers).filter(
681
+ (entry): entry is [string, string] => entry[1] !== null,
682
+ ),
683
+ );
684
+ modelRuntime.registerProvider(selected.provider, { headers });
678
685
  }
679
686
  if (auth.apiKey && !options.modelRegistry.isUsingOAuth(model)) {
680
687
  await modelRuntime.setRuntimeApiKey(selected.provider, auth.apiKey);
@@ -869,8 +876,8 @@ export const createWorkerSession = Effect.fn("WorkerSession.create")(function* (
869
876
  const services = yield* acquireWorkerServices(options, dependencies, modelRuntime);
870
877
 
871
878
  // createAgentSessionServices registers extension providers and refreshes, but
872
- // discards that result in Pi 0.80.10. Keep this worker-owned probe so provider
873
- // errors and aborts remain typed acquisition failures; remove it when Pi surfaces them.
879
+ // does not surface the refresh result. Keep this worker-owned probe so provider
880
+ // errors and aborts remain typed acquisition failures.
874
881
  yield* refreshModelRuntime(modelRuntime, definition);
875
882
  const model = yield* Effect.try({
876
883
  try: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachwill/pi-orchestrate",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "description": "Concurrent worker orchestration for Pi",
6
6
  "exports": {},
@@ -29,17 +29,18 @@
29
29
  "effect": "4.0.0-rc.111"
30
30
  },
31
31
  "peerDependencies": {
32
- "@earendil-works/pi-agent-core": "^0.80.10",
33
- "@earendil-works/pi-ai": "^0.80.10",
34
- "@earendil-works/pi-coding-agent": "^0.80.10",
35
- "@earendil-works/pi-tui": "^0.80.10",
32
+ "@earendil-works/pi-agent-core": "^0.85.0",
33
+ "@earendil-works/pi-ai": "^0.85.0",
34
+ "@earendil-works/pi-coding-agent": "^0.85.0",
35
+ "@earendil-works/pi-tui": "^0.85.0",
36
36
  "typebox": "*"
37
37
  },
38
38
  "devDependencies": {
39
- "@earendil-works/pi-agent-core": "0.80.10",
40
- "@earendil-works/pi-ai": "0.80.10",
41
- "@earendil-works/pi-coding-agent": "0.80.10",
42
- "@earendil-works/pi-tui": "0.80.10",
39
+ "@earendil-works/pi-agent-core": "0.85.0",
40
+ "@earendil-works/pi-ai": "0.85.0",
41
+ "@earendil-works/pi-coding-agent": "0.85.0",
42
+ "@earendil-works/pi-server": "0.85.0",
43
+ "@earendil-works/pi-tui": "0.85.0",
43
44
  "@types/bun": "^1.3.14",
44
45
  "@types/node": "^22.19.17",
45
46
  "typebox": "^1.1.37",