@nowcrew/daemon 0.5.30 → 0.5.31

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.
@@ -16,6 +16,8 @@ import { decodeExternalOutputEvent, extractExternalAnswer, ExternalAnswerDecoder
16
16
  import { cleanupMaterializedAttachments as cleanupAttachments, executionAttachmentDirectory, materializeAttachments, } from "./attachments.js";
17
17
  import { routeRuntimeAttachments, runtimeCapability, } from "./runtime-capabilities.js";
18
18
  import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
19
+ import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
20
+ import { dslog } from "./slog.js";
19
21
  function truncateUtf8(value, maxBytes) {
20
22
  if (maxBytes <= 0)
21
23
  return "";
@@ -198,6 +200,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
198
200
  }), dependencies.cancellation);
199
201
  let materialized = null;
200
202
  let knownAttachmentDirectory = null;
203
+ let startupReservation = null;
201
204
  try {
202
205
  if (isDeepSeekCodex && !providerConfig.providerApiKey) {
203
206
  throw new Error("DeepSeek API key is not configured for this Agent");
@@ -288,6 +291,28 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
288
291
  const launchRuntime = dependencies.launchRuntime ?? launchLegacyRuntime;
289
292
  if (dependencies.cancellation?.isRequested())
290
293
  throw new RuntimeCancelledError();
294
+ startupReservation = dependencies.startupGate?.reserve(runtime.name) ?? null;
295
+ if (startupReservation !== null) {
296
+ const startupQueueEnteredAt = Date.now();
297
+ dslog("runtime.start_queued", "runtime 等待启动许可", {
298
+ execution_id: input.executionId,
299
+ runtime: runtime.name,
300
+ queued: startupReservation.isQueued(),
301
+ });
302
+ try {
303
+ await awaitWithCancellation(startupReservation.ready, dependencies.cancellation);
304
+ dslog("runtime.start_granted", "runtime 获得启动许可", {
305
+ execution_id: input.executionId,
306
+ runtime: runtime.name,
307
+ startup_queue_ms: Date.now() - startupQueueEnteredAt,
308
+ });
309
+ }
310
+ catch (error) {
311
+ startupReservation.release();
312
+ throw error;
313
+ }
314
+ }
315
+ const runtimeLaunchAt = Date.now();
291
316
  const child = await launchRuntime({
292
317
  runtime: runtime.name,
293
318
  bin: runtime.name,
@@ -321,11 +346,30 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
321
346
  const externalOutput = new ExternalAnswerDecoder();
322
347
  // 每轮独立:tool_use/result 关联状态不能跨 execution 泄漏。
323
348
  const consoleFormatter = createConsoleFormatter();
349
+ let runtimeReady = false;
350
+ let resolveRuntimeReady;
351
+ const runtimeReadySignal = new Promise((resolve) => { resolveRuntimeReady = resolve; });
352
+ let runtimeReadyNotification = Promise.resolve();
353
+ const markRuntimeReady = () => {
354
+ if (runtimeReady)
355
+ return;
356
+ runtimeReady = true;
357
+ startupReservation?.release();
358
+ dslog("runtime.start_ready", "runtime 已完成初始化", {
359
+ execution_id: input.executionId,
360
+ runtime: runtime.name,
361
+ startup_ms: Date.now() - runtimeLaunchAt,
362
+ });
363
+ runtimeReadyNotification = Promise.resolve(callbacks.onRuntimeReady?.(runtime.name));
364
+ resolveRuntimeReady();
365
+ };
324
366
  const readline = createInterface({ input: child.stdout });
325
367
  readline.on("line", (line) => {
326
368
  const event = parseLine(line);
327
369
  if (!event)
328
370
  return;
371
+ if (isRuntimeReadyEvent(runtime.name, event))
372
+ markRuntimeReady();
329
373
  const meta = extractRunMeta(event);
330
374
  if (meta.sessionId)
331
375
  sessionId = meta.sessionId;
@@ -358,6 +402,39 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
358
402
  });
359
403
  let runtimeExit;
360
404
  try {
405
+ if (startupReservation !== null) {
406
+ let startupTimer;
407
+ let startupOutcome;
408
+ try {
409
+ startupOutcome = await awaitWithCancellation(Promise.race([
410
+ runtimeReadySignal.then(() => ({ kind: "ready" })),
411
+ child.exit.then((exit) => ({ kind: "exit", exit })),
412
+ new Promise((resolve) => {
413
+ startupTimer = setTimeout(() => resolve({ kind: "timeout" }), dependencies.startupTimeoutMs ?? 60_000);
414
+ }),
415
+ ]), dependencies.cancellation);
416
+ }
417
+ finally {
418
+ if (startupTimer !== undefined)
419
+ clearTimeout(startupTimer);
420
+ }
421
+ if (startupOutcome.kind !== "ready") {
422
+ if (startupOutcome.kind === "timeout") {
423
+ dslog("runtime.start_timeout", "runtime 启动超时", {
424
+ level: "ERROR",
425
+ execution_id: input.executionId,
426
+ runtime: runtime.name,
427
+ startup_timeout_ms: dependencies.startupTimeoutMs ?? 60_000,
428
+ });
429
+ await child.cancel?.();
430
+ }
431
+ startupReservation.release();
432
+ throw new Error(startupOutcome.kind === "timeout"
433
+ ? `${runtime.name} startup timed out`
434
+ : `${runtime.name} exited before signaling ready (exit ${startupOutcome.exit.exitCode})`);
435
+ }
436
+ await runtimeReadyNotification;
437
+ }
361
438
  runtimeExit = await awaitWithCancellation(child.exit, dependencies.cancellation);
362
439
  }
363
440
  catch (error) {
@@ -415,6 +492,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
415
492
  };
416
493
  }
417
494
  finally {
495
+ startupReservation?.release();
418
496
  const attachmentDirectories = new Set([
419
497
  ...(knownAttachmentDirectory === null ? [] : [knownAttachmentDirectory]),
420
498
  ...(materialized === null ? [] : [materialized.directory]),
@@ -25,6 +25,7 @@ export const DAEMON_CAPABILITIES = [
25
25
  "execution_external_output_v1",
26
26
  "execution_attachments_v1",
27
27
  "execution_answer_stream_v1",
28
+ "execution_machine_queue_v1",
28
29
  ];
29
30
  export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
30
31
  /** 候选 runtime CLI:展示名 → 可执行文件名。 */
@@ -117,7 +118,21 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
117
118
  capabilities: DAEMON_CAPABILITIES,
118
119
  ...(backend.supported ? {
119
120
  executionProtocol: EXECUTION_PROTOCOL,
120
- executionLimits: Object.freeze({ ...executionLimits }),
121
+ executionLimits: Object.freeze({
122
+ maxPromptBytes: executionLimits.maxPromptBytes,
123
+ maxTimeoutMs: executionLimits.maxTimeoutMs,
124
+ maxEventBytes: executionLimits.maxEventBytes,
125
+ maxParallelPerAgent: executionLimits.maxParallelPerAgent,
126
+ maxQueuedPerAgent: executionLimits.maxQueuedPerAgent,
127
+ }),
128
+ executionScheduler: Object.freeze({
129
+ maxParallelTotal: executionLimits.maxParallelTotal,
130
+ maxQueuedTotal: executionLimits.maxQueuedTotal,
131
+ maxStartingTotal: executionLimits.maxStartingTotal,
132
+ maxStartingPerRuntime: executionLimits.maxStartingPerRuntime,
133
+ startupGapMs: executionLimits.startupGapMs,
134
+ startupTimeoutMs: executionLimits.startupTimeoutMs,
135
+ }),
121
136
  } : {}),
122
137
  agentHandles,
123
138
  };
@@ -0,0 +1,402 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createInterface } from "node:readline";
3
+ import { query, } from "@anthropic-ai/claude-agent-sdk";
4
+ import { WebSocket } from "ws";
5
+ import { z } from "zod";
6
+ import { toConsoleLines } from "../console.js";
7
+ import { ChannelToGatewaySchema, GatewayToChannelSchema, REMOTE_PROTOCOL_VERSION, } from "./protocol.js";
8
+ import { resolveClaudeWrapperSession } from "./wrapper.js";
9
+ class InputStream {
10
+ values = [];
11
+ waiters = [];
12
+ stopped = false;
13
+ push(value) {
14
+ if (this.stopped)
15
+ return;
16
+ const waiter = this.waiters.shift();
17
+ if (waiter)
18
+ waiter({ value, done: false });
19
+ else
20
+ this.values.push(value);
21
+ }
22
+ close() {
23
+ this.stopped = true;
24
+ for (const waiter of this.waiters.splice(0))
25
+ waiter({ value: undefined, done: true });
26
+ }
27
+ [Symbol.asyncIterator]() {
28
+ return {
29
+ next: () => {
30
+ const value = this.values.shift();
31
+ if (value)
32
+ return Promise.resolve({ value, done: false });
33
+ if (this.stopped)
34
+ return Promise.resolve({ value: undefined, done: true });
35
+ return new Promise((resolve) => this.waiters.push(resolve));
36
+ },
37
+ };
38
+ }
39
+ }
40
+ const AskQuestionInputSchema = z.object({
41
+ questions: z.array(z.object({
42
+ question: z.string().min(1),
43
+ header: z.string().default("Question"),
44
+ options: z.array(z.object({
45
+ label: z.string(),
46
+ description: z.string().default(""),
47
+ }).passthrough()).default([]),
48
+ }).passthrough()).min(1).max(4),
49
+ }).passthrough();
50
+ function takeValue(args, index, option) {
51
+ const value = args[index + 1];
52
+ if (!value || value.startsWith("-"))
53
+ throw new Error(`${option} requires a value`);
54
+ return value;
55
+ }
56
+ function splitTools(value) {
57
+ return value.split(/[ ,]+/).map((part) => part.trim()).filter(Boolean);
58
+ }
59
+ export function parseClaudeBridgeArgs(userArgs) {
60
+ const resolved = resolveClaudeWrapperSession(userArgs);
61
+ const options = {};
62
+ const prompts = [];
63
+ let title;
64
+ const args = resolved.args;
65
+ for (let index = 0; index < args.length; index += 1) {
66
+ const arg = args[index];
67
+ const inline = (name) => arg.startsWith(`${name}=`) ? arg.slice(name.length + 1) : null;
68
+ if (["--session-id", "--resume", "-r"].includes(arg)) {
69
+ index += 1;
70
+ continue;
71
+ }
72
+ if (arg.startsWith("--session-id=") || arg.startsWith("--resume="))
73
+ continue;
74
+ if (arg === "--model" || arg === "--effort" || arg === "--permission-mode" || arg === "--name" || arg === "-n"
75
+ || arg === "--add-dir" || arg === "--allowedTools" || arg === "--allowed-tools"
76
+ || arg === "--disallowedTools" || arg === "--disallowed-tools" || arg === "--settings") {
77
+ const value = takeValue(args, index, arg);
78
+ index += 1;
79
+ if (arg === "--model")
80
+ options.model = value;
81
+ else if (arg === "--effort")
82
+ options.effort = z.enum(["low", "medium", "high", "xhigh", "max"]).parse(value);
83
+ else if (arg === "--permission-mode")
84
+ options.permissionMode = z.enum(["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"]).parse(value);
85
+ else if (arg === "--name" || arg === "-n")
86
+ title = value;
87
+ else if (arg === "--add-dir")
88
+ (options.additionalDirectories ??= []).push(value);
89
+ else if (arg === "--allowedTools" || arg === "--allowed-tools")
90
+ options.allowedTools = splitTools(value);
91
+ else if (arg === "--disallowedTools" || arg === "--disallowed-tools")
92
+ options.disallowedTools = splitTools(value);
93
+ else
94
+ options.settings = value;
95
+ continue;
96
+ }
97
+ const model = inline("--model");
98
+ const effort = inline("--effort");
99
+ const permission = inline("--permission-mode");
100
+ const name = inline("--name");
101
+ if (model !== null)
102
+ options.model = model;
103
+ else if (effort !== null)
104
+ options.effort = z.enum(["low", "medium", "high", "xhigh", "max"]).parse(effort);
105
+ else if (permission !== null)
106
+ options.permissionMode = z.enum(["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"]).parse(permission);
107
+ else if (name !== null)
108
+ title = name;
109
+ else if (arg === "--dangerously-skip-permissions") {
110
+ options.permissionMode = "bypassPermissions";
111
+ options.allowDangerouslySkipPermissions = true;
112
+ }
113
+ else if (arg.startsWith("-")) {
114
+ throw new Error(`Claude mobile bridge does not support ${arg}`);
115
+ }
116
+ else
117
+ prompts.push(arg);
118
+ }
119
+ const hasResume = userArgs.some((value) => value === "--resume" || value === "-r" || value.startsWith("--resume="));
120
+ return {
121
+ sessionId: resolved.sessionId,
122
+ resume: hasResume,
123
+ ...(title === undefined ? {} : { title }),
124
+ ...(prompts.length === 0 ? {} : { initialPrompt: prompts.join(" ") }),
125
+ options,
126
+ };
127
+ }
128
+ function userMessage(sessionId, text) {
129
+ return {
130
+ type: "user",
131
+ message: { role: "user", content: text },
132
+ parent_tool_use_id: null,
133
+ session_id: sessionId,
134
+ };
135
+ }
136
+ function preview(input) {
137
+ const command = typeof input.command === "string" ? input.command : null;
138
+ const path = typeof input.file_path === "string" ? input.file_path : null;
139
+ return (command ?? path ?? JSON.stringify(input)).slice(0, 16_000);
140
+ }
141
+ function printMessage(message) {
142
+ let printedText = false;
143
+ for (const line of toConsoleLines(message)) {
144
+ if (line.stream === "result" || !line.text.trim())
145
+ continue;
146
+ if (line.stream === "text")
147
+ printedText = true;
148
+ const prefix = line.stream === "thinking" ? "Thinking: "
149
+ : line.stream === "tool" ? "Tool: "
150
+ : line.stream === "tool_result" ? "Result: "
151
+ : line.stream === "error" ? "Error: " : "";
152
+ process.stdout.write(`\n${prefix}${line.text}\n`);
153
+ }
154
+ return printedText;
155
+ }
156
+ export async function runClaudeBridge(userArgs, context) {
157
+ const parsed = parseClaudeBridgeArgs(userArgs);
158
+ const generation = randomUUID();
159
+ const inputStream = new InputStream();
160
+ const inputs = [];
161
+ const interactions = new Map();
162
+ const terminalInteractions = [];
163
+ let terminalHandler = null;
164
+ let active = null;
165
+ let stopped = false;
166
+ let socket = null;
167
+ let reconnectTimer;
168
+ let assistantPrinted = false;
169
+ const url = new URL("/ws/channel", context.gatewayUrl);
170
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
171
+ const send = (value) => {
172
+ if (socket?.readyState === WebSocket.OPEN)
173
+ socket.send(JSON.stringify(ChannelToGatewaySchema.parse(value)));
174
+ };
175
+ const register = () => send({
176
+ type: "channel.register",
177
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
178
+ sessionId: parsed.sessionId,
179
+ cwd: context.cwd,
180
+ pid: process.pid,
181
+ generation,
182
+ ...(parsed.title === undefined ? {} : { title: parsed.title }),
183
+ });
184
+ const finishInteraction = (requestId, value, local) => {
185
+ const interaction = interactions.get(requestId);
186
+ if (!interaction)
187
+ return;
188
+ interactions.delete(requestId);
189
+ const index = terminalInteractions.indexOf(requestId);
190
+ if (index >= 0)
191
+ terminalInteractions.splice(index, 1);
192
+ terminalHandler = null;
193
+ interaction.resolve(value);
194
+ if (local)
195
+ send({
196
+ type: "bridge.resolved",
197
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
198
+ sessionId: parsed.sessionId,
199
+ requestId,
200
+ });
201
+ };
202
+ const connectGateway = () => new Promise((resolve, reject) => {
203
+ const next = new WebSocket(url, { headers: { authorization: `Bearer ${context.token}` } });
204
+ socket = next;
205
+ next.once("open", () => {
206
+ register();
207
+ if (active)
208
+ send({ type: "bridge.status", protocolVersion: REMOTE_PROTOCOL_VERSION, sessionId: parsed.sessionId, busy: true });
209
+ for (const interaction of interactions.values())
210
+ send(interaction.requestFrame);
211
+ resolve();
212
+ });
213
+ next.on("message", (raw) => {
214
+ let decoded;
215
+ try {
216
+ decoded = JSON.parse(raw.toString());
217
+ }
218
+ catch {
219
+ return;
220
+ }
221
+ const frame = GatewayToChannelSchema.safeParse(decoded);
222
+ if (!frame.success)
223
+ return;
224
+ if (frame.data.type === "channel.prompt")
225
+ enqueue({ text: frame.data.text, requestId: frame.data.requestId });
226
+ else if (frame.data.type === "bridge.approval_response") {
227
+ finishInteraction(frame.data.requestId, { decision: frame.data.decision }, false);
228
+ }
229
+ else if (frame.data.type === "bridge.question_response") {
230
+ finishInteraction(frame.data.requestId, { answers: frame.data.answers }, false);
231
+ }
232
+ });
233
+ next.once("error", reject);
234
+ next.on("close", () => {
235
+ if (socket === next)
236
+ socket = null;
237
+ if (!stopped)
238
+ reconnectTimer = setTimeout(() => { void connectGateway().catch(() => { }); }, 1_000);
239
+ });
240
+ });
241
+ const sendNext = () => {
242
+ if (active || inputs.length === 0 || stopped)
243
+ return;
244
+ active = inputs.shift();
245
+ assistantPrinted = false;
246
+ inputStream.push(userMessage(parsed.sessionId, active.text));
247
+ send({ type: "bridge.status", protocolVersion: REMOTE_PROTOCOL_VERSION, sessionId: parsed.sessionId, busy: true });
248
+ };
249
+ function enqueue(value) {
250
+ inputs.push(value);
251
+ sendNext();
252
+ }
253
+ const waitForInteraction = (interaction, render, handleLine) => {
254
+ const promise = new Promise((resolve) => {
255
+ interactions.set(interaction.id, { ...interaction, resolve });
256
+ });
257
+ terminalInteractions.push(interaction.id);
258
+ render();
259
+ terminalHandler = (line) => {
260
+ const result = handleLine(line);
261
+ if (result)
262
+ finishInteraction(interaction.id, result, true);
263
+ };
264
+ return promise;
265
+ };
266
+ const canUseTool = async (toolName, input, { signal }) => {
267
+ const requestId = randomUUID();
268
+ if (toolName === "AskUserQuestion") {
269
+ const parsedQuestions = AskQuestionInputSchema.safeParse(input);
270
+ if (!parsedQuestions.success)
271
+ return { behavior: "deny", message: "Unsupported question format" };
272
+ const questions = parsedQuestions.data.questions.map((question, index) => ({
273
+ id: `q${index}`,
274
+ header: question.header,
275
+ question: question.question,
276
+ isSecret: false,
277
+ options: question.options,
278
+ }));
279
+ const requestFrame = {
280
+ type: "bridge.question",
281
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
282
+ sessionId: parsed.sessionId,
283
+ requestId,
284
+ questions,
285
+ };
286
+ send(requestFrame);
287
+ const waiting = waitForInteraction({ id: requestId, kind: "question", questions, requestFrame }, () => {
288
+ process.stdout.write("\nInput needed:\n");
289
+ for (const [index, question] of questions.entries()) {
290
+ process.stdout.write(`${index + 1}. ${question.header}: ${question.question}\n`);
291
+ for (const option of question.options)
292
+ process.stdout.write(` - ${option.label}: ${option.description}\n`);
293
+ }
294
+ process.stdout.write("Answer: ");
295
+ }, (line) => {
296
+ const values = line.split(";").map((value) => value.trim());
297
+ return { answers: Object.fromEntries(questions.map((question, index) => [question.id, [values[index] ?? ""]])) };
298
+ });
299
+ signal.addEventListener("abort", () => finishInteraction(requestId, { decision: "deny" }, true), { once: true });
300
+ const result = await waiting;
301
+ if (!result.answers)
302
+ return { behavior: "deny", message: "User input cancelled" };
303
+ const answers = Object.fromEntries(questions.map((question) => [
304
+ question.question,
305
+ result.answers?.[question.id]?.join(", ") ?? "",
306
+ ]));
307
+ return { behavior: "allow", updatedInput: { ...input, answers } };
308
+ }
309
+ const requestFrame = {
310
+ type: "bridge.approval",
311
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
312
+ sessionId: parsed.sessionId,
313
+ requestId,
314
+ tool: toolName,
315
+ preview: preview(input),
316
+ };
317
+ send(requestFrame);
318
+ const waiting = waitForInteraction({ id: requestId, kind: "approval", requestFrame }, () => process.stdout.write(`\nApproval required: ${toolName}\n${preview(input)}\nAllow? `), (line) => /^(y|yes|allow)$/i.test(line.trim()) ? { decision: "allow" }
319
+ : /^(n|no|deny)$/i.test(line.trim()) ? { decision: "deny" } : null);
320
+ signal.addEventListener("abort", () => finishInteraction(requestId, { decision: "deny" }, true), { once: true });
321
+ const result = await waiting;
322
+ return result.decision === "allow"
323
+ ? { behavior: "allow", updatedInput: input }
324
+ : { behavior: "deny", message: "User denied this action" };
325
+ };
326
+ await connectGateway();
327
+ const conversation = query({
328
+ prompt: inputStream,
329
+ options: {
330
+ cwd: context.cwd,
331
+ canUseTool,
332
+ settingSources: ["user", "project", "local"],
333
+ includePartialMessages: false,
334
+ ...(parsed.resume ? { resume: parsed.sessionId } : { sessionId: parsed.sessionId }),
335
+ ...parsed.options,
336
+ ...(process.env.NOWCREW_CLAUDE_BIN ? { pathToClaudeCodeExecutable: process.env.NOWCREW_CLAUDE_BIN } : {}),
337
+ },
338
+ });
339
+ const terminal = createInterface({ input: process.stdin, output: process.stdout });
340
+ terminal.setPrompt("› ");
341
+ terminal.on("line", (line) => {
342
+ if (terminalHandler && terminalInteractions.length > 0)
343
+ terminalHandler(line);
344
+ else if (line.trim())
345
+ enqueue({ text: line.trim(), requestId: null });
346
+ terminal.prompt();
347
+ });
348
+ terminal.on("close", () => { stopped = true; conversation.close(); });
349
+ process.stdout.write(`Claude mobile session ${parsed.sessionId}\n`);
350
+ terminal.prompt();
351
+ if (parsed.initialPrompt)
352
+ enqueue({ text: parsed.initialPrompt, requestId: null });
353
+ const stop = () => {
354
+ if (stopped)
355
+ return;
356
+ stopped = true;
357
+ if (reconnectTimer)
358
+ clearTimeout(reconnectTimer);
359
+ inputStream.close();
360
+ conversation.close();
361
+ terminal.close();
362
+ socket?.close();
363
+ for (const id of [...interactions.keys()])
364
+ finishInteraction(id, { decision: "deny" }, true);
365
+ };
366
+ process.once("SIGINT", stop);
367
+ process.once("SIGTERM", stop);
368
+ try {
369
+ for await (const message of conversation) {
370
+ if (message.type === "assistant")
371
+ assistantPrinted = printMessage(message) || assistantPrinted;
372
+ else
373
+ printMessage(message);
374
+ if (message.type !== "result")
375
+ continue;
376
+ const result = message.subtype === "success" && "result" in message && typeof message.result === "string"
377
+ ? message.result.trim()
378
+ : "";
379
+ if (!assistantPrinted && result)
380
+ process.stdout.write(`\n${result}\n`);
381
+ const completedInput = active;
382
+ if (completedInput?.requestId && result)
383
+ send({
384
+ type: "channel.reply",
385
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
386
+ sessionId: parsed.sessionId,
387
+ requestId: completedInput.requestId,
388
+ text: result,
389
+ });
390
+ active = null;
391
+ send({ type: "bridge.status", protocolVersion: REMOTE_PROTOCOL_VERSION, sessionId: parsed.sessionId, busy: false });
392
+ terminal.prompt();
393
+ sendNext();
394
+ }
395
+ return stopped ? 0 : 1;
396
+ }
397
+ finally {
398
+ process.off("SIGINT", stop);
399
+ process.off("SIGTERM", stop);
400
+ stop();
401
+ }
402
+ }