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.
@@ -1,11 +1,15 @@
1
- /** Deterministic JSONL/IPC worker used only by the dispatcher's lifecycle tests. */
1
+ /** Deterministic JSONL/IPC worker used only by the dispatcher's lifecycle and proxy tests. */
2
2
 
3
+ import { appendFileSync } from "node:fs";
3
4
  import { appendFile, readFile } from "node:fs/promises";
4
- import readline from "node:readline";
5
+ import { readStrictJsonl } from "./strict-jsonl.mjs";
5
6
 
6
7
  const scenario = process.env.PI_TASK_DISPATCHER_FAKE_SCENARIO ?? "normal";
7
8
  const markerPath = process.env.PI_TASK_DISPATCHER_FAKE_PROMPT_MARKER;
9
+ const observationPath = process.env.PI_TASK_DISPATCHER_FAKE_OBSERVATION_MARKER;
8
10
  let promptCount = 0;
11
+ let proxyConfiguration;
12
+ const pendingProxyCalls = new Map();
9
13
 
10
14
  function send(event) {
11
15
  process.stdout.write(`${JSON.stringify(event)}\n`);
@@ -22,105 +26,251 @@ function usage(input, output) {
22
26
  };
23
27
  }
24
28
 
25
- readline.createInterface({ input: process.stdin }).on("line", async (line) => {
26
- if (!line.trim()) return;
27
- const command = JSON.parse(line);
28
- if (command.type === "set_auto_compaction") {
29
- send({ type: "response", id: command.id, command: command.type, success: true });
29
+ function textFromResult(result) {
30
+ return (result?.content ?? [])
31
+ .filter((part) => part?.type === "text" && typeof part.text === "string")
32
+ .map((part) => part.text)
33
+ .join("\n");
34
+ }
35
+
36
+ async function observe(value) {
37
+ if (observationPath) await appendFile(observationPath, `${JSON.stringify(value)}\n`, "utf8");
38
+ }
39
+
40
+ /** Capture responses the parent dispatcher sends over its private IPC channel. */
41
+ process.on("message", (message) => {
42
+ if (!message || typeof message !== "object") return;
43
+ if (message.type !== "task_dispatcher_proxy_update" && message.type !== "task_dispatcher_proxy_result") return;
44
+ const pending = pendingProxyCalls.get(message.requestId);
45
+ if (!pending) return;
46
+ if (message.type === "task_dispatcher_proxy_update") {
47
+ pending.updates.push(message.result);
48
+ pending.onUpdate?.(message.result);
30
49
  return;
31
50
  }
32
- if (command.type === "get_session_stats") {
33
- if (scenario === "stats-timeout") return;
51
+ pendingProxyCalls.delete(message.requestId);
52
+ pending.resolve({ result: message.result, updates: pending.updates });
53
+ });
54
+
55
+ /** Load and inspect the generated proxy configuration without executing its extension source. */
56
+ async function loadProxyConfiguration(extensionValues) {
57
+ const proxyPath = extensionValues.find((value) => value?.endsWith("tool-proxies.ts"));
58
+ if (!proxyPath) throw new Error("runtime proxy extension was not loaded");
59
+ const source = await readFile(proxyPath, "utf8");
60
+ const match = source.match(/^const configuration = (.+) as \{ nonce: string; tools: ProxyTool\[\] \};$/m);
61
+ if (!match) throw new Error("runtime proxy extension omitted its serialized configuration");
62
+ const configuration = JSON.parse(match[1]);
63
+ if (!configuration || typeof configuration.nonce !== "string" || !Array.isArray(configuration.tools)) {
64
+ throw new Error("runtime proxy extension configuration is invalid");
65
+ }
66
+ return { configuration, source };
67
+ }
68
+
69
+ /** Send one simulated child proxy request and retain every parent-streamed partial result. */
70
+ function proxyCall(toolName, args, requestId, nonce = proxyConfiguration.nonce, onUpdate) {
71
+ return new Promise((resolve, reject) => {
72
+ if (typeof process.send !== "function") {
73
+ reject(new Error("fake worker has no IPC channel"));
74
+ return;
75
+ }
76
+ pendingProxyCalls.set(requestId, { resolve, updates: [], onUpdate });
77
+ process.send({ type: "task_dispatcher_proxy_request", nonce, requestId, toolName, args });
78
+ });
79
+ }
80
+
81
+ /** Emit the worker-visible lifecycle around one proxied manager tool. */
82
+ async function emitProxyTool(toolCallId, toolName, args, parentToolCallId) {
83
+ send({ type: "tool_execution_start", toolCallId, toolName, args, ...(parentToolCallId ? { parentToolCallId } : {}) });
84
+ const response = await proxyCall(toolName, args, toolCallId, proxyConfiguration.nonce, (partialResult) => {
85
+ send({ type: "tool_execution_update", toolCallId, toolName, args, partialResult, ...(parentToolCallId ? { parentToolCallId } : {}) });
86
+ });
87
+ send({
88
+ type: "tool_execution_end",
89
+ toolCallId,
90
+ toolName,
91
+ args,
92
+ isError: response.result?.isError === true,
93
+ result: response.result,
94
+ ...(parentToolCallId ? { parentToolCallId } : {}),
95
+ });
96
+ return response;
97
+ }
98
+
99
+ function boundaryAndAbort(toolResults = 1) {
100
+ process.send?.({ type: "task_dispatcher_boundary", turnIndex: 0, toolResults });
101
+ send({ type: "message_end", message: { role: "assistant", stopReason: "error", errorMessage: "This operation was aborted", content: [] } });
102
+ send({ type: "agent_settled" });
103
+ }
104
+
105
+ async function validateFirstPrompt() {
106
+ const toolsIndex = process.argv.indexOf("--tools");
107
+ const selectedTools = toolsIndex >= 0 ? process.argv[toolsIndex + 1]?.split(",") ?? [] : [];
108
+ const extensionValues = process.argv.flatMap((argument, index) => argument === "--extension" ? [process.argv[index + 1]] : []);
109
+ const loaded = await loadProxyConfiguration(extensionValues);
110
+ proxyConfiguration = loaded.configuration;
111
+ const proxyNames = proxyConfiguration.tools.map((tool) => tool.name);
112
+ if (scenario === "normal" && extensionValues.some((value) => value?.endsWith("codemode-only.ts"))) {
113
+ throw new Error("codemode extension was injected without explicit selection");
114
+ }
115
+ if (scenario === "codemode") {
116
+ const nativeCodemodePath = extensionValues.find((value) => value?.endsWith("codemode-only.ts"));
117
+ let nativeCodemodeSource = "";
118
+ try {
119
+ if (nativeCodemodePath) nativeCodemodeSource = await readFile(nativeCodemodePath, "utf8");
120
+ } catch {
121
+ // The deterministic check below reports the source failure.
122
+ }
123
+ if (
124
+ !selectedTools.includes("codemode")
125
+ || extensionValues.includes("builtin:codemode")
126
+ || !nativeCodemodePath
127
+ || !nativeCodemodeSource.includes('createCodemodeExtension({ mode: "only" })')
128
+ || !nativeCodemodeSource.includes('from "@earendil-works/pi-coding-agent"')
129
+ || proxyNames.join(",") !== "read"
130
+ ) {
131
+ throw new Error("native codemode-only extension or selected proxy visibility was incorrect");
132
+ }
133
+ }
134
+ if (scenario === "extension" && proxyNames.join(",") !== "extension_tool") {
135
+ throw new Error(`extension tool proxy visibility was not exact: ${proxyNames.join(",")}`);
136
+ }
137
+ if (scenario === "transitive" && proxyNames.join(",") !== "extension_tool,read") {
138
+ throw new Error(`transitive tool proxy visibility was not exact: ${proxyNames.join(",")}`);
139
+ }
140
+ if (scenario === "proxy-rich" && proxyNames.join(",") !== "rich_tool") {
141
+ throw new Error(`rich tool proxy visibility was not exact: ${proxyNames.join(",")}`);
142
+ }
143
+ if (!loaded.source.includes("task_dispatcher_proxy_request") || !loaded.source.includes("Type.Unsafe")) {
144
+ throw new Error("runtime proxy extension omitted its IPC or schema bridge");
145
+ }
146
+ }
147
+
148
+ async function runFirstPrompt() {
149
+ if (scenario === "presentation") {
150
+ send({ type: "message_start", message: { role: "assistant" } });
151
+ send({ type: "message_update", assistantMessageEvent: { type: "text_start", contentIndex: 0 } });
152
+ send({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "stream delta that must be replaced" } });
153
+ await new Promise((resolve) => setTimeout(resolve, 150));
154
+ send({ type: "message_update", assistantMessageEvent: { type: "text_end", contentIndex: 0, content: "stream block that must be replaced" } });
34
155
  send({
35
- type: "response",
36
- id: command.id,
37
- command: command.type,
38
- success: true,
39
- data: { contextUsage: { tokens: 100 + promptCount, contextWindow: 1000, percent: 10 + promptCount } },
156
+ type: "message_end",
157
+ message: { role: "assistant", stopReason: "toolUse", content: [{ type: "text", text: "authoritative first assistant message" }], usage: usage(10, 2) },
40
158
  });
159
+ await emitProxyTool("fake-presentation-read", "read", { path: "presentation.txt" });
160
+ boundaryAndAbort();
41
161
  return;
42
162
  }
43
- if (command.type === "compact") {
44
- send({ type: "response", id: command.id, command: command.type, success: false, error: "Nothing to compact (fake session)" });
163
+ if (scenario === "length") {
164
+ send({
165
+ type: "message_end",
166
+ message: { role: "assistant", stopReason: "length", content: [{ type: "text", text: "truncated worker report" }], usage: usage(7, 4) },
167
+ });
168
+ send({ type: "agent_settled" });
45
169
  return;
46
170
  }
47
- if (command.type !== "prompt") return;
48
-
49
- promptCount++;
50
- if (markerPath) await appendFile(markerPath, `${promptCount}\n`, "utf8");
51
- if (scenario === "timeout") return;
52
- if (scenario === "handled") {
53
- send({ type: "response", id: command.id, command: command.type, success: true, data: { disposition: "handled" } });
171
+ if (scenario === "security") {
172
+ send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
173
+ const cross = await proxyCall("read", { path: "cross-job.txt" }, "security-cross-job", "wrong-job-nonce");
174
+ const unknown = await proxyCall("not_selected", {}, "security-unknown");
175
+ const first = await proxyCall("read", { path: "once.txt" }, "security-replay");
176
+ const replay = await proxyCall("read", { path: "twice.txt" }, "security-replay");
177
+ await observe({
178
+ kind: "security-active",
179
+ cross: textFromResult(cross.result),
180
+ unknown: textFromResult(unknown.result),
181
+ first: textFromResult(first.result),
182
+ replay: textFromResult(replay.result),
183
+ });
184
+ boundaryAndAbort();
185
+ setTimeout(() => {
186
+ void proxyCall("read", { path: "paused.txt" }, "security-paused")
187
+ .then((paused) => observe({ kind: "security-paused", text: textFromResult(paused.result) }))
188
+ .catch((error) => observe({ kind: "security-paused", text: String(error) }));
189
+ }, 40);
190
+ return;
191
+ }
192
+ if (scenario === "concurrent") {
193
+ send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
194
+ await Promise.all([
195
+ emitProxyTool("fake-concurrent-read", "read", { path: "first.txt" }),
196
+ emitProxyTool("fake-concurrent-extension", "extension_tool", { action: "second" }),
197
+ ]);
198
+ boundaryAndAbort(2);
199
+ return;
200
+ }
201
+ if (scenario === "cancel-proxy") {
202
+ send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
203
+ await emitProxyTool("fake-cancel-read", "read", { path: "wait-for-abort.txt" });
204
+ boundaryAndAbort();
205
+ return;
206
+ }
207
+ if (scenario === "identity-race") {
208
+ send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
209
+ await observe({ kind: "identity-race-ready" });
210
+ await new Promise((resolve) => setTimeout(resolve, 100));
211
+ await emitProxyTool("fake-identity-race-read", "read", { path: "identity-race.txt" });
212
+ boundaryAndAbort();
213
+ return;
214
+ }
215
+ if (scenario === "child-exit") {
216
+ process.exit(17);
217
+ return;
218
+ }
219
+ if (scenario === "extension" || scenario === "transitive") {
220
+ send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
221
+ await emitProxyTool("fake-extension-1", "extension_tool", { action: "delegated" });
222
+ boundaryAndAbort();
223
+ return;
224
+ }
225
+ if (scenario === "proxy-rich") {
226
+ send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
227
+ const response = await emitProxyTool("fake-rich-1", "rich_tool", { value: "rich" });
228
+ await observe({
229
+ kind: "rich",
230
+ updates: response.updates.length,
231
+ hasImage: response.result?.content?.some((part) => part?.type === "image") === true,
232
+ structuredContent: response.result?.structuredContent,
233
+ isError: response.result?.isError === true,
234
+ terminate: response.result?.terminate === true,
235
+ hasUsage: Object.prototype.hasOwnProperty.call(response.result ?? {}, "usage"),
236
+ });
237
+ boundaryAndAbort();
54
238
  return;
55
239
  }
56
240
 
57
- if (promptCount === 1) {
58
- const toolsIndex = process.argv.indexOf("--tools");
59
- const selectedTools = toolsIndex >= 0 ? process.argv[toolsIndex + 1]?.split(",") ?? [] : [];
60
- const extensionValues = process.argv.flatMap((argument, index) => argument === "--extension" ? [process.argv[index + 1]] : []);
61
- if (scenario === "normal" && extensionValues.some((value) => value?.endsWith("codemode-only.ts"))) {
62
- send({ type: "response", id: command.id, command: command.type, success: false, error: "codemode extension was injected without explicit selection" });
63
- return;
64
- }
65
- if (scenario === "codemode") {
66
- const nativeCodemodePath = extensionValues.find((value) => value?.endsWith("codemode-only.ts"));
67
- let nativeCodemodeSource = "";
68
- try {
69
- if (nativeCodemodePath) nativeCodemodeSource = await readFile(nativeCodemodePath, "utf8");
70
- } catch {
71
- // The failed source check below reports a deterministic fake-RPC error.
72
- }
73
- if (
74
- !selectedTools.includes("codemode")
75
- || extensionValues.includes("builtin:codemode")
76
- || !nativeCodemodePath
77
- || !nativeCodemodeSource.includes('createCodemodeExtension({ mode: "only" })')
78
- || !nativeCodemodeSource.includes('from "@earendil-works/pi-coding-agent"')
79
- ) {
80
- send({ type: "response", id: command.id, command: command.type, success: false, error: "native codemode-only extension was not isolated and loaded" });
81
- return;
82
- }
83
- }
241
+ send({ type: "message_end", message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) } });
242
+ if (scenario === "codemode") {
243
+ send({ type: "tool_execution_start", toolCallId: "fake-codemode-1", toolName: "codemode", args: "return image" });
244
+ await emitProxyTool("fake-read-nested", "read", { path: "reference.png" }, "fake-codemode-1");
245
+ send({
246
+ type: "tool_execution_end",
247
+ toolCallId: "fake-codemode-1",
248
+ toolName: "codemode",
249
+ isError: false,
250
+ result: {
251
+ content: [
252
+ { type: "text", text: "generated image retained in runtime temporary storage" },
253
+ { type: "image", data: "ZmFrZQ==", mimeType: "image/png" },
254
+ ],
255
+ usage: usage(7, 3),
256
+ },
257
+ });
258
+ } else {
259
+ await emitProxyTool("fake-read-1", "read", { path: "package.json" });
84
260
  }
261
+ if (scenario === "missing-boundary") {
262
+ send({
263
+ type: "message_end",
264
+ message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "unsafe final response after tool" }], usage: usage(2, 2) },
265
+ });
266
+ } else {
267
+ boundaryAndAbort();
268
+ }
269
+ if (scenario === "missing-boundary") send({ type: "agent_settled" });
270
+ }
85
271
 
86
- send({ type: "response", id: command.id, command: command.type, success: true, data: { disposition: "started" } });
272
+ async function runContinuation() {
87
273
  if (scenario === "presentation") {
88
- if (promptCount === 1) {
89
- send({ type: "message_start", message: { role: "assistant" } });
90
- send({ type: "message_update", assistantMessageEvent: { type: "text_start", contentIndex: 0 } });
91
- send({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "stream delta that must be replaced" } });
92
- // Keep the fake stream active long enough to exercise the dispatcher's coalesced live update.
93
- await new Promise((resolve) => setTimeout(resolve, 150));
94
- send({ type: "message_update", assistantMessageEvent: { type: "text_end", contentIndex: 0, content: "stream block that must be replaced" } });
95
- send({
96
- type: "message_end",
97
- message: { role: "assistant", stopReason: "toolUse", content: [{ type: "text", text: "authoritative first assistant message" }], usage: usage(10, 2) },
98
- });
99
- send({ type: "tool_execution_start", toolCallId: "fake-presentation-read", toolName: "read", args: { path: "presentation.txt" } });
100
- send({
101
- type: "tool_execution_update",
102
- toolCallId: "fake-presentation-read",
103
- toolName: "read",
104
- args: { path: "presentation.txt" },
105
- partialResult: { content: [{ type: "text", text: "partial read result that must be replaced" }] },
106
- });
107
- // Keep the partial tool result observable before the authoritative result replaces it.
108
- await new Promise((resolve) => setTimeout(resolve, 150));
109
- send({
110
- type: "tool_execution_end",
111
- toolCallId: "fake-presentation-read",
112
- toolName: "read",
113
- isError: false,
114
- result: {
115
- content: [{ type: "text", text: "final read result one\nfinal read result two\nfinal read result three\nfinal read result four\nfinal read result five\nfinal read result six \u001b[31mred\u001b[0m" }],
116
- usage: usage(3, 1),
117
- },
118
- });
119
- process.send?.({ type: "task_dispatcher_boundary", turnIndex: 0, toolResults: 1 });
120
- send({ type: "message_end", message: { role: "assistant", stopReason: "error", errorMessage: "This operation was aborted", content: [] } });
121
- send({ type: "agent_settled" });
122
- return;
123
- }
124
274
  const longReport = [
125
275
  "presentation opening",
126
276
  "A".repeat(2_600),
@@ -132,76 +282,66 @@ readline.createInterface({ input: process.stdin }).on("line", async (line) => {
132
282
  send({ type: "message_update", assistantMessageEvent: { type: "text_start", contentIndex: 0 } });
133
283
  send({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "partial final assistant text" } });
134
284
  send({ type: "message_update", assistantMessageEvent: { type: "text_end", contentIndex: 0, content: "replaced final assistant text" } });
135
- send({
136
- type: "message_end",
137
- message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: longReport }], usage: usage(5, 3) },
138
- });
285
+ send({ type: "message_end", message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: longReport }], usage: usage(5, 3) } });
139
286
  send({ type: "agent_settled" });
140
287
  return;
141
288
  }
142
- if (scenario === "length" && promptCount === 1) {
143
- send({
144
- type: "message_end",
145
- message: {
146
- role: "assistant",
147
- stopReason: "length",
148
- content: [{ type: "text", text: "truncated worker report" }],
149
- usage: usage(7, 4),
150
- },
151
- });
152
- send({ type: "agent_settled" });
289
+ send({
290
+ type: "message_end",
291
+ message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "deterministic worker complete" }], usage: usage(5, 3) },
292
+ });
293
+ send({ type: "agent_settled" });
294
+ }
295
+
296
+ process.on("SIGTERM", () => {
297
+ if (observationPath) appendFileSync(observationPath, `${JSON.stringify({ kind: "signal", signal: "SIGTERM" })}\n`, "utf8");
298
+ process.exit(0);
299
+ });
300
+
301
+ // Expose the disposable test process identity because Windows terminates it without delivering SIGTERM.
302
+ await observe({ kind: "worker-start", pid: process.pid });
303
+
304
+ // Keep strict LF-framed RPC command handling separate from private bidirectional IPC.
305
+ await readStrictJsonl(process.stdin, async (line) => {
306
+ if (!line.trim()) return;
307
+ const command = JSON.parse(line);
308
+ if (command.type === "set_auto_compaction") {
309
+ send({ type: "response", id: command.id, command: command.type, success: true });
153
310
  return;
154
311
  }
155
- if (promptCount === 1) {
312
+ if (command.type === "get_session_stats") {
313
+ if (scenario === "stats-timeout") return;
156
314
  send({
157
- type: "message_end",
158
- message: { role: "assistant", stopReason: "toolUse", content: [], usage: usage(10, 2) },
315
+ type: "response",
316
+ id: command.id,
317
+ command: command.type,
318
+ success: true,
319
+ data: { contextUsage: { tokens: 100 + promptCount, contextWindow: 1000, percent: 10 + promptCount } },
159
320
  });
160
- if (scenario === "codemode") {
161
- send({ type: "tool_execution_start", toolCallId: "fake-codemode-1", toolName: "codemode", args: "return image" });
162
- send({ type: "tool_execution_start", toolCallId: "fake-read-nested", parentToolCallId: "fake-codemode-1", toolName: "read", args: { path: "reference.png" } });
163
- send({ type: "tool_execution_end", toolCallId: "fake-read-nested", parentToolCallId: "fake-codemode-1", toolName: "read", isError: false, result: { usage: usage(3, 1) } });
164
- send({
165
- type: "tool_execution_end",
166
- toolCallId: "fake-codemode-1",
167
- toolName: "codemode",
168
- isError: false,
169
- result: {
170
- content: [
171
- { type: "text", text: "generated image retained in runtime temporary storage" },
172
- { type: "image", data: "ZmFrZQ==", mimeType: "image/png" },
173
- ],
174
- usage: usage(7, 3),
175
- },
176
- });
177
- } else {
178
- send({ type: "tool_execution_start", toolCallId: "fake-read-1", toolName: "read", args: { path: "package.json" } });
179
- send({ type: "tool_execution_end", toolCallId: "fake-read-1", toolName: "read", isError: false, result: { usage: usage(3, 1) } });
180
- }
181
- if (scenario === "missing-boundary") {
182
- send({
183
- type: "message_end",
184
- message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "unsafe final response after tool" }], usage: usage(2, 2) },
185
- });
186
- } else {
187
- process.send?.({ type: "task_dispatcher_boundary", turnIndex: 0, toolResults: 1 });
188
- send({
189
- type: "message_end",
190
- message: { role: "assistant", stopReason: "error", errorMessage: "This operation was aborted", content: [] },
191
- });
192
- }
193
- send({ type: "agent_settled" });
194
321
  return;
195
322
  }
323
+ if (command.type === "compact") {
324
+ send({ type: "response", id: command.id, command: command.type, success: false, error: "Nothing to compact (fake session)" });
325
+ return;
326
+ }
327
+ if (command.type !== "prompt") return;
196
328
 
197
- send({
198
- type: "message_end",
199
- message: {
200
- role: "assistant",
201
- stopReason: "stop",
202
- content: [{ type: "text", text: "deterministic worker complete" }],
203
- usage: usage(5, 3),
204
- },
205
- });
206
- send({ type: "agent_settled" });
329
+ promptCount++;
330
+ if (markerPath) await appendFile(markerPath, `${promptCount}\n`, "utf8");
331
+ if (scenario === "timeout") return;
332
+ if (scenario === "handled") {
333
+ send({ type: "response", id: command.id, command: command.type, success: true, data: { disposition: "handled" } });
334
+ return;
335
+ }
336
+ if (promptCount === 1) {
337
+ try {
338
+ await validateFirstPrompt();
339
+ } catch (error) {
340
+ send({ type: "response", id: command.id, command: command.type, success: false, error: error instanceof Error ? error.message : String(error) });
341
+ return;
342
+ }
343
+ }
344
+ send({ type: "response", id: command.id, command: command.type, success: true, data: { disposition: "started" } });
345
+ if (promptCount === 1) await runFirstPrompt();
346
+ else await runContinuation();
207
347
  });