pi-task-dispatcher 3.1.2 → 3.2.1

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.2",
3
+ "version": "3.2.1",
4
4
  "description": "Run configurable Pi worker models as supervised, observable, resumable jobs.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -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,71 @@ 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.");
73
- // Validate the shared least-privilege codemode guidance through every registered delegation surface.
74
- const schemaGuidance = tools.get("delegate_mechanical").parameters.properties.tools.description;
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;
75
208
  assert.match(schemaGuidance, /codemode.*allowlist.*direct tool.*otherwise omit/i, "Schema guidance no longer describes the codemode delegation rule.");
76
209
  for (const name of ["delegate_mechanical", "delegate_assistant", "delegate_engineering", "delegate_designer"]) {
77
- const delegate = tools.get(name);
78
- assert.equal(delegate.parameters.properties.tools.description, schemaGuidance, `${name} does not expose the shared codemode guidance in its tools schema.`);
79
- assert(delegate.promptGuidelines.includes(schemaGuidance), `${name} does not expose the schema codemode guidance in its manager prompt metadata.`);
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.`);
80
213
  }
81
214
  const theme = { fg(_color, text) { return text; }, bold(text) { return text; } };
82
215
  let rendererInvalidations = 0;
@@ -84,7 +217,7 @@ try {
84
217
  const callComponent = start.renderCall({}, theme, renderContext);
85
218
  assert.doesNotThrow(() => callComponent.render(80), "Partial streamed arguments must never crash the renderer.");
86
219
 
87
- // 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.
88
221
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "normal";
89
222
  let result = await start.execute("start-normal", { task: "fake normal job", tools: ["read"], workload }, undefined, undefined, context);
90
223
  assert.equal(result.isError, false);
@@ -94,34 +227,26 @@ try {
94
227
  assert.equal(result.details.sequence, 1);
95
228
  assert.equal(result.details.turns, 1);
96
229
  assert.equal(result.details.actionLedger.length, 1);
97
- assert.equal(result.usage.input, 13);
98
- assert.equal(result.usage.output, 3);
99
- 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.");
100
236
  const partialRender = start.renderResult(result, { expanded: false, isPartial: true }, theme, renderContext).render(100).join("\n");
101
237
  assert.match(partialRender, /Last: read package\.json/);
102
238
  assert.match(partialRender, /Elapsed:/);
103
239
  assert.equal(statusUpdates.at(-1)?.text, "engineer-1 · 11.0%/1k • active-model • medium");
104
240
  const resultComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext);
105
241
  renderContext.lastComponent = resultComponent;
106
- const reusedResultComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext);
107
- assert.equal(reusedResultComponent, resultComponent, "The result renderer did not reuse Pi's last component.");
108
- const firstRender = reusedResultComponent.render(100).join("\n");
109
- assert.match(firstRender, /Took:/);
110
- assert.doesNotMatch(firstRender, /Elapsed:/);
111
- for (let index = 0; index < 1_000; index++) {
112
- const repeatedComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, renderContext);
113
- assert.equal(repeatedComponent.render(100).join("\n"), firstRender, "Repeated render output changed.");
114
- assert.match(callComponent.render(100).join("\n"), /engineer-1: paused/);
115
- }
116
- assert.equal(rendererInvalidations, 0, "A renderer invalidated its own render pass, risking an infinite loop.");
117
- 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.");
118
246
  const normalJobId = result.details.jobId;
119
247
  const managerArgs = { jobId: normalJobId, action: "continue", actionBatches: 1 };
120
248
  const managerRenderContext = { args: managerArgs, state: {}, invalidate() { rendererInvalidations++; }, cwd: projectRoot };
121
- const managerCall = control.renderCall(managerArgs, theme, managerRenderContext);
122
- assert.match(managerCall.render(100).join("\n"), /manager: engineer-1 → continue/);
123
- assert.doesNotMatch(managerCall.render(100).join("\n"), /continue\//);
124
-
249
+ assert.match(control.renderCall(managerArgs, theme, managerRenderContext).render(100).join("\n"), /manager: engineer-1 → continue/);
125
250
  result = await control.execute("continue-normal", managerArgs, undefined, undefined, context);
126
251
  assert.equal(result.isError, false);
127
252
  assert.equal(result.details.status, "completed");
@@ -129,13 +254,8 @@ try {
129
254
  assert.equal(result.details.workerText, "deterministic worker complete");
130
255
  assert.equal(result.usage.input, 5, "Continuation must return only newly observed usage.");
131
256
  assert.equal(result.usage.totalTokens, 8);
132
- assert.equal(statusUpdates.at(-1)?.text, "engineer-1 · 12.0%/1k • active-model • medium");
133
- assert(!statusUpdates.some((update) => update.text === undefined), "Worker status must remain visible after it first appears.");
134
- control.renderResult(result, { expanded: false, isPartial: false }, theme, managerRenderContext).render(100);
135
- assert.match(managerCall.render(100).join("\n"), /manager: engineer-1 → completed/);
136
- assert.doesNotMatch(managerCall.render(100).join("\n"), /continue\/completed/);
137
257
 
138
- // 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.
139
259
  await writeFile(configPath, JSON.stringify({ designer: { model: "media-provider/media-model", thinking: "high" } }), "utf8");
140
260
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "codemode";
141
261
  result = await designerStart.execute("start-designer", { task: "fake image job", tools: ["read", "codemode"], workload }, undefined, undefined, context);
@@ -148,150 +268,216 @@ try {
148
268
  assert.equal(result.details.actionCounts.mutations, 1);
149
269
  assert.equal(result.details.recentActions.at(-1).toolName, "codemode");
150
270
  assert.deepEqual(result.details.recentActions.at(-1).mediaOutputs, [{ mimeType: "image/png", bytes: 4 }]);
151
- assert.match(result.details.recentActions.at(-1).label, /1 image/);
152
- assert.doesNotMatch(JSON.stringify(result.details), /ZmFrZQ==/, "Snapshots must not retain base64 media payloads.");
153
- assert.equal(result.usage.input, 20, "Each nested and parent execution must contribute its own pre-merge usage exactly once.");
154
- assert.equal(result.usage.output, 6);
155
- assert.equal(result.usage.totalTokens, 26);
156
- assert.equal(
157
- statusUpdates.at(-1)?.text,
158
- "designer-2 · 11.0%/1k • media-model • high | engineer-1 · 12.0%/1k • active-model • medium",
159
- "Starting another role must retain the previous role's footer status.",
160
- );
161
- result = await control.execute(
162
- "continue-designer",
163
- { jobId: result.details.jobId, action: "continue", actionBatches: 1 },
164
- undefined,
165
- undefined,
166
- context,
167
- );
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);
168
274
  assert.equal(result.details.status, "completed");
169
- assert.match(statusUpdates.at(-1)?.text ?? "", /designer-2.*\|.*engineer-1/);
170
275
  await writeFile(configPath, "{}\n", "utf8");
171
276
 
172
- // 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.
173
278
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "presentation";
174
279
  const presentationUpdates = [];
175
- result = await start.execute(
176
- "start-presentation",
177
- { task: "fake presentation job", tools: ["read"], workload },
178
- undefined,
179
- (update) => presentationUpdates.push(update.details?.presentationText ?? ""),
180
- context,
181
- );
280
+ result = await start.execute("start-presentation", { task: "fake presentation job", tools: ["read"], workload }, undefined, (update) => presentationUpdates.push(update.details?.presentationText ?? ""), context);
182
281
  assert(presentationUpdates.some((text) => text.includes("stream delta that must be replaced")), "Live assistant stream text was not presented.");
183
- assert(presentationUpdates.some((text) => text.includes("partial read result that must be replaced")), "Live tool partial text was not presented.");
184
- 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.");
185
283
  assert.equal(result.details.status, "paused");
186
284
  assert.match(result.details.presentationText, /authoritative first assistant message/);
187
- assert.match(result.details.presentationText, /tool start: read presentation\.txt/);
188
- assert.match(result.details.presentationText, /tool completed: read presentation\.txt/);
189
285
  assert.match(result.details.presentationText, /final read result six red/);
190
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/);
191
- assert.doesNotMatch(result.details.presentationText, /\u001b/, "Presentation text must not retain terminal control sequences.");
192
287
  const presentationRenderContext = { args: {}, state: {}, invalidate() { rendererInvalidations++; }, cwd: projectRoot };
193
288
  const collapsedComponent = start.renderResult(result, { expanded: false, isPartial: false }, theme, presentationRenderContext);
194
289
  presentationRenderContext.lastComponent = collapsedComponent;
195
290
  const collapsed = collapsedComponent.render(100).join("\n");
196
291
  assert.match(collapsed, /earlier visual rows omitted/);
197
- assert.match(collapsed, /to expand/);
198
292
  assert.match(collapsed, /final read result six red/);
199
- assert.doesNotMatch(collapsed, /authoritative first assistant message/, "Collapsed output must retain only the latest visual rows.");
200
-
201
- result = await control.execute(
202
- "continue-presentation",
203
- { jobId: result.details.jobId, action: "continue", actionBatches: 1 },
204
- undefined,
205
- undefined,
206
- context,
207
- );
208
- assert.equal(result.isError, false);
293
+ result = await control.execute("continue-presentation", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
209
294
  assert.equal(result.details.status, "completed");
210
295
  assert(result.details.workerTextDisplay.length <= 5_000, "Model-facing worker text was not bounded.");
211
296
  assert(result.details.presentationText.length > 5_000, "Structured presentation text was unexpectedly bounded.");
212
297
  assert.match(result.details.presentationText, /MIDDLE-ONLY-IN-EXPANDED-PRESENTATION/);
213
- assert.doesNotMatch(result.content[0].text, /MIDDLE-ONLY-IN-EXPANDED-PRESENTATION/, "Model-facing state exposed the omitted middle of a long report.");
214
- const expandedComponent = control.renderResult(result, { expanded: true, isPartial: false }, theme, presentationRenderContext);
215
- presentationRenderContext.lastComponent = expandedComponent;
216
- const expanded = expandedComponent.render(100).join("\n");
217
- assert.match(expanded, /MIDDLE-ONLY-IN-EXPANDED-PRESENTATION/);
218
- assert.match(expanded, /latest-presentation-marker/);
219
- const reusedExpandedComponent = control.renderResult(result, { expanded: true, isPartial: false }, theme, presentationRenderContext);
220
- assert.equal(reusedExpandedComponent, expandedComponent, "Expanded renderer state was not reused.");
221
- 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/);
299
+
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;
222
382
 
223
- // A tool-free output-limit stop must preserve the session so the manager can recover the report.
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.
224
410
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "length";
225
411
  result = await start.execute("start-length", { task: "fake truncated job", tools: ["read"], workload }, undefined, undefined, context);
226
- assert.equal(result.isError, false);
227
412
  assert.equal(result.details.status, "paused");
228
- assert.equal(result.details.turns, 1);
229
- assert.equal(result.details.workerText, "truncated worker report");
230
413
  assert.match(result.details.phase, /output limit/i);
231
- assert.match(result.details.recommendation, /continue to recover/i);
232
- result = await control.execute(
233
- "continue-length",
234
- { jobId: result.details.jobId, action: "continue", actionBatches: 1 },
235
- undefined,
236
- undefined,
237
- context,
238
- );
239
- assert.equal(result.isError, false);
414
+ result = await control.execute("continue-length", { jobId: result.details.jobId, action: "continue", actionBatches: 1 }, undefined, undefined, context);
240
415
  assert.equal(result.details.status, "completed");
241
- assert.equal(result.details.workerText, "deterministic worker complete");
242
-
243
- // A handled prompt starts no agent run and must fail promptly rather than waiting for settlement.
244
416
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "handled";
245
- const handledStartedAt = Date.now();
246
417
  result = await start.execute("start-handled", { task: "fake handled job", tools: ["read"], workload }, undefined, undefined, context);
247
418
  assert.equal(result.isError, true);
248
- assert.equal(result.details.status, "hard_stopped");
249
419
  assert.match(result.content[0].text, /intercepted by an input handler/i);
250
- assert(Date.now() - handledStartedAt < 5_000, "Handled prompt should not wait for agent settlement.");
251
-
252
- // A tool-use turn without the private IPC signal must fail closed instead of being mistaken for completion.
253
420
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "missing-boundary";
254
421
  result = await start.execute("start-missing-boundary", { task: "fake missing boundary job", tools: ["read"], workload }, undefined, undefined, context);
255
422
  assert.equal(result.isError, true);
256
- assert.equal(result.details.status, "hard_stopped");
257
423
  assert.match(result.content[0].text, /without an enforced IPC boundary/i);
258
-
259
- // A stats timeout after a valid boundary must remain terminal rather than resurrecting as paused.
260
424
  await writeFile(configPath, JSON.stringify({ policy: { absoluteMaxSeconds: 5, noProgressSeconds: 1 } }), "utf8");
261
425
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "stats-timeout";
262
426
  result = await start.execute("start-stats-timeout", { task: "fake stats timeout job", tools: ["read"], workload }, undefined, undefined, context);
263
427
  assert.equal(result.isError, true);
264
- assert.equal(result.details.status, "hard_stopped");
265
428
  assert.notEqual(result.details.status, "paused");
266
-
267
- // A silent prompt must terminate the worker at the configured active-time ceiling.
268
429
  await writeFile(configPath, JSON.stringify({ policy: { absoluteMaxSeconds: 1, noProgressSeconds: 1 } }), "utf8");
269
430
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "timeout";
270
- const timeoutStartedAt = Date.now();
271
431
  result = await start.execute("start-timeout", { task: "fake timeout job", tools: ["read"], workload }, undefined, undefined, context);
272
432
  assert.equal(result.isError, true);
273
433
  assert.equal(result.details.status, "hard_stopped");
274
- assert.match(result.content[0].text, /(deadline|timed out|closed)/i);
275
- assert(Date.now() - timeoutStartedAt < 5_000, "Silent prompt should be bounded by test policy.");
276
-
277
- // An already-aborted parent operation may initialize the child but must never submit task work.
278
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.
279
452
  await rm(promptMarker, { force: true });
280
453
  process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO = "normal";
281
- const controller = new AbortController();
282
- controller.abort();
283
- 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);
284
457
  assert.equal(result.isError, true);
285
458
  assert.equal(result.details.status, "cancelled");
286
459
  await assert.rejects(access(promptMarker), /ENOENT/, "An already-aborted operation submitted a prompt.");
287
-
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");
288
464
  await handlers.get("session_shutdown")?.({}, context);
289
- 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.");
290
475
  } finally {
291
476
  delete process.env.PI_TASK_DISPATCHER_TEST_MODE;
292
477
  delete process.env.PI_TASK_DISPATCHER_CONFIG_PATH;
293
478
  delete process.env.PI_TASK_DISPATCHER_CLI_OVERRIDE;
294
479
  delete process.env.PI_TASK_DISPATCHER_FAKE_PROMPT_MARKER;
480
+ delete process.env.PI_TASK_DISPATCHER_FAKE_OBSERVATION_MARKER;
295
481
  delete process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO;
296
482
  if (inheritedWorkerRole === undefined) delete process.env.PI_TASK_DISPATCHER_WORKER;
297
483
  else process.env.PI_TASK_DISPATCHER_WORKER = inheritedWorkerRole;