pi-task-dispatcher 3.1.0 → 3.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-task-dispatcher",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Run configurable Pi worker models as supervised, observable, resumable jobs.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -26,7 +26,7 @@
26
26
  "scripts": {
27
27
  "test:gate": "node tests/gate-probe.mjs",
28
28
  "test:fake-rpc": "node tests/fake-rpc-check.mjs",
29
- "check": "node tests/static-check.mjs && node tests/fake-rpc-check.mjs"
29
+ "check": "node tests/static-check.mjs && node tests/strict-jsonl-check.mjs && node tests/fake-rpc-check.mjs"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@earendil-works/pi-coding-agent": "*",
@@ -1,4 +1,4 @@
1
- /** Exercise WorkerJob lifecycle transitions against a deterministic fake JSONL/IPC child. */
1
+ /** Exercise worker lifecycle, manager-proxied capabilities, and cleanup against a deterministic fake child. */
2
2
 
3
3
  import assert from "node:assert/strict";
4
4
  import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
@@ -17,14 +17,71 @@ const projectRoot = path.resolve(import.meta.dirname, "..");
17
17
  const temporaryDirectory = await mkdtemp(path.join(tmpdir(), "pi-task-dispatcher-fake-rpc-"));
18
18
  const configPath = path.join(temporaryDirectory, "task-dispatcher.json");
19
19
  const promptMarker = path.join(temporaryDirectory, "prompts.txt");
20
+ const observationPath = path.join(temporaryDirectory, "observations.jsonl");
20
21
  const inheritedWorkerRole = process.env.PI_TASK_DISPATCHER_WORKER;
21
22
 
23
+ function usage(input, output) {
24
+ return {
25
+ input,
26
+ output,
27
+ cacheRead: 0,
28
+ cacheWrite: 0,
29
+ totalTokens: input + output,
30
+ cost: { input: input / 1000, output: output / 1000, cacheRead: 0, cacheWrite: 0, total: (input + output) / 1000 },
31
+ };
32
+ }
33
+
34
+ function tool(name, label, description, annotations, executionMode = "parallel") {
35
+ return {
36
+ name,
37
+ label,
38
+ description,
39
+ parameters: { type: "object", additionalProperties: false, properties: { path: { type: "string" }, action: { type: "string" }, value: { type: "string" } } },
40
+ outputSchema: { type: "object", additionalProperties: true },
41
+ executionMode,
42
+ annotations,
43
+ };
44
+ }
45
+
46
+ function terminal(status) {
47
+ return new Set(["completed", "failed", "cancelled", "checkpointed", "taken_over", "hard_stopped"]).has(status);
48
+ }
49
+
50
+ async function observations() {
51
+ const text = await readFile(observationPath, "utf8").catch(() => "");
52
+ return text.split("\n").filter(Boolean).map((line) => JSON.parse(line));
53
+ }
54
+
55
+ async function clearObservations() {
56
+ await writeFile(observationPath, "", "utf8");
57
+ }
58
+
59
+ async function waitForObservation(predicate, label) {
60
+ const deadline = Date.now() + 3_000;
61
+ while (Date.now() < deadline) {
62
+ const match = (await observations()).find(predicate);
63
+ if (match) return match;
64
+ await new Promise((resolve) => setTimeout(resolve, 20));
65
+ }
66
+ throw new Error(`Timed out waiting for ${label}.`);
67
+ }
68
+
69
+ async function waitFor(predicate, label) {
70
+ const deadline = Date.now() + 3_000;
71
+ while (Date.now() < deadline) {
72
+ if (predicate()) return;
73
+ await new Promise((resolve) => setTimeout(resolve, 20));
74
+ }
75
+ throw new Error(`Timed out waiting for ${label}.`);
76
+ }
77
+
22
78
  // The test loads the parent extension even when invoked from a delegated review worker.
23
79
  delete process.env.PI_TASK_DISPATCHER_WORKER;
24
80
  process.env.PI_TASK_DISPATCHER_TEST_MODE = "1";
25
81
  process.env.PI_TASK_DISPATCHER_CONFIG_PATH = configPath;
26
82
  process.env.PI_TASK_DISPATCHER_CLI_OVERRIDE = path.join(projectRoot, "tests", "fake-rpc-worker.mjs");
27
83
  process.env.PI_TASK_DISPATCHER_FAKE_PROMPT_MARKER = promptMarker;
84
+ process.env.PI_TASK_DISPATCHER_FAKE_OBSERVATION_MARKER = observationPath;
28
85
 
29
86
  try {
30
87
  await writeFile(configPath, "{}\n", "utf8");
@@ -38,22 +95,47 @@ try {
38
95
  });
39
96
  const loaded = await jiti.import(path.join(projectRoot, "extensions", "task-dispatcher.ts"));
40
97
  const register = loaded.default ?? loaded;
41
- const tools = new Map();
98
+ const registeredTools = new Map();
42
99
  const handlers = new Map();
100
+ const managerTools = [
101
+ tool("read", "Read", "Manager-controlled read override", { readOnlyHint: true }, "parallel"),
102
+ tool("bash", "Bash", "Manager-controlled command", { destructiveHint: true }, "sequential"),
103
+ tool("edit", "Edit", "Manager-controlled edit", { destructiveHint: true }, "sequential"),
104
+ tool("write", "Write", "Manager-controlled write", { destructiveHint: true }, "sequential"),
105
+ tool("codemode", "Codemode", "Child-local codemode capability", { readOnlyHint: false }, "sequential"),
106
+ tool("extension_tool", "Extension tool", "An extension-defined callable tool", { destructiveHint: true }, "parallel"),
107
+ tool("rich_tool", "Rich tool", "A proxy serialization exercise", { destructiveHint: true }, "parallel"),
108
+ ];
109
+ const managerMetadata = managerTools.map((entry) => ({
110
+ name: entry.name,
111
+ description: entry.description,
112
+ parameters: entry.parameters,
113
+ namespace: entry.name === "extension_tool" ? { name: "extension", description: "Manager extension namespace", instructions: "Use exact manager arguments." } : undefined,
114
+ annotations: entry.annotations,
115
+ promptGuidelines: [`${entry.name} is controlled by the manager.`],
116
+ sourceInfo: { path: `<fake:${entry.name}>`, source: "fake", scope: "temporary", origin: "top-level", baseDir: undefined },
117
+ }));
118
+ managerMetadata.push({ name: "registered_only", description: "Registered but excluded from this invocation", annotations: { readOnlyHint: true }, promptGuidelines: [], sourceInfo: { path: "<fake:registered-only>", source: "fake", scope: "temporary", origin: "top-level" } });
43
119
  const pi = {
44
- registerTool(tool) { tools.set(tool.name, tool); },
45
- getActiveTools() { return ["read", "bash", "edit", "write", "codemode"]; },
120
+ registerTool(entry) { registeredTools.set(entry.name, entry); },
121
+ getActiveTools() { return managerTools.map((entry) => entry.name); },
122
+ getAllTools() { return managerMetadata; },
46
123
  on(event, handler) { handlers.set(event, handler); },
47
124
  };
48
125
  register(pi);
49
126
 
50
127
  const activeModel = { provider: "fake-provider", id: "active-model", contextWindow: 272_000, maxTokens: 128_000 };
51
128
  const statusUpdates = [];
129
+ const parentExecutions = [];
130
+ const transitiveDecisions = [];
131
+ let activeParentExecutions = 0;
132
+ let maximumConcurrentParentExecutions = 0;
52
133
  const context = {
53
134
  cwd: projectRoot,
54
135
  hasUI: true,
55
136
  model: activeModel,
56
137
  thinkingLevel: "medium",
138
+ get tools() { return managerTools.filter((tool) => tool.name !== "codemode"); },
57
139
  ui: {
58
140
  theme: { fg(_color, text) { return text; } },
59
141
  setWidget() {},
@@ -63,20 +145,79 @@ try {
63
145
  find(provider, id) { return { provider, id, contextWindow: 272_000, maxTokens: 128_000 }; },
64
146
  hasConfiguredAuth() { return true; },
65
147
  },
148
+ async executeTool(name, args, options) {
149
+ const call = { name, args, options, aborted: false, usage: undefined };
150
+ parentExecutions.push(call);
151
+ activeParentExecutions++;
152
+ maximumConcurrentParentExecutions = Math.max(maximumConcurrentParentExecutions, activeParentExecutions);
153
+ try {
154
+ if (process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO === "transitive" && name === "extension_tool") {
155
+ const authorize = handlers.get("tool_call");
156
+ transitiveDecisions.push(
157
+ authorize?.({ type: "tool_call", toolCallId: "start-transitive/1", parentToolCallId: "start-transitive", toolName: "extension_tool", input: args }, context),
158
+ authorize?.({ type: "tool_call", toolCallId: "start-transitive/1/1", parentToolCallId: "start-transitive/1", toolName: "read", input: { path: "allowed.txt" } }, context),
159
+ authorize?.({ type: "tool_call", toolCallId: "start-transitive/1/2", parentToolCallId: "start-transitive/1", toolName: "edit", input: { path: "blocked.txt" } }, context),
160
+ );
161
+ }
162
+ if (args?.path === "wait-for-abort.txt") {
163
+ await new Promise((resolve) => {
164
+ if (options.signal.aborted) return resolve();
165
+ options.signal.addEventListener("abort", resolve, { once: true });
166
+ });
167
+ call.aborted = true;
168
+ return { toolCall: { id: "aborted" }, isError: true, result: { content: [{ type: "text", text: "manager operation cancelled" }] } };
169
+ }
170
+ if (args?.path === "presentation.txt") {
171
+ options.onUpdate?.({ content: [{ type: "text", text: "partial read result that must be replaced" }], details: { internal: "hidden" }, usage: usage(99, 99) });
172
+ await new Promise((resolve) => setTimeout(resolve, 150));
173
+ const result = { content: [{ type: "text", text: "final read result six red" }], details: { internal: "hidden" }, usage: usage(3, 1) };
174
+ call.usage = result.usage;
175
+ return { toolCall: { id: "presentation" }, isError: false, result };
176
+ }
177
+ if (name === "rich_tool") {
178
+ options.onUpdate?.({ content: [{ type: "text", text: "partial rich result" }] });
179
+ const result = {
180
+ content: [{ type: "text", text: "rich manager result" }, { type: "image", data: "ZmFrZQ==", mimeType: "image/png" }],
181
+ structuredContent: { source: "manager", count: 1 },
182
+ terminate: true,
183
+ usage: usage(3, 1),
184
+ };
185
+ call.usage = result.usage;
186
+ return { toolCall: { id: "rich" }, isError: true, result };
187
+ }
188
+ if (process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO === "concurrent") await new Promise((resolve) => setTimeout(resolve, 40));
189
+ const result = {
190
+ content: [{ type: "text", text: name === "extension_tool" ? "manager extension override" : `manager read override: ${args?.path ?? args?.action ?? "unknown"}` }],
191
+ details: { managerHook: true },
192
+ usage: usage(3, 1),
193
+ };
194
+ call.usage = result.usage;
195
+ return { toolCall: { id: `${name}-${parentExecutions.length}` }, isError: false, result };
196
+ } finally {
197
+ activeParentExecutions--;
198
+ }
199
+ },
66
200
  };
67
201
  const workload = { investigationUnits: 1, changeUnits: 0, verificationUnits: 1, expectedLongRunningSeconds: 0 };
68
- const start = tools.get("delegate_engineering");
69
- const designerStart = tools.get("delegate_designer");
70
- const control = tools.get("delegate_control");
202
+ const start = registeredTools.get("delegate_engineering");
203
+ const designerStart = registeredTools.get("delegate_designer");
204
+ const control = registeredTools.get("delegate_control");
71
205
  assert(start && designerStart && control, "Expected supervised tools to register.");
72
- assert(tools.has("delegate_mechanical") && tools.has("delegate_assistant"), "Expected every worker role to register.");
206
+ assert(registeredTools.has("delegate_mechanical") && registeredTools.has("delegate_assistant"), "Expected every worker role to register.");
207
+ const schemaGuidance = registeredTools.get("delegate_mechanical").parameters.properties.tools.description;
208
+ assert.match(schemaGuidance, /codemode.*allowlist.*direct tool.*otherwise omit/i, "Schema guidance no longer describes the codemode delegation rule.");
209
+ for (const name of ["delegate_mechanical", "delegate_assistant", "delegate_engineering", "delegate_designer"]) {
210
+ const delegate = registeredTools.get(name);
211
+ assert.equal(delegate.parameters.properties.tools.description, schemaGuidance, `${name} does not expose shared codemode guidance.`);
212
+ assert(delegate.promptGuidelines.includes(schemaGuidance), `${name} does not expose manager codemode guidance.`);
213
+ }
73
214
  const theme = { fg(_color, text) { return text; }, bold(text) { return text; } };
74
215
  let rendererInvalidations = 0;
75
216
  const renderContext = { args: {}, state: {}, invalidate() { rendererInvalidations++; }, cwd: projectRoot };
76
217
  const callComponent = start.renderCall({}, theme, renderContext);
77
218
  assert.doesNotThrow(() => callComponent.render(80), "Partial streamed arguments must never crash the renderer.");
78
219
 
79
- // A normal fake job must pause, account assistant and tool-result usage once, then resume to completion.
220
+ // A normal job proves built-in names resolve through the manager context, without merging nested usage into child output.
80
221
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "normal";
81
222
  let result = await start.execute("start-normal", { task: "fake normal job", tools: ["read"], workload }, undefined, undefined, context);
82
223
  assert.equal(result.isError, false);
@@ -86,34 +227,26 @@ try {
86
227
  assert.equal(result.details.sequence, 1);
87
228
  assert.equal(result.details.turns, 1);
88
229
  assert.equal(result.details.actionLedger.length, 1);
89
- assert.equal(result.usage.input, 13);
90
- assert.equal(result.usage.output, 3);
91
- assert.equal(result.usage.totalTokens, 16);
230
+ assert.equal(result.details.actionLedger[0].mutating, false, "Read-only annotations must classify read as non-mutating.");
231
+ assert.equal(result.usage.input, 10, "Nested manager usage must not be sent back into child accounting.");
232
+ assert.equal(result.usage.output, 2);
233
+ assert.equal(parentExecutions.at(-1).name, "read");
234
+ assert.equal(parentExecutions.at(-1).usage.totalTokens, 4, "The parent manager hook must observe nested usage exactly once.");
235
+ assert.equal(parentExecutions.at(-1).args.path, "package.json", "Built-in name must route through manager override arguments.");
92
236
  const partialRender = start.renderResult(result, { expanded: false, isPartial: true }, theme, renderContext).render(100).join("\n");
93
237
  assert.match(partialRender, /Last: read package\.json/);
94
238
  assert.match(partialRender, /Elapsed:/);
95
239
  assert.equal(statusUpdates.at(-1)?.text, "engineer-1 · 11.0%/1k • active-model • medium");
96
240
  const resultComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext);
97
241
  renderContext.lastComponent = resultComponent;
98
- const reusedResultComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext);
99
- assert.equal(reusedResultComponent, resultComponent, "The result renderer did not reuse Pi's last component.");
100
- const firstRender = reusedResultComponent.render(100).join("\n");
101
- assert.match(firstRender, /Took:/);
102
- assert.doesNotMatch(firstRender, /Elapsed:/);
103
- for (let index = 0; index < 1_000; index++) {
104
- const repeatedComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext);
105
- assert.equal(repeatedComponent.render(100).join("\n"), firstRender, "Repeated render output changed.");
106
- assert.match(callComponent.render(100).join("\n"), /engineer-1: paused/);
107
- }
108
- assert.equal(rendererInvalidations, 0, "A renderer invalidated its own render pass, risking an infinite loop.");
109
- assert.match(callComponent.render(100).join("\n"), /engineer-1: paused/, "The shared call header did not update to paused.");
242
+ assert.equal(start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext), resultComponent, "The result renderer did not reuse Pi's last component.");
243
+ const firstRender = resultComponent.render(100).join("\n");
244
+ for (let index = 0; index < 100; index++) assert.equal(start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext).render(100).join("\n"), firstRender);
245
+ assert.equal(rendererInvalidations, 0, "A renderer invalidated its own render pass.");
110
246
  const normalJobId = result.details.jobId;
111
247
  const managerArgs = { jobId: normalJobId, action: "continue", actionBatches: 1 };
112
248
  const managerRenderContext = { args: managerArgs, state: {}, invalidate() { rendererInvalidations++; }, cwd: projectRoot };
113
- const managerCall = control.renderCall(managerArgs, theme, managerRenderContext);
114
- assert.match(managerCall.render(100).join("\n"), /manager: engineer-1 → continue/);
115
- assert.doesNotMatch(managerCall.render(100).join("\n"), /continue\//);
116
-
249
+ assert.match(control.renderCall(managerArgs, theme, managerRenderContext).render(100).join("\n"), /manager: engineer-1 → continue/);
117
250
  result = await control.execute("continue-normal", managerArgs, undefined, undefined, context);
118
251
  assert.equal(result.isError, false);
119
252
  assert.equal(result.details.status, "completed");
@@ -121,13 +254,8 @@ try {
121
254
  assert.equal(result.details.workerText, "deterministic worker complete");
122
255
  assert.equal(result.usage.input, 5, "Continuation must return only newly observed usage.");
123
256
  assert.equal(result.usage.totalTokens, 8);
124
- assert.equal(statusUpdates.at(-1)?.text, "engineer-1 · 12.0%/1k • active-model • medium");
125
- assert(!statusUpdates.some((update) => update.text === undefined), "Worker status must remain visible after it first appears.");
126
- control.renderResult(result, { expanded: false, isPartial: false }, theme, managerRenderContext).render(100);
127
- assert.match(managerCall.render(100).join("\n"), /manager: engineer-1 → completed/);
128
- assert.doesNotMatch(managerCall.render(100).join("\n"), /continue\/completed/);
129
257
 
130
- // The designer must load only the selected native codemode-only extension and account nested usage once.
258
+ // Codemode remains local while only the manager-selected non-codemode capability is proxied.
131
259
  await writeFile(configPath, JSON.stringify({ designer: { model: "media-provider/media-model", thinking: "high" } }), "utf8");
132
260
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "codemode";
133
261
  result = await designerStart.execute("start-designer", { task: "fake image job", tools: ["read", "codemode"], workload }, undefined, undefined, context);
@@ -140,150 +268,216 @@ try {
140
268
  assert.equal(result.details.actionCounts.mutations, 1);
141
269
  assert.equal(result.details.recentActions.at(-1).toolName, "codemode");
142
270
  assert.deepEqual(result.details.recentActions.at(-1).mediaOutputs, [{ mimeType: "image/png", bytes: 4 }]);
143
- assert.match(result.details.recentActions.at(-1).label, /1 image/);
144
- assert.doesNotMatch(JSON.stringify(result.details), /ZmFrZQ==/, "Snapshots must not retain base64 media payloads.");
145
- assert.equal(result.usage.input, 20, "Each nested and parent execution must contribute its own pre-merge usage exactly once.");
146
- assert.equal(result.usage.output, 6);
147
- assert.equal(result.usage.totalTokens, 26);
148
- assert.equal(
149
- statusUpdates.at(-1)?.text,
150
- "designer-2 · 11.0%/1k • media-model • high | engineer-1 · 12.0%/1k • active-model • medium",
151
- "Starting another role must retain the previous role's footer status.",
152
- );
153
- result = await control.execute(
154
- "continue-designer",
155
- { jobId: result.details.jobId, action: "continue", actionBatches: 1 },
156
- undefined,
157
- undefined,
158
- context,
159
- );
271
+ assert.equal(result.usage.input, 17, "Child-local codemode usage remains child usage; proxied manager usage does not duplicate.");
272
+ assert.equal(result.usage.output, 5);
273
+ result = await control.execute("continue-designer", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
160
274
  assert.equal(result.details.status, "completed");
161
- assert.match(statusUpdates.at(-1)?.text ?? "", /designer-2.*\|.*engineer-1/);
162
275
  await writeFile(configPath, "{}\n", "utf8");
163
276
 
164
- // Streaming RPC updates must replace their partial text, preserve full UI details, and keep model output bounded.
277
+ // Parent streaming updates replace partial presentation text and preserve the bounded/expanded display contract.
165
278
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "presentation";
166
279
  const presentationUpdates = [];
167
- result = await start.execute(
168
- "start-presentation",
169
- { task: "fake presentation job", tools: ["read"], workload },
170
- undefined,
171
- (update) => presentationUpdates.push(update.details?.presentationText ?? ""),
172
- context,
173
- );
280
+ result = await start.execute("start-presentation", { task: "fake presentation job", tools: ["read"], workload }, undefined, (update) => presentationUpdates.push(update.details?.presentationText ?? ""), context);
174
281
  assert(presentationUpdates.some((text) => text.includes("stream delta that must be replaced")), "Live assistant stream text was not presented.");
175
- assert(presentationUpdates.some((text) => text.includes("partial read result that must be replaced")), "Live tool partial text was not presented.");
176
- assert.equal(result.isError, false);
282
+ assert(presentationUpdates.some((text) => text.includes("partial read result that must be replaced")), "Manager-proxied streaming update was not presented.");
177
283
  assert.equal(result.details.status, "paused");
178
284
  assert.match(result.details.presentationText, /authoritative first assistant message/);
179
- assert.match(result.details.presentationText, /tool start: read presentation\.txt/);
180
- assert.match(result.details.presentationText, /tool completed: read presentation\.txt/);
181
285
  assert.match(result.details.presentationText, /final read result six red/);
182
286
  assert.doesNotMatch(result.details.presentationText, /stream delta that must be replaced|stream block that must be replaced|partial read result that must be replaced/);
183
- assert.doesNotMatch(result.details.presentationText, /\u001b/, "Presentation text must not retain terminal control sequences.");
184
287
  const presentationRenderContext = { args: {}, state: {}, invalidate() { rendererInvalidations++; }, cwd: projectRoot };
185
288
  const collapsedComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, presentationRenderContext);
186
289
  presentationRenderContext.lastComponent = collapsedComponent;
187
290
  const collapsed = collapsedComponent.render(100).join("\n");
188
291
  assert.match(collapsed, /earlier visual rows omitted/);
189
- assert.match(collapsed, /to expand/);
190
292
  assert.match(collapsed, /final read result six red/);
191
- assert.doesNotMatch(collapsed, /authoritative first assistant message/, "Collapsed output must retain only the latest visual rows.");
192
-
193
- result = await control.execute(
194
- "continue-presentation",
195
- { jobId: result.details.jobId, action: "continue", actionBatches: 1 },
196
- undefined,
197
- undefined,
198
- context,
199
- );
200
- assert.equal(result.isError, false);
293
+ result = await control.execute("continue-presentation", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
201
294
  assert.equal(result.details.status, "completed");
202
295
  assert(result.details.workerTextDisplay.length <= 5_000, "Model-facing worker text was not bounded.");
203
296
  assert(result.details.presentationText.length > 5_000, "Structured presentation text was unexpectedly bounded.");
204
297
  assert.match(result.details.presentationText, /MIDDLE-ONLY-IN-EXPANDED-PRESENTATION/);
205
- assert.doesNotMatch(result.content[0].text, /MIDDLE-ONLY-IN-EXPANDED-PRESENTATION/, "Model-facing state exposed the omitted middle of a long report.");
206
- const expandedComponent = control.renderResult(result, { expanded: true, isPartial: false }, theme, presentationRenderContext);
207
- presentationRenderContext.lastComponent = expandedComponent;
208
- const expanded = expandedComponent.render(100).join("\n");
209
- assert.match(expanded, /MIDDLE-ONLY-IN-EXPANDED-PRESENTATION/);
210
- assert.match(expanded, /latest-presentation-marker/);
211
- const reusedExpandedComponent = control.renderResult(result, { expanded: true, isPartial: false }, theme, presentationRenderContext);
212
- assert.equal(reusedExpandedComponent, expandedComponent, "Expanded renderer state was not reused.");
213
- assert.equal(reusedExpandedComponent.render(100).join("\n"), expanded, "Repeated expanded rendering changed output.");
298
+ assert.doesNotMatch(result.content[0].text, /MIDDLE-ONLY-IN-EXPANDED-PRESENTATION/);
214
299
 
215
- // A tool-free output-limit stop must preserve the session so the manager can recover the report.
300
+ // Extension-defined tools are accepted when callable in ctx.tools and expose only the exact selected proxy surface.
301
+ process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "extension";
302
+ result = await start.execute("start-extension", { task: "use extension capability", tools: ["extension_tool"], workload }, undefined, undefined, context);
303
+ assert.equal(result.isError, false);
304
+ assert.equal(result.details.status, "paused");
305
+ assert.equal(parentExecutions.at(-1).name, "extension_tool");
306
+ assert.match(result.details.presentationText, /manager extension override/);
307
+ await control.execute("continue-extension", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
308
+
309
+ // Transitive manager calls remain inside the exact selected grant, including through extension tools.
310
+ process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "transitive";
311
+ result = await start.execute("start-transitive", { task: "enforce transitive capabilities", tools: ["extension_tool", "read"], workload }, undefined, undefined, context);
312
+ assert.equal(result.details.status, "paused");
313
+ transitiveDecisions.push(handlers.get("tool_call")?.({ type: "tool_call", toolCallId: "start-transitive/1/3", parentToolCallId: "start-transitive/1", toolName: "read", input: { path: "late.txt" } }, context));
314
+ assert.equal(transitiveDecisions.length, 4);
315
+ assert.equal(transitiveDecisions[0], undefined, "The selected first-level proxy call was blocked.");
316
+ assert.equal(transitiveDecisions[1], undefined, "A selected transitive manager call was blocked.");
317
+ assert.equal(transitiveDecisions[2]?.block, true, "An unselected transitive manager call was not blocked.");
318
+ assert.match(transitiveDecisions[2]?.reason ?? "", /outside this delegation's selected capability grant/i);
319
+ assert.equal(transitiveDecisions[3]?.block, true, "A delayed transitive call survived executor-lease revocation.");
320
+ assert.match(transitiveDecisions[3]?.reason ?? "", /executor lease is no longer active/i);
321
+ await control.execute("continue-transitive", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
322
+
323
+ // Proxy forwarding preserves streamed content, image and structured output, isError, terminate, and strips nested usage/details.
324
+ await clearObservations();
325
+ process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "proxy-rich";
326
+ result = await start.execute("start-rich", { task: "exercise proxy result transport", tools: ["rich_tool"], workload }, undefined, undefined, context);
327
+ assert.equal(result.details.status, "paused");
328
+ assert.equal(result.details.recentActions.at(-1).status, "failed");
329
+ assert.deepEqual(result.details.recentActions.at(-1).mediaOutputs, [{ mimeType: "image/png", bytes: 4 }]);
330
+ const rich = await waitForObservation((entry) => entry.kind === "rich", "rich proxy observation");
331
+ assert.equal(rich.updates, 1);
332
+ assert.equal(rich.hasImage, true);
333
+ assert.deepEqual(rich.structuredContent, { source: "manager", count: 1 });
334
+ assert.equal(rich.isError, true);
335
+ assert.equal(rich.terminate, true);
336
+ assert.equal(rich.hasUsage, false, "Nested usage must be stripped before delivery to the child.");
337
+ await control.execute("continue-rich", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
338
+
339
+ // Multiple child proxy requests can execute concurrently through the manager hook.
340
+ maximumConcurrentParentExecutions = 0;
341
+ process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "concurrent";
342
+ result = await start.execute("start-concurrent", { task: "run two manager tools", tools: ["read", "extension_tool"], workload }, undefined, undefined, context);
343
+ assert.equal(result.details.status, "paused");
344
+ assert.equal(maximumConcurrentParentExecutions, 2, "Independent proxy requests were serialized instead of using manager execution concurrency.");
345
+ await control.execute("continue-concurrent", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
346
+
347
+ // Reject spoofed job nonces, unknown names, replayed ids, and post-boundary requests without invoking unauthorized manager tools.
348
+ await clearObservations();
349
+ const securityCallsBefore = parentExecutions.length;
350
+ process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "security";
351
+ result = await start.execute("start-security", { task: "probe private proxy validation", tools: ["read"], workload }, undefined, undefined, context);
352
+ assert.equal(result.details.status, "paused");
353
+ const activeSecurity = await waitForObservation((entry) => entry.kind === "security-active", "active security rejections");
354
+ assert.match(activeSecurity.cross, /nonce is invalid/i);
355
+ assert.match(activeSecurity.unknown, /not in this job's selected capability grant/i);
356
+ assert.match(activeSecurity.first, /manager read override: once\.txt/);
357
+ assert.match(activeSecurity.replay, /request id was replayed/i);
358
+ const pausedSecurity = await waitForObservation((entry) => entry.kind === "security-paused", "paused security rejection");
359
+ assert.match(pausedSecurity.text, /no bounded manager executor lease is active/i);
360
+ assert.equal(parentExecutions.length - securityCallsBefore, 1, "Only the first valid security probe may reach ctx.executeTool().");
361
+ await control.execute("continue-security", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
362
+
363
+ // Continuations fail closed if current manager callable definitions no longer match the original grant.
364
+ process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "normal";
365
+ result = await start.execute("start-revalidate", { task: "revalidate manager definition", tools: ["read"], workload }, undefined, undefined, context);
366
+ const originalReadParameters = managerTools.find((entry) => entry.name === "read").parameters;
367
+ managerTools.find((entry) => entry.name === "read").parameters = { ...originalReadParameters, properties: { ...originalReadParameters.properties, changed: { type: "boolean" } } };
368
+ const revalidated = await control.execute("continue-revalidate", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
369
+ assert.equal(revalidated.isError, true);
370
+ assert.equal(revalidated.details.status, "hard_stopped");
371
+ assert.match(revalidated.content[0].text, /manifest revalidation failed/i);
372
+ managerTools.find((entry) => entry.name === "read").parameters = originalReadParameters;
373
+ result = await start.execute("start-revalidate-identity", { task: "revalidate manager identity", tools: ["read"], workload }, undefined, undefined, context);
374
+ const readIndex = managerTools.findIndex((entry) => entry.name === "read");
375
+ const originalReadTool = managerTools[readIndex];
376
+ managerTools[readIndex] = { ...originalReadTool };
377
+ const identityRevalidated = await control.execute("continue-revalidate-identity", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
378
+ assert.equal(identityRevalidated.isError, true);
379
+ assert.equal(identityRevalidated.details.status, "hard_stopped");
380
+ assert.match(identityRevalidated.content[0].text, /original callable identity/i);
381
+ managerTools[readIndex] = originalReadTool;
382
+
383
+ // Revalidate again at execution time so a replacement during an active lease cannot win a race.
384
+ await clearObservations();
385
+ process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "identity-race";
386
+ const identityRace = start.execute("start-identity-race", { task: "reject a mid-lease tool replacement", tools: ["read"], workload }, undefined, undefined, context);
387
+ await waitForObservation((entry) => entry.kind === "identity-race-ready", "identity-race worker readiness");
388
+ managerTools[readIndex] = { ...originalReadTool };
389
+ result = await identityRace;
390
+ assert.equal(result.isError, true);
391
+ assert.equal(result.details.status, "hard_stopped");
392
+ assert.match(result.content[0].text, /grant changed before read execution|original callable identity/i);
393
+ managerTools[readIndex] = originalReadTool;
394
+
395
+ // Fail closed on non-JSON-safe callable schemas, recursive names, and registered-but-noncallable requests.
396
+ managerTools.find((entry) => entry.name === "read").parameters = { type: "object", invalid: () => undefined };
397
+ process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "normal";
398
+ result = await start.execute("start-invalid-schema", { task: "reject unsafe schema", tools: ["read"], workload }, undefined, undefined, context);
399
+ assert.equal(result.isError, true);
400
+ assert.match(result.content[0].text, /not JSON-serializable/i);
401
+ managerTools.find((entry) => entry.name === "read").parameters = originalReadParameters;
402
+ result = await start.execute("start-recursive", { task: "reject recursive delegation", tools: ["delegate_engineering"], workload }, undefined, undefined, context);
403
+ assert.equal(result.isError, true);
404
+ assert.match(result.content[0].text, /cannot be delegated recursively/i);
405
+ result = await start.execute("start-noncallable", { task: "reject unavailable tool", tools: ["registered_only"], workload }, undefined, undefined, context);
406
+ assert.equal(result.isError, true);
407
+ assert.match(result.content[0].text, /registered but not callable/i);
408
+
409
+ // Existing lifecycle fail-closed paths remain enforced with the manager proxy present.
216
410
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "length";
217
411
  result = await start.execute("start-length", { task: "fake truncated job", tools: ["read"], workload }, undefined, undefined, context);
218
- assert.equal(result.isError, false);
219
412
  assert.equal(result.details.status, "paused");
220
- assert.equal(result.details.turns, 1);
221
- assert.equal(result.details.workerText, "truncated worker report");
222
413
  assert.match(result.details.phase, /output limit/i);
223
- assert.match(result.details.recommendation, /continue to recover/i);
224
- result = await control.execute(
225
- "continue-length",
226
- { jobId: result.details.jobId, action: "continue", actionBatches: 1 },
227
- undefined,
228
- undefined,
229
- context,
230
- );
231
- assert.equal(result.isError, false);
414
+ result = await control.execute("continue-length", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
232
415
  assert.equal(result.details.status, "completed");
233
- assert.equal(result.details.workerText, "deterministic worker complete");
234
-
235
- // A handled prompt starts no agent run and must fail promptly rather than waiting for settlement.
236
416
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "handled";
237
- const handledStartedAt = Date.now();
238
417
  result = await start.execute("start-handled", { task: "fake handled job", tools: ["read"], workload }, undefined, undefined, context);
239
418
  assert.equal(result.isError, true);
240
- assert.equal(result.details.status, "hard_stopped");
241
419
  assert.match(result.content[0].text, /intercepted by an input handler/i);
242
- assert(Date.now() - handledStartedAt < 5_000, "Handled prompt should not wait for agent settlement.");
243
-
244
- // A tool-use turn without the private IPC signal must fail closed instead of being mistaken for completion.
245
420
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "missing-boundary";
246
421
  result = await start.execute("start-missing-boundary", { task: "fake missing boundary job", tools: ["read"], workload }, undefined, undefined, context);
247
422
  assert.equal(result.isError, true);
248
- assert.equal(result.details.status, "hard_stopped");
249
423
  assert.match(result.content[0].text, /without an enforced IPC boundary/i);
250
-
251
- // A stats timeout after a valid boundary must remain terminal rather than resurrecting as paused.
252
424
  await writeFile(configPath, JSON.stringify({ policy: { absoluteMaxSeconds: 5, noProgressSeconds: 1 } }), "utf8");
253
425
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "stats-timeout";
254
426
  result = await start.execute("start-stats-timeout", { task: "fake stats timeout job", tools: ["read"], workload }, undefined, undefined, context);
255
427
  assert.equal(result.isError, true);
256
- assert.equal(result.details.status, "hard_stopped");
257
428
  assert.notEqual(result.details.status, "paused");
258
-
259
- // A silent prompt must terminate the worker at the configured active-time ceiling.
260
429
  await writeFile(configPath, JSON.stringify({ policy: { absoluteMaxSeconds: 1, noProgressSeconds: 1 } }), "utf8");
261
430
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "timeout";
262
- const timeoutStartedAt = Date.now();
263
431
  result = await start.execute("start-timeout", { task: "fake timeout job", tools: ["read"], workload }, undefined, undefined, context);
264
432
  assert.equal(result.isError, true);
265
433
  assert.equal(result.details.status, "hard_stopped");
266
- assert.match(result.content[0].text, /(deadline|timed out|closed)/i);
267
- assert(Date.now() - timeoutStartedAt < 5_000, "Silent prompt should be bounded by test policy.");
268
-
269
- // An already-aborted parent operation may initialize the child but must never submit task work.
270
434
  await writeFile(configPath, "{}\n", "utf8");
435
+
436
+ // Parent cancellation aborts the nested manager execution and child exit cannot leave a live executor lease.
437
+ process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "cancel-proxy";
438
+ const cancellation = new AbortController();
439
+ const cancellationResult = start.execute("start-cancel-proxy", { task: "cancel nested manager work", tools: ["read"], workload }, cancellation.signal, undefined, context);
440
+ await waitFor(() => parentExecutions.some((call) => call.args?.path === "wait-for-abort.txt"), "nested manager execution start");
441
+ cancellation.abort();
442
+ result = await cancellationResult;
443
+ assert.equal(result.isError, true);
444
+ assert.equal(result.details.status, "cancelled");
445
+ await waitFor(() => parentExecutions.some((call) => call.args?.path === "wait-for-abort.txt" && call.aborted), "nested manager execution abort");
446
+ process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "child-exit";
447
+ result = await start.execute("start-child-exit", { task: "child exits", tools: ["read"], workload }, undefined, undefined, context);
448
+ assert.equal(result.isError, true);
449
+ assert(terminal(result.details.status), "Child exit left a nonterminal worker state.");
450
+
451
+ // An already-aborted operation never submits task work, and session shutdown terminates an idle child.
271
452
  await rm(promptMarker, { force: true });
272
453
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "normal";
273
- const controller = new AbortController();
274
- controller.abort();
275
- result = await start.execute("start-aborted", { task: "must not be submitted", tools: ["read"], workload }, controller.signal, undefined, context);
454
+ const alreadyAborted = new AbortController();
455
+ alreadyAborted.abort();
456
+ result = await start.execute("start-aborted", { task: "must not be submitted", tools: ["read"], workload }, alreadyAborted.signal, undefined, context);
276
457
  assert.equal(result.isError, true);
277
458
  assert.equal(result.details.status, "cancelled");
278
459
  await assert.rejects(access(promptMarker), /ENOENT/, "An already-aborted operation submitted a prompt.");
279
-
460
+ await clearObservations();
461
+ result = await start.execute("start-shutdown", { task: "session cleanup", tools: ["read"], workload }, undefined, undefined, context);
462
+ assert.equal(result.details.status, "paused");
463
+ const workerStart = await waitForObservation((entry) => entry.kind === "worker-start" && Number.isInteger(entry.pid), "session shutdown child identity");
280
464
  await handlers.get("session_shutdown")?.({}, context);
281
- console.log("Deterministic fake-RPC lifecycle checks passed.");
465
+ await waitFor(() => {
466
+ try {
467
+ process.kill(workerStart.pid, 0);
468
+ return false;
469
+ } catch {
470
+ return true;
471
+ }
472
+ }, "session shutdown child termination");
473
+
474
+ console.log("Deterministic fake-RPC lifecycle and manager-proxy checks passed.");
282
475
  } finally {
283
476
  delete process.env.PI_TASK_DISPATCHER_TEST_MODE;
284
477
  delete process.env.PI_TASK_DISPATCHER_CONFIG_PATH;
285
478
  delete process.env.PI_TASK_DISPATCHER_CLI_OVERRIDE;
286
479
  delete process.env.PI_TASK_DISPATCHER_FAKE_PROMPT_MARKER;
480
+ delete process.env.PI_TASK_DISPATCHER_FAKE_OBSERVATION_MARKER;
287
481
  delete process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO;
288
482
  if (inheritedWorkerRole === undefined) delete process.env.PI_TASK_DISPATCHER_WORKER;
289
483
  else process.env.PI_TASK_DISPATCHER_WORKER = inheritedWorkerRole;