paseo-acp-agy 1.2.6 → 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.
- package/dist/acp-server.d.ts +2 -1
- package/dist/acp-server.js +153 -17
- package/dist/antigravity-process.d.ts +10 -1
- package/dist/antigravity-process.js +129 -36
- package/dist/index.js +24 -4
- package/dist/paseo-patcher.js +176 -38
- package/dist/protocol.d.ts +12 -0
- package/dist/protocol.js +31 -6
- package/dist/session-store.d.ts +4 -0
- package/dist/session-store.js +59 -0
- package/dist/session.d.ts +6 -1
- package/dist/session.js +47 -2
- package/dist/transcript.d.ts +25 -0
- package/dist/transcript.js +157 -0
- package/package.json +2 -2
package/dist/acp-server.d.ts
CHANGED
|
@@ -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;
|
package/dist/acp-server.js
CHANGED
|
@@ -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()
|
|
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
|
-
|
|
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:
|
|
190
|
-
sessionCapabilities: {
|
|
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
|
-
|
|
248
|
-
|
|
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,
|
|
@@ -450,25 +579,32 @@ export class ACPServer {
|
|
|
450
579
|
}
|
|
451
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
|
|
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:
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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 =
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
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
|
-
|
|
606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
681
|
+
}
|
|
682
|
+
settleFn();
|
|
611
683
|
};
|
|
612
684
|
try {
|
|
613
685
|
await this.applyPendingRestart(active);
|
|
614
686
|
if (active.cancelled) {
|
|
615
|
-
|
|
687
|
+
cleanupAndSettle(() => active.reject(new Error("Prompt turn cancelled before execution")));
|
|
616
688
|
return turnPromise;
|
|
617
689
|
}
|
|
618
|
-
if
|
|
619
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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();
|