paseo-acp-agy 1.2.5 → 1.2.7

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,6 +1,7 @@
1
1
  import { Readable, Writable } from "node:stream";
2
+ import { EventEmitter } from "node:events";
2
3
  import { SessionManager } from "./session.js";
3
- export declare class ACPServer {
4
+ export declare class ACPServer extends EventEmitter {
4
5
  private sessionManager;
5
6
  private input;
6
7
  private output;
@@ -1,10 +1,12 @@
1
1
  import readline from "node:readline";
2
+ import { EventEmitter } from "node:events";
2
3
  import { logger } from "./logger.js";
3
4
  import { ACP_METHODS, AVAILABLE_MODES, AVAILABLE_PERMISSIONS, fetchAvailableModels, buildConfigOptionsForSession, extractPromptText, mapToolNameToKind, extractImageMarkdownLink, isNarrationText, calculateUsageCostUsd, roundUsageCostUsd, getModelContextWindow, fetchAntigravityUsage, } from "./protocol.js";
4
5
  import { SessionManager } from "./session.js";
5
6
  import { executeSlashCommand, AVAILABLE_SLASH_COMMANDS } from "./slash-commands.js";
6
7
  import { getShortVersion } from "./version.js";
7
8
  import { resolveDefaultAgyBinary } from "./antigravity-process.js";
9
+ import { loadConversationTranscript } from "./transcript.js";
8
10
  function splitModelAndEffort(modelInput) {
9
11
  if (!modelInput)
10
12
  return {};
@@ -16,7 +18,7 @@ function splitModelAndEffort(modelInput) {
16
18
  }
17
19
  return { model: modelInput };
18
20
  }
19
- export class ACPServer {
21
+ export class ACPServer extends EventEmitter {
20
22
  sessionManager;
21
23
  input;
22
24
  output;
@@ -25,6 +27,7 @@ export class ACPServer {
25
27
  rl = null;
26
28
  isRunning = false;
27
29
  constructor(options = {}) {
30
+ super();
28
31
  this.input = options.input || process.stdin;
29
32
  this.output = options.output || process.stdout;
30
33
  this.binaryPath = options.binaryPath || resolveDefaultAgyBinary();
@@ -37,6 +40,14 @@ export class ACPServer {
37
40
  if (this.isRunning)
38
41
  return;
39
42
  this.isRunning = true;
43
+ this.output.on("error", (err) => {
44
+ if (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED") {
45
+ logger.debug("ACP output stream closed", { code: err.code });
46
+ }
47
+ else {
48
+ logger.error("ACP output stream error", { error: err.message, code: err.code });
49
+ }
50
+ });
40
51
  this.rl = readline.createInterface({
41
52
  input: this.input,
42
53
  output: undefined,
@@ -49,14 +60,23 @@ export class ACPServer {
49
60
  });
50
61
  this.rl.on("close", () => {
51
62
  logger.info("ACP input stream closed, shutting down server");
52
- this.stop().catch((err) => {
63
+ this.stop()
64
+ .catch((err) => {
53
65
  logger.error("Error stopping ACP server on close", { error: err.message });
66
+ })
67
+ .finally(() => {
68
+ this.emit("close");
54
69
  });
55
70
  });
56
71
  logger.info("ACP Server started");
57
72
  }
58
73
  send(msg) {
59
- this.output.write(JSON.stringify(msg) + "\n");
74
+ try {
75
+ this.output.write(JSON.stringify(msg) + "\n");
76
+ }
77
+ catch (err) {
78
+ logger.warn("Failed to write to ACP output stream", { error: err.message });
79
+ }
60
80
  }
61
81
  sendNotification(method, params) {
62
82
  const notification = { jsonrpc: "2.0", method, params };
@@ -186,8 +206,12 @@ export class ACPServer {
186
206
  agentInfo: { name: "agy-acp", version: shortVersion },
187
207
  serverInfo: { name: "agy-acp", version: shortVersion },
188
208
  agentCapabilities: {
189
- loadSession: false,
190
- sessionCapabilities: { resume: {} },
209
+ loadSession: true,
210
+ sessionCapabilities: {
211
+ resume: {},
212
+ list: {},
213
+ fork: {},
214
+ },
191
215
  },
192
216
  });
193
217
  }
@@ -234,7 +258,6 @@ export class ACPServer {
234
258
  break;
235
259
  }
236
260
  let session = this.sessionManager.getSession(sessionId);
237
- let created = false;
238
261
  try {
239
262
  if (!session) {
240
263
  session = this.sessionManager.createSession({
@@ -242,10 +265,79 @@ export class ACPServer {
242
265
  cwd,
243
266
  binaryPath: this.binaryPath,
244
267
  });
245
- created = true;
246
268
  }
247
- if (!created && session.process.currentConversationId) {
248
- await session.ensureReadyForResume();
269
+ const conversationId = session.process.currentConversationId;
270
+ if (conversationId) {
271
+ try {
272
+ const replayItems = await loadConversationTranscript(conversationId);
273
+ if (!session.title && replayItems.length > 0) {
274
+ const firstUser = replayItems.find((item) => item.type === "user_message");
275
+ if (firstUser && firstUser.text) {
276
+ session.setTitle(firstUser.text.slice(0, 80).replace(/\r?\n/g, " ").trim());
277
+ }
278
+ }
279
+ for (const item of replayItems) {
280
+ switch (item.type) {
281
+ case "user_message":
282
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
283
+ sessionId: session.id,
284
+ update: {
285
+ sessionUpdate: "user_message_chunk",
286
+ content: { type: "text", text: item.text },
287
+ },
288
+ });
289
+ break;
290
+ case "agent_thought":
291
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
292
+ sessionId: session.id,
293
+ update: {
294
+ sessionUpdate: "agent_thought_chunk",
295
+ content: { type: "text", text: item.text },
296
+ },
297
+ });
298
+ break;
299
+ case "tool_call":
300
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
301
+ sessionId: session.id,
302
+ update: {
303
+ sessionUpdate: "tool_call",
304
+ toolCallId: item.toolCallId,
305
+ title: item.title,
306
+ kind: item.kind,
307
+ rawInput: item.rawInput,
308
+ },
309
+ });
310
+ break;
311
+ case "tool_call_update":
312
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
313
+ sessionId: session.id,
314
+ update: {
315
+ sessionUpdate: "tool_call_update",
316
+ toolCallId: item.toolCallId,
317
+ status: item.status,
318
+ rawOutput: item.rawOutput,
319
+ },
320
+ });
321
+ break;
322
+ case "agent_message":
323
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
324
+ sessionId: session.id,
325
+ update: {
326
+ sessionUpdate: "agent_message_chunk",
327
+ content: { type: "text", text: item.text },
328
+ },
329
+ });
330
+ break;
331
+ }
332
+ }
333
+ }
334
+ catch (replayErr) {
335
+ logger.warn("Failed to replay conversation transcript during session load", {
336
+ sessionId,
337
+ conversationId,
338
+ error: replayErr.message,
339
+ });
340
+ }
249
341
  }
250
342
  if (!isNotification) {
251
343
  this.sendSuccess(id, await this.sessionState(session));
@@ -309,6 +401,40 @@ export class ACPServer {
309
401
  }
310
402
  break;
311
403
  }
404
+ case ACP_METHODS.SESSION_LIST:
405
+ case ACP_METHODS.SESSION_LIST_CAMEL:
406
+ case ACP_METHODS.SESSION_LIST_ALIAS: {
407
+ const cwd = typeof params.cwd === "string" ? params.cwd : undefined;
408
+ const sessions = this.sessionManager.listSessions(cwd);
409
+ if (!isNotification) {
410
+ this.sendSuccess(id, { sessions });
411
+ }
412
+ break;
413
+ }
414
+ case ACP_METHODS.SESSION_FORK:
415
+ case ACP_METHODS.SESSION_FORK_ALIAS: {
416
+ const sourceSessionId = String(params.sessionId || "");
417
+ const cwd = typeof params.cwd === "string" ? params.cwd : undefined;
418
+ const sourceSession = sourceSessionId ? this.sessionManager.getSession(sourceSessionId) : undefined;
419
+ const newSession = this.sessionManager.createSession({
420
+ cwd: cwd || sourceSession?.cwd,
421
+ model: sourceSession?.model,
422
+ effort: sourceSession?.effort,
423
+ mode: sourceSession?.mode,
424
+ permission: sourceSession?.permission,
425
+ conversationId: sourceSession?.process.currentConversationId,
426
+ binaryPath: this.binaryPath,
427
+ });
428
+ if (sourceSession?.title) {
429
+ newSession.setTitle(`${sourceSession.title} (fork)`);
430
+ }
431
+ if (!isNotification) {
432
+ this.sendSuccess(id, await this.sessionState(newSession));
433
+ this.publishCommands(newSession.id);
434
+ this.publishUsageUpdate(newSession);
435
+ }
436
+ break;
437
+ }
312
438
  case ACP_METHODS.SESSION_PROMPT:
313
439
  case ACP_METHODS.SESSION_PROMPT_ALIAS: {
314
440
  const sessionId = String(params.sessionId || "");
@@ -331,6 +457,9 @@ export class ACPServer {
331
457
  try {
332
458
  session.touch();
333
459
  const promptText = extractPromptText(params.prompt);
460
+ if (!session.title && promptText) {
461
+ session.setTitle(promptText.slice(0, 80).replace(/\r?\n/g, " ").trim());
462
+ }
334
463
  logger.info("Processing ACP prompt", {
335
464
  sessionId,
336
465
  promptLength: promptText.length,
@@ -444,31 +573,38 @@ export class ACPServer {
444
573
  if (step.usage) {
445
574
  const inputTokens = step.usage.input_tokens ?? 0;
446
575
  const outputTokens = step.usage.output_tokens ?? 0;
447
- if (inputTokens > 0) {
448
- latestStepContextTokens = inputTokens + outputTokens;
449
- }
450
- const turnCost = calculateUsageCostUsd(executingModel, inputTokens, outputTokens, step.usage.cache_read_tokens || 0);
451
576
  const cachedReadTokens = step.usage.cache_read_tokens ?? 0;
577
+ if (inputTokens > 0 || cachedReadTokens > 0) {
578
+ latestStepContextTokens = inputTokens + cachedReadTokens + outputTokens;
579
+ }
580
+ const turnCost = calculateUsageCostUsd(executingModel, inputTokens, outputTokens, cachedReadTokens);
452
581
  const thoughtTokens = step.usage.thinking_tokens;
453
- const totalTokens = step.usage.total_tokens ?? (inputTokens + outputTokens);
454
582
  const totalCostUsd = roundUsageCostUsd(session.usage.totalCostUsd + turnCost);
455
583
  const contextWindowMaxTokens = getModelContextWindow(executingModel);
456
- const contextWindowUsedTokens = Math.min(inputTokens + outputTokens, contextWindowMaxTokens);
584
+ const stepContextUsed = Math.min(latestStepContextTokens ?? (inputTokens + cachedReadTokens + outputTokens), contextWindowMaxTokens);
585
+ session.usage.contextWindowUsedTokens = stepContextUsed;
586
+ const cumulativeInputTokens = session.usage.inputTokens + inputTokens;
587
+ const cumulativeOutputTokens = session.usage.outputTokens + outputTokens;
588
+ const cumulativeCachedTokens = session.usage.cachedInputTokens + cachedReadTokens;
589
+ const cumulativeTotalTokens = cumulativeInputTokens + cumulativeOutputTokens;
457
590
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
458
591
  sessionId: session.id,
459
592
  update: {
460
593
  sessionUpdate: "usage_update",
461
594
  size: contextWindowMaxTokens,
462
- used: contextWindowUsedTokens,
595
+ used: stepContextUsed,
463
596
  cost: { amount: totalCostUsd, currency: "USD" },
464
- inputTokens,
465
- outputTokens,
466
- cachedReadTokens,
597
+ inputTokens: cumulativeInputTokens,
598
+ outputTokens: cumulativeOutputTokens,
599
+ cachedReadTokens: cumulativeCachedTokens,
600
+ cachedInputTokens: cumulativeCachedTokens,
467
601
  ...(thoughtTokens !== undefined ? { thoughtTokens } : {}),
468
- totalTokens,
602
+ totalTokens: cumulativeTotalTokens,
469
603
  totalCostUsd,
470
604
  contextWindowMaxTokens,
471
- contextWindowUsedTokens,
605
+ contextWindowUsedTokens: stepContextUsed,
606
+ stepInputTokens: inputTokens,
607
+ stepOutputTokens: outputTokens,
472
608
  },
473
609
  });
474
610
  }
@@ -1,3 +1,4 @@
1
+ import { ChildProcess } from "node:child_process";
1
2
  import { EventEmitter } from "node:events";
2
3
  import { AgyInitEvent, AgyStepUpdateEvent, AgyResultEvent } from "./protocol.js";
3
4
  import { PermissionSettings } from "./permissions.js";
@@ -15,6 +16,12 @@ export interface AntigravityProcessOptions {
15
16
  permissions?: PermissionSettings;
16
17
  env?: Record<string, string>;
17
18
  }
19
+ interface ActiveTurn {
20
+ child: ChildProcess | null;
21
+ cancelled: boolean;
22
+ resolve: (result: AgyResultEvent) => void;
23
+ reject: (err: Error) => void;
24
+ }
18
25
  export declare class AntigravityProcess extends EventEmitter {
19
26
  private child;
20
27
  private binaryPath;
@@ -51,8 +58,10 @@ export declare class AntigravityProcess extends EventEmitter {
51
58
  private applyPendingRestart;
52
59
  start(): Promise<void>;
53
60
  private handleEvent;
54
- ensureReady(expectedConversationId?: string, timeoutMs?: number): Promise<void>;
61
+ ensureReady(expectedConversationId?: string, timeoutMs?: number, forTurn?: ActiveTurn, allowAutoHeal?: boolean): Promise<void>;
62
+ private ensureReadyInternal;
55
63
  sendPrompt(text: string, onStepUpdate?: (event: AgyStepUpdateEvent) => void): Promise<AgyResultEvent>;
56
64
  cancelCurrentTurn(): boolean;
57
65
  close(): Promise<void>;
58
66
  }
67
+ export {};
@@ -400,6 +400,7 @@ export class AntigravityProcess extends EventEmitter {
400
400
  let stdoutBuffer = "";
401
401
  let stderrBuffer = "";
402
402
  child.stdout.on("data", (chunk) => {
403
+ this.emit("activity");
403
404
  stdoutBuffer += chunk;
404
405
  const lines = stdoutBuffer.split("\n");
405
406
  stdoutBuffer = lines.pop() || "";
@@ -453,6 +454,7 @@ export class AntigravityProcess extends EventEmitter {
453
454
  }
454
455
  else if (this.child === child) {
455
456
  this.child = null;
457
+ this.initialInfo = null;
456
458
  }
457
459
  this.emit("exit", { code, signal, pid: child.pid });
458
460
  });
@@ -485,27 +487,49 @@ export class AntigravityProcess extends EventEmitter {
485
487
  break;
486
488
  }
487
489
  case "result": {
488
- if (!ownsTurn || !this.currentTurnPromise)
489
- break;
490
490
  const resultEvent = event;
491
491
  if (resultEvent.result?.conversation_id) {
492
492
  this.conversationId = resultEvent.result.conversation_id;
493
493
  }
494
- this.emit("result", resultEvent);
495
- const active = this.currentTurnPromise;
496
- this.currentTurnPromise = null;
497
- active.resolve(resultEvent);
494
+ if (ownsProcess) {
495
+ this.emit("result", resultEvent);
496
+ }
497
+ if (ownsTurn && this.currentTurnPromise) {
498
+ const active = this.currentTurnPromise;
499
+ this.currentTurnPromise = null;
500
+ active.resolve(resultEvent);
501
+ }
498
502
  break;
499
503
  }
500
504
  default:
501
505
  logger.warn("Unknown agy event received", { raw: event });
502
506
  }
503
507
  }
504
- async ensureReady(expectedConversationId, timeoutMs = 10_000) {
505
- if (this.currentTurnPromise) {
506
- throw new Error("Cannot validate an Antigravity session while a prompt turn is active");
508
+ async ensureReady(expectedConversationId, timeoutMs = 15_000, forTurn, allowAutoHeal = true) {
509
+ try {
510
+ await this.ensureReadyInternal(expectedConversationId, timeoutMs, forTurn);
511
+ }
512
+ catch (err) {
513
+ if (allowAutoHeal && this.conversationId) {
514
+ logger.warn("Failed to start/resume agy process with conversation ID; auto-healing by clearing conversation ID and starting fresh", { conversationId: this.conversationId, error: err.message });
515
+ const failedChild = this.child;
516
+ this.conversationId = undefined;
517
+ this.initialInfo = null;
518
+ if (failedChild) {
519
+ await this.terminateChild(failedChild).catch(() => { });
520
+ }
521
+ await this.ensureReadyInternal(undefined, timeoutMs, forTurn);
522
+ this.emit("conversation_healed", { previousConversationId: expectedConversationId });
523
+ return;
524
+ }
525
+ throw err;
507
526
  }
508
- await this.applyPendingRestart();
527
+ }
528
+ async ensureReadyInternal(expectedConversationId, timeoutMs = 15_000, forTurn) {
529
+ if (this.currentTurnPromise && this.currentTurnPromise !== forTurn) {
530
+ throw new Error("Cannot validate an Antigravity session while another prompt turn is active");
531
+ }
532
+ await this.applyPendingRestart(forTurn);
509
533
  if (!this.isRunning)
510
534
  await this.start();
511
535
  const child = this.child;
@@ -524,8 +548,10 @@ export class AntigravityProcess extends EventEmitter {
524
548
  let settled = false;
525
549
  const cleanup = () => {
526
550
  this.off("init", onInit);
551
+ this.off("result", onResult);
527
552
  child.off("error", onError);
528
553
  child.off("close", onClose);
554
+ child.off("exit", onExit);
529
555
  clearTimeout(timer);
530
556
  };
531
557
  const finishReject = (err) => {
@@ -557,8 +583,16 @@ export class AntigravityProcess extends EventEmitter {
557
583
  }
558
584
  finishResolve();
559
585
  };
586
+ const onResult = (event) => {
587
+ if (this.child !== child)
588
+ return;
589
+ if (event.result?.status === "ERROR") {
590
+ finishReject(new Error(event.result.error || "Antigravity failed before initialization"));
591
+ }
592
+ };
560
593
  const onError = (err) => finishReject(err);
561
594
  const onClose = (code, signal) => finishReject(new Error(`Antigravity exited before initialization: code=${code} signal=${signal}`));
595
+ const onExit = (code, signal) => finishReject(new Error(`Antigravity exited before initialization: code=${code} signal=${signal}`));
562
596
  const timer = setTimeout(() => {
563
597
  if (settled)
564
598
  return;
@@ -571,8 +605,10 @@ export class AntigravityProcess extends EventEmitter {
571
605
  });
572
606
  }, timeoutMs);
573
607
  this.on("init", onInit);
608
+ this.on("result", onResult);
574
609
  child.once("error", onError);
575
610
  child.once("close", onClose);
611
+ child.once("exit", onExit);
576
612
  if (this.initialInfo !== null) {
577
613
  onInit({
578
614
  event: "init",
@@ -602,49 +638,88 @@ export class AntigravityProcess extends EventEmitter {
602
638
  // turn while the child is still starting or restarting.
603
639
  this.currentTurnPromise = active;
604
640
  let stepListener = null;
605
- const failBeforeWrite = (err) => {
606
- if (stepListener)
641
+ let activityListener = null;
642
+ let inactivityTimer = null;
643
+ const turnTimeoutMs = Number(process.env.AGY_ACP_TURN_TIMEOUT_MS) > 0
644
+ ? Number(process.env.AGY_ACP_TURN_TIMEOUT_MS)
645
+ : 15 * 60 * 1000; // 15 minutes default turn inactivity timeout
646
+ const resetInactivityTimer = () => {
647
+ if (inactivityTimer)
648
+ clearTimeout(inactivityTimer);
649
+ inactivityTimer = setTimeout(() => {
650
+ logger.error("Turn timed out due to lack of activity from agy", {
651
+ timeoutMs: turnTimeoutMs,
652
+ conversationId: this.conversationId,
653
+ });
654
+ if (active.child) {
655
+ const childToKill = active.child;
656
+ this.signalProcessTree(childToKill, "SIGINT");
657
+ setTimeout(() => {
658
+ if (this.isProcessTreeAlive(childToKill)) {
659
+ this.signalProcessTree(childToKill, "SIGKILL");
660
+ }
661
+ }, 3000);
662
+ }
663
+ cleanupAndSettle(() => active.reject(new Error(`Turn timed out after ${turnTimeoutMs / 1000}s of inactivity`)));
664
+ }, turnTimeoutMs);
665
+ };
666
+ const cleanupAndSettle = (settleFn) => {
667
+ if (inactivityTimer) {
668
+ clearTimeout(inactivityTimer);
669
+ inactivityTimer = null;
670
+ }
671
+ if (stepListener) {
607
672
  this.off("step_update", stepListener);
608
- if (this.currentTurnPromise === active)
673
+ stepListener = null;
674
+ }
675
+ if (activityListener) {
676
+ this.off("activity", activityListener);
677
+ activityListener = null;
678
+ }
679
+ if (this.currentTurnPromise === active) {
609
680
  this.currentTurnPromise = null;
610
- active.reject(err);
681
+ }
682
+ settleFn();
611
683
  };
612
684
  try {
613
685
  await this.applyPendingRestart(active);
614
686
  if (active.cancelled) {
615
- failBeforeWrite(new Error("Prompt turn cancelled before execution"));
687
+ cleanupAndSettle(() => active.reject(new Error("Prompt turn cancelled before execution")));
616
688
  return turnPromise;
617
689
  }
618
- if (!this.isRunning)
619
- await this.start();
690
+ // Synchronize readiness & auto-heal if conversation resume was broken
691
+ await this.ensureReady(this.conversationId, 15_000, active, true);
620
692
  if (active.cancelled) {
621
- failBeforeWrite(new Error("Prompt turn cancelled before execution"));
693
+ cleanupAndSettle(() => active.reject(new Error("Prompt turn cancelled before execution")));
622
694
  return turnPromise;
623
695
  }
624
696
  const child = this.child;
625
697
  if (!child || !child.stdin || !child.stdin.writable) {
626
- failBeforeWrite(new Error("Cannot send prompt: agy process stdin is not writable"));
698
+ cleanupAndSettle(() => active.reject(new Error("Cannot send prompt: agy process stdin is not writable")));
627
699
  return turnPromise;
628
700
  }
629
701
  active.child = child;
702
+ const origResolve = active.resolve;
703
+ const origReject = active.reject;
704
+ active.resolve = (result) => {
705
+ cleanupAndSettle(() => origResolve(result));
706
+ };
707
+ active.reject = (err) => {
708
+ cleanupAndSettle(() => origReject(err));
709
+ };
630
710
  if (onStepUpdate) {
631
- stepListener = onStepUpdate;
632
- this.on("step_update", stepListener);
633
- const originalResolve = active.resolve;
634
- const originalReject = active.reject;
635
- active.resolve = (result) => {
636
- if (stepListener)
637
- this.off("step_update", stepListener);
638
- originalResolve(result);
639
- };
640
- active.reject = (err) => {
641
- if (stepListener)
642
- this.off("step_update", stepListener);
643
- originalReject(err);
711
+ stepListener = (event) => {
712
+ resetInactivityTimer();
713
+ onStepUpdate(event);
644
714
  };
715
+ this.on("step_update", stepListener);
645
716
  }
717
+ activityListener = () => resetInactivityTimer();
718
+ this.on("activity", activityListener);
719
+ // Start initial inactivity timer
720
+ resetInactivityTimer();
646
721
  if (active.cancelled) {
647
- failBeforeWrite(new Error("Prompt turn cancelled before execution"));
722
+ cleanupAndSettle(() => active.reject(new Error("Prompt turn cancelled before execution")));
648
723
  return turnPromise;
649
724
  }
650
725
  const payload = {
@@ -655,7 +730,7 @@ export class AntigravityProcess extends EventEmitter {
655
730
  child.stdin.write(JSON.stringify(payload) + "\n");
656
731
  }
657
732
  catch (err) {
658
- failBeforeWrite(err);
733
+ cleanupAndSettle(() => active.reject(err));
659
734
  }
660
735
  return turnPromise;
661
736
  }
@@ -665,8 +740,26 @@ export class AntigravityProcess extends EventEmitter {
665
740
  return false;
666
741
  active.cancelled = true;
667
742
  logger.info("Cancelling active agy turn", { hasChild: Boolean(active.child) });
668
- if (active.child)
669
- this.signalProcessTree(active.child, "SIGINT");
743
+ if (active.child) {
744
+ const child = active.child;
745
+ this.signalProcessTree(child, "SIGINT");
746
+ setTimeout(() => {
747
+ if (this.isProcessTreeAlive(child)) {
748
+ logger.warn("agy process did not exit after SIGINT cancellation; sending SIGTERM", {
749
+ pid: child.pid,
750
+ });
751
+ this.signalProcessTree(child, "SIGTERM");
752
+ setTimeout(() => {
753
+ if (this.isProcessTreeAlive(child)) {
754
+ logger.warn("agy process did not exit after SIGTERM; sending SIGKILL", {
755
+ pid: child.pid,
756
+ });
757
+ this.signalProcessTree(child, "SIGKILL");
758
+ }
759
+ }, 1500);
760
+ }
761
+ }, 3000);
762
+ }
670
763
  return true;
671
764
  }
672
765
  async close() {
package/dist/index.js CHANGED
@@ -160,7 +160,7 @@ if (args.includes("doctor") || args.includes("--doctor")) {
160
160
  process.stdout.write(" Then launch Paseo again.\n");
161
161
  }
162
162
  else {
163
- process.stdout.write(" Tip: Run 'paseo daemon restart' or close the Paseo app to reload.\n");
163
+ process.stdout.write(" Tip: Run 'paseo restart' or close the Paseo app/daemon to reload with updated patches.\n");
164
164
  }
165
165
  }
166
166
  else {
@@ -238,11 +238,15 @@ if (args.includes("setup") ||
238
238
  process.stdout.write("Paseo is already up-to-date and configured for Antigravity telemetry.\n");
239
239
  }
240
240
  if (isPaseoRunning()) {
241
+ const isWin = process.platform === "win32";
241
242
  process.stdout.write(`\n⚠️ IMPORTANT: Paseo is currently running in the background!\n` +
242
- ` Windows caches running executables in memory, so changes will only take effect after restarting Paseo.\n` +
243
- ` In PowerShell, run:\n` +
244
- ` Stop-Process -Name "Paseo" -Force -ErrorAction SilentlyContinue\n` +
245
- ` Then launch Paseo again.\n\n`);
243
+ (isWin
244
+ ? ` Windows caches running executables in memory, so changes will only take effect after restarting Paseo.\n` +
245
+ ` In PowerShell, run:\n` +
246
+ ` Stop-Process -Name "Paseo" -Force -ErrorAction SilentlyContinue\n` +
247
+ ` Then launch Paseo again.\n\n`
248
+ : ` Changes will only take effect after restarting the Paseo daemon/app.\n` +
249
+ ` Run 'paseo restart' or close the Paseo app to reload.\n\n`));
246
250
  }
247
251
  else {
248
252
  process.stdout.write(`Please start Paseo to apply changes.\n\n`);
@@ -262,6 +266,19 @@ void ensurePaseoIntegration().catch(() => { });
262
266
  const skipNarration = args.includes("--skip-naration") ||
263
267
  args.includes("--skip-narration") ||
264
268
  process.env.AGY_ACP_SKIP_NARRATION === "true";
269
+ process.on("uncaughtException", (err) => {
270
+ if (err.code === "EPIPE") {
271
+ logger.debug("Suppressed EPIPE in uncaughtException");
272
+ return;
273
+ }
274
+ logger.error("Uncaught exception in agy-acp process", { error: err.message, stack: err.stack });
275
+ });
276
+ process.on("unhandledRejection", (reason) => {
277
+ logger.error("Unhandled rejection in agy-acp process", {
278
+ reason: reason instanceof Error ? reason.message : String(reason),
279
+ stack: reason instanceof Error ? reason.stack : undefined,
280
+ });
281
+ });
265
282
  const server = new ACPServer({ skipNarration });
266
283
  const cleanup = async () => {
267
284
  try {
@@ -275,4 +292,7 @@ const cleanup = async () => {
275
292
  };
276
293
  process.on("SIGINT", cleanup);
277
294
  process.on("SIGTERM", cleanup);
295
+ server.on("close", () => {
296
+ process.exit(0);
297
+ });
278
298
  server.start();