@simonepri/refined-antigravity-acp 0.1.0 → 0.1.2
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/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +56 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/index.server.d.ts +4 -0
- package/dist/index.server.d.ts.map +1 -0
- package/dist/index.server.js +7 -0
- package/dist/index.server.js.map +1 -0
- package/dist/src/agy-patch.d.ts +65 -0
- package/dist/src/agy-patch.d.ts.map +1 -0
- package/dist/src/agy-patch.js +351 -0
- package/dist/src/agy-patch.js.map +1 -0
- package/dist/src/auth.d.ts +27 -0
- package/dist/src/auth.d.ts.map +1 -0
- package/dist/src/auth.js +113 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/command.d.ts +5 -0
- package/dist/src/command.d.ts.map +1 -0
- package/dist/src/command.js +74 -0
- package/dist/src/command.js.map +1 -0
- package/dist/src/connector.d.ts +188 -0
- package/dist/src/connector.d.ts.map +1 -0
- package/dist/src/connector.js +1138 -0
- package/dist/src/connector.js.map +1 -0
- package/dist/src/history.d.ts +12 -0
- package/dist/src/history.d.ts.map +1 -0
- package/dist/src/history.js +152 -0
- package/dist/src/history.js.map +1 -0
- package/dist/src/mcp-proxy.d.ts +83 -0
- package/dist/src/mcp-proxy.d.ts.map +1 -0
- package/dist/src/mcp-proxy.js +308 -0
- package/dist/src/mcp-proxy.js.map +1 -0
- package/dist/src/provider.d.ts +11 -0
- package/dist/src/provider.d.ts.map +1 -0
- package/dist/src/provider.js +31 -0
- package/dist/src/provider.js.map +1 -0
- package/dist/src/sanitize.d.ts +29 -0
- package/dist/src/sanitize.d.ts.map +1 -0
- package/dist/src/sanitize.js +237 -0
- package/dist/src/sanitize.js.map +1 -0
- package/dist/src/skills.d.ts +17 -0
- package/dist/src/skills.d.ts.map +1 -0
- package/dist/src/skills.js +140 -0
- package/dist/src/skills.js.map +1 -0
- package/dist/src/steering.d.ts +30 -0
- package/dist/src/steering.d.ts.map +1 -0
- package/dist/src/steering.js +174 -0
- package/dist/src/steering.js.map +1 -0
- package/package.json +20 -13
- package/paseo-plugin.json +0 -1
- package/readme.md +3 -5
- package/.fallowrc.json +0 -11
- package/.github/workflows/ci.yml +0 -24
- package/.github/workflows/release.yml +0 -41
- package/.oxlintignore +0 -2
- package/.oxlintrc.json +0 -152
- package/.release-please-manifest.json +0 -3
- package/bin/cli.ts +0 -58
- package/index.server.ts +0 -9
- package/pnpm-workspace.yaml +0 -44
- package/release-please-config.json +0 -13
- package/server/agy-patch.test.ts +0 -445
- package/server/agy-patch.ts +0 -445
- package/server/auth.test.ts +0 -51
- package/server/auth.ts +0 -122
- package/server/command.test.ts +0 -18
- package/server/command.ts +0 -92
- package/server/connector.test.ts +0 -1136
- package/server/connector.ts +0 -1386
- package/server/history.test.ts +0 -139
- package/server/history.ts +0 -157
- package/server/mcp-proxy.test.ts +0 -368
- package/server/mcp-proxy.ts +0 -375
- package/server/provider.test.ts +0 -11
- package/server/provider.ts +0 -37
- package/server/sanitize.test.ts +0 -174
- package/server/sanitize.ts +0 -286
- package/server/skills.test.ts +0 -121
- package/server/skills.ts +0 -183
- package/server/steering.test.ts +0 -405
- package/server/steering.ts +0 -199
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,1138 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { once } from "node:events";
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import { agyCommand, ensureAntigravityBinary } from "./command.js";
|
|
7
|
+
import { ensurePatchedBinary } from "./agy-patch.js";
|
|
8
|
+
import { formatSystemContext, sanitizeAssistantText, StreamSanitizer } from "./sanitize.js";
|
|
9
|
+
import { ensureAuthenticated } from "./auth.js";
|
|
10
|
+
import { sharedMcpProxyPool } from "./mcp-proxy.js";
|
|
11
|
+
export const DEFAULT_HANG_GRACE_MS = 5000;
|
|
12
|
+
export const DEFAULT_HANG_INACTIVITY_MS = 30_000;
|
|
13
|
+
export const DEFAULT_RECYCLE_TIMEOUT_MS = 30_000;
|
|
14
|
+
export const RECYCLE_ID_PREFIX = "paseo-antigravity:recycle:";
|
|
15
|
+
export const RECYCLE_INIT_ID = `${RECYCLE_ID_PREFIX}init`;
|
|
16
|
+
export const RECYCLE_LOAD_ID = `${RECYCLE_ID_PREFIX}load`;
|
|
17
|
+
export const RECYCLE_MODE_ID = `${RECYCLE_ID_PREFIX}mode`;
|
|
18
|
+
export const RECYCLE_CONFIG_PREFIX = `${RECYCLE_ID_PREFIX}config:`;
|
|
19
|
+
function parseEnvMs(val, defaultMs) {
|
|
20
|
+
if (!val)
|
|
21
|
+
return defaultMs;
|
|
22
|
+
const num = Number(val);
|
|
23
|
+
return Number.isFinite(num) && num > 0 ? num : defaultMs;
|
|
24
|
+
}
|
|
25
|
+
function extractJsonCandidate(line) {
|
|
26
|
+
const marker = "RAW WS MSG:";
|
|
27
|
+
const idx = line.indexOf(marker);
|
|
28
|
+
if (idx !== -1) {
|
|
29
|
+
return line.slice(idx + marker.length).trim();
|
|
30
|
+
}
|
|
31
|
+
const keyIdx = line.indexOf('"trajectoryStateUpdate"');
|
|
32
|
+
if (keyIdx === -1)
|
|
33
|
+
return null;
|
|
34
|
+
const braceIdx = line.lastIndexOf("{", keyIdx);
|
|
35
|
+
if (braceIdx === -1)
|
|
36
|
+
return null;
|
|
37
|
+
return line.slice(braceIdx).trim();
|
|
38
|
+
}
|
|
39
|
+
function parseTsuObject(parsed) {
|
|
40
|
+
if (!parsed || typeof parsed !== "object")
|
|
41
|
+
return null;
|
|
42
|
+
const tsu = parsed.trajectoryStateUpdate;
|
|
43
|
+
if (!tsu || typeof tsu !== "object")
|
|
44
|
+
return null;
|
|
45
|
+
const o = tsu;
|
|
46
|
+
if (typeof o.trajectoryId !== "string" || typeof o.state !== "string")
|
|
47
|
+
return null;
|
|
48
|
+
return { trajectoryId: o.trajectoryId, state: o.state };
|
|
49
|
+
}
|
|
50
|
+
export function parseTrajectoryStateUpdate(line) {
|
|
51
|
+
const candidate = extractJsonCandidate(line);
|
|
52
|
+
if (!candidate)
|
|
53
|
+
return null;
|
|
54
|
+
try {
|
|
55
|
+
return parseTsuObject(JSON.parse(candidate));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
const lastBrace = candidate.lastIndexOf("}");
|
|
59
|
+
if (lastBrace === -1)
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
return parseTsuObject(JSON.parse(candidate.slice(0, lastBrace + 1)));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function parseStepUpdateObject(parsed) {
|
|
70
|
+
if (!parsed || typeof parsed !== "object")
|
|
71
|
+
return null;
|
|
72
|
+
const su = parsed.stepUpdate;
|
|
73
|
+
if (!su || typeof su !== "object")
|
|
74
|
+
return null;
|
|
75
|
+
const o = su;
|
|
76
|
+
if (typeof o.trajectoryId !== "string" || typeof o.state !== "string")
|
|
77
|
+
return null;
|
|
78
|
+
return {
|
|
79
|
+
trajectoryId: o.trajectoryId,
|
|
80
|
+
state: o.state,
|
|
81
|
+
target: typeof o.target === "string" ? o.target : undefined,
|
|
82
|
+
source: typeof o.source === "string" ? o.source : undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function parseStepUpdate(line) {
|
|
86
|
+
const candidate = extractJsonCandidate(line);
|
|
87
|
+
if (!candidate)
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
90
|
+
return parseStepUpdateObject(JSON.parse(candidate));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
const lastBrace = candidate.lastIndexOf("}");
|
|
94
|
+
if (lastBrace === -1)
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
return parseStepUpdateObject(JSON.parse(candidate.slice(0, lastBrace + 1)));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export class HangDetector {
|
|
105
|
+
graceMs;
|
|
106
|
+
inactivityMs;
|
|
107
|
+
onHangDeclared;
|
|
108
|
+
pendingPrompts = new Map();
|
|
109
|
+
lastStates = new Map();
|
|
110
|
+
hungSessions = new Set();
|
|
111
|
+
now;
|
|
112
|
+
constructor(options) {
|
|
113
|
+
this.graceMs =
|
|
114
|
+
options?.graceMs ?? parseEnvMs(process.env.PASEO_AGY_HANG_GRACE_MS, DEFAULT_HANG_GRACE_MS);
|
|
115
|
+
this.inactivityMs =
|
|
116
|
+
options?.inactivityMs ??
|
|
117
|
+
parseEnvMs(process.env.PASEO_AGY_HANG_INACTIVITY_MS, DEFAULT_HANG_INACTIVITY_MS);
|
|
118
|
+
this.onHangDeclared = options?.onHangDeclared;
|
|
119
|
+
this.now = options?.now ?? Date.now;
|
|
120
|
+
}
|
|
121
|
+
isHung(nativeSessionId) {
|
|
122
|
+
return this.hungSessions.has(nativeSessionId);
|
|
123
|
+
}
|
|
124
|
+
clearHung(nativeSessionId) {
|
|
125
|
+
this.hungSessions.delete(nativeSessionId);
|
|
126
|
+
this.lastStates.delete(nativeSessionId);
|
|
127
|
+
}
|
|
128
|
+
scheduleInactivityTimer(nativeSessionId, promptId, prompt) {
|
|
129
|
+
if (prompt.inactivityTimer) {
|
|
130
|
+
clearTimeout(prompt.inactivityTimer);
|
|
131
|
+
}
|
|
132
|
+
prompt.inactivityTimer = setTimeout(() => {
|
|
133
|
+
this.checkInactivity(nativeSessionId, promptId);
|
|
134
|
+
}, this.inactivityMs);
|
|
135
|
+
}
|
|
136
|
+
startGraceTimer(nativeSessionId, id, prompt) {
|
|
137
|
+
if (prompt.timer)
|
|
138
|
+
return;
|
|
139
|
+
prompt.timer = setTimeout(() => {
|
|
140
|
+
this.onGraceTimerFired(nativeSessionId, id);
|
|
141
|
+
}, this.graceMs);
|
|
142
|
+
}
|
|
143
|
+
recordPrompt(id, nativeSessionId) {
|
|
144
|
+
const now = this.now();
|
|
145
|
+
const prompt = {
|
|
146
|
+
nativeSessionId,
|
|
147
|
+
sentAt: now,
|
|
148
|
+
lastActivityAt: now,
|
|
149
|
+
};
|
|
150
|
+
this.pendingPrompts.set(id, prompt);
|
|
151
|
+
this.scheduleInactivityTimer(nativeSessionId, id, prompt);
|
|
152
|
+
if (this.lastStates.get(nativeSessionId) === "STATE_WAITING_FOR_TASKS") {
|
|
153
|
+
this.startGraceTimer(nativeSessionId, id, prompt);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
recordActivity(nativeSessionId) {
|
|
157
|
+
const now = this.now();
|
|
158
|
+
for (const [id, prompt] of this.pendingPrompts.entries()) {
|
|
159
|
+
if (prompt.nativeSessionId === nativeSessionId) {
|
|
160
|
+
prompt.lastActivityAt = now;
|
|
161
|
+
this.scheduleInactivityTimer(nativeSessionId, id, prompt);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
resolvePrompt(id) {
|
|
166
|
+
const prompt = this.pendingPrompts.get(id);
|
|
167
|
+
if (!prompt)
|
|
168
|
+
return;
|
|
169
|
+
if (prompt.timer) {
|
|
170
|
+
clearTimeout(prompt.timer);
|
|
171
|
+
}
|
|
172
|
+
if (prompt.inactivityTimer) {
|
|
173
|
+
clearTimeout(prompt.inactivityTimer);
|
|
174
|
+
}
|
|
175
|
+
this.pendingPrompts.delete(id);
|
|
176
|
+
}
|
|
177
|
+
clearPromptTimers(prompt) {
|
|
178
|
+
if (prompt.timer) {
|
|
179
|
+
clearTimeout(prompt.timer);
|
|
180
|
+
prompt.timer = undefined;
|
|
181
|
+
}
|
|
182
|
+
if (prompt.inactivityTimer) {
|
|
183
|
+
clearTimeout(prompt.inactivityTimer);
|
|
184
|
+
prompt.inactivityTimer = undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
logHangReason(nativeSessionId, promptId, reason) {
|
|
188
|
+
if (reason === "inactivity") {
|
|
189
|
+
console.error(`[paseo-antigravity] Inactivity backstop fired for session ${nativeSessionId}, prompt ${promptId}`);
|
|
190
|
+
}
|
|
191
|
+
else if (reason === "turn_complete") {
|
|
192
|
+
console.error(`[paseo-antigravity] Turn completion grace timer fired for session ${nativeSessionId}, prompt ${promptId}`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
console.error(`[paseo-antigravity] Hang declared for session ${nativeSessionId}, prompt ${promptId} (STATE_WAITING_FOR_TASKS)`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
declareHang(nativeSessionId, promptId, reason) {
|
|
199
|
+
this.hungSessions.add(nativeSessionId);
|
|
200
|
+
const prompt = this.pendingPrompts.get(promptId);
|
|
201
|
+
if (prompt) {
|
|
202
|
+
this.clearPromptTimers(prompt);
|
|
203
|
+
this.pendingPrompts.delete(promptId);
|
|
204
|
+
}
|
|
205
|
+
this.logHangReason(nativeSessionId, promptId, reason);
|
|
206
|
+
this.onHangDeclared?.(nativeSessionId, promptId);
|
|
207
|
+
}
|
|
208
|
+
onGraceTimerFired(nativeSessionId, promptId, reason = "grace") {
|
|
209
|
+
const prompt = this.pendingPrompts.get(promptId);
|
|
210
|
+
if (!prompt)
|
|
211
|
+
return;
|
|
212
|
+
prompt.timer = undefined;
|
|
213
|
+
this.declareHang(nativeSessionId, promptId, reason);
|
|
214
|
+
}
|
|
215
|
+
checkInactivity(nativeSessionId, promptId) {
|
|
216
|
+
const prompt = this.pendingPrompts.get(promptId);
|
|
217
|
+
if (!prompt)
|
|
218
|
+
return;
|
|
219
|
+
const elapsed = this.now() - prompt.lastActivityAt;
|
|
220
|
+
if (elapsed >= this.inactivityMs) {
|
|
221
|
+
this.declareHang(nativeSessionId, promptId, "inactivity");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const remaining = this.inactivityMs - elapsed;
|
|
225
|
+
prompt.inactivityTimer = setTimeout(() => {
|
|
226
|
+
this.checkInactivity(nativeSessionId, promptId);
|
|
227
|
+
}, remaining);
|
|
228
|
+
}
|
|
229
|
+
handleWaitingForTasks(nativeSessionId) {
|
|
230
|
+
for (const [id, prompt] of this.pendingPrompts.entries()) {
|
|
231
|
+
if (prompt.nativeSessionId === nativeSessionId) {
|
|
232
|
+
this.startGraceTimer(nativeSessionId, id, prompt);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
handleTurnCompleteCandidate(nativeSessionId) {
|
|
237
|
+
for (const [id, prompt] of this.pendingPrompts.entries()) {
|
|
238
|
+
if (prompt.nativeSessionId === nativeSessionId && !prompt.timer) {
|
|
239
|
+
prompt.timer = setTimeout(() => {
|
|
240
|
+
this.onGraceTimerFired(nativeSessionId, id, "turn_complete");
|
|
241
|
+
}, this.graceMs);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
clearGraceTimersForSession(nativeSessionId) {
|
|
246
|
+
for (const prompt of this.pendingPrompts.values()) {
|
|
247
|
+
if (prompt.nativeSessionId === nativeSessionId && prompt.timer) {
|
|
248
|
+
clearTimeout(prompt.timer);
|
|
249
|
+
prompt.timer = undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
processStderrLine(line) {
|
|
254
|
+
const tsu = parseTrajectoryStateUpdate(line);
|
|
255
|
+
if (tsu) {
|
|
256
|
+
const { trajectoryId, state } = tsu;
|
|
257
|
+
this.lastStates.set(trajectoryId, state);
|
|
258
|
+
if (state === "STATE_WAITING_FOR_TASKS") {
|
|
259
|
+
this.handleWaitingForTasks(trajectoryId);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
this.clearGraceTimersForSession(trajectoryId);
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const su = parseStepUpdate(line);
|
|
267
|
+
if (su) {
|
|
268
|
+
const { trajectoryId, state, target } = su;
|
|
269
|
+
if (state === "STATE_DONE" && target === "TARGET_USER") {
|
|
270
|
+
this.handleTurnCompleteCandidate(trajectoryId);
|
|
271
|
+
}
|
|
272
|
+
else if (state === "STATE_ACTIVE" && target === "TARGET_ENVIRONMENT") {
|
|
273
|
+
// Active environment step (e.g. running a tool) clears grace timer
|
|
274
|
+
this.clearGraceTimersForSession(trajectoryId);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
processOutbound(msg) {
|
|
279
|
+
if (!("method" in msg) || msg.method !== "session/prompt")
|
|
280
|
+
return;
|
|
281
|
+
const m = msg;
|
|
282
|
+
if (typeof m.id !== "string" && typeof m.id !== "number")
|
|
283
|
+
return;
|
|
284
|
+
if (typeof m.params?.sessionId !== "string")
|
|
285
|
+
return;
|
|
286
|
+
this.recordPrompt(m.id, m.params.sessionId);
|
|
287
|
+
}
|
|
288
|
+
processInbound(msg) {
|
|
289
|
+
if ("method" in msg && msg.method === "session/update") {
|
|
290
|
+
const sessionId = msg.params?.sessionId;
|
|
291
|
+
if (typeof sessionId === "string") {
|
|
292
|
+
this.recordActivity(sessionId);
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (("result" in msg || "error" in msg) && "id" in msg) {
|
|
297
|
+
const id = msg.id;
|
|
298
|
+
if (typeof id === "string" || typeof id === "number") {
|
|
299
|
+
this.resolvePrompt(id);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
dispose() {
|
|
304
|
+
for (const prompt of this.pendingPrompts.values()) {
|
|
305
|
+
this.clearPromptTimers(prompt);
|
|
306
|
+
}
|
|
307
|
+
this.pendingPrompts.clear();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Strips duplicate mode configuration options from ACP session/new and session/load responses.
|
|
312
|
+
*
|
|
313
|
+
* agy_acp_server returns modes both in `result.modes` and as a setting option with `id: "mode"`
|
|
314
|
+
* in `result.configOptions`. Filtering out `id: "mode"` prevents duplicate controls in the Paseo UI.
|
|
315
|
+
*/
|
|
316
|
+
export function filterConfigOptions(result) {
|
|
317
|
+
if (!result || typeof result !== "object" || !("configOptions" in result))
|
|
318
|
+
return;
|
|
319
|
+
const res = result;
|
|
320
|
+
if (!Array.isArray(res.configOptions))
|
|
321
|
+
return;
|
|
322
|
+
res.configOptions = res.configOptions.filter((opt) => {
|
|
323
|
+
if (!opt || typeof opt !== "object")
|
|
324
|
+
return true;
|
|
325
|
+
const o = opt;
|
|
326
|
+
return o.id !== "mode" && o.category !== "mode";
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function fallbackSanitizeBlock(block) {
|
|
330
|
+
if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
|
|
331
|
+
block.text = sanitizeAssistantText(block.text);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function fallbackSanitizeInboundText(msg) {
|
|
335
|
+
if (!("method" in msg) || msg.method !== "session/update")
|
|
336
|
+
return;
|
|
337
|
+
const content = msg.params?.update?.content;
|
|
338
|
+
if (!content)
|
|
339
|
+
return;
|
|
340
|
+
if (Array.isArray(content)) {
|
|
341
|
+
for (const block of content)
|
|
342
|
+
fallbackSanitizeBlock(block);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
fallbackSanitizeBlock(content);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function sanitizeBlock(block, sanitizer) {
|
|
349
|
+
if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
|
|
350
|
+
block.text = sanitizer.process(block.text);
|
|
351
|
+
return block.text !== "";
|
|
352
|
+
}
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
function extractUpdateContent(msg) {
|
|
356
|
+
if (!("method" in msg) || msg.method !== "session/update")
|
|
357
|
+
return null;
|
|
358
|
+
const update = msg.params?.update;
|
|
359
|
+
if (!update || typeof update !== "object")
|
|
360
|
+
return null;
|
|
361
|
+
const content = update.content;
|
|
362
|
+
if (!content)
|
|
363
|
+
return null;
|
|
364
|
+
const isMessageChunk = update.sessionUpdate === "agent_message_chunk";
|
|
365
|
+
return { content, isMessageChunk };
|
|
366
|
+
}
|
|
367
|
+
function sanitizeInboundText(msg, sanitizer) {
|
|
368
|
+
const target = extractUpdateContent(msg);
|
|
369
|
+
if (!target)
|
|
370
|
+
return true;
|
|
371
|
+
let hasNonEmpty = false;
|
|
372
|
+
if (Array.isArray(target.content)) {
|
|
373
|
+
for (const block of target.content) {
|
|
374
|
+
if (sanitizeBlock(block, sanitizer))
|
|
375
|
+
hasNonEmpty = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
hasNonEmpty = sanitizeBlock(target.content, sanitizer);
|
|
380
|
+
}
|
|
381
|
+
return !target.isMessageChunk || hasNonEmpty;
|
|
382
|
+
}
|
|
383
|
+
export function createSystemPromptState() {
|
|
384
|
+
return {
|
|
385
|
+
pendingSystemPrompts: new Map(),
|
|
386
|
+
sessionSystemPrompts: new Map(),
|
|
387
|
+
injectedSessions: new Set(),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const defaultSystemPromptState = createSystemPromptState();
|
|
391
|
+
export function resetSystemPromptState() {
|
|
392
|
+
defaultSystemPromptState.pendingSystemPrompts.clear();
|
|
393
|
+
defaultSystemPromptState.sessionSystemPrompts.clear();
|
|
394
|
+
defaultSystemPromptState.injectedSessions.clear();
|
|
395
|
+
}
|
|
396
|
+
function sessionIdOf(msg) {
|
|
397
|
+
if (!("method" in msg) || msg.method !== "session/update")
|
|
398
|
+
return null;
|
|
399
|
+
const sessionId = msg.params?.sessionId;
|
|
400
|
+
return typeof sessionId === "string" ? sessionId : null;
|
|
401
|
+
}
|
|
402
|
+
function agentMessageChunk(sessionId, text) {
|
|
403
|
+
return {
|
|
404
|
+
jsonrpc: "2.0",
|
|
405
|
+
method: "session/update",
|
|
406
|
+
params: {
|
|
407
|
+
sessionId,
|
|
408
|
+
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } },
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Processes an inbound ACP message received from `agy_acp_server`:
|
|
414
|
+
* 1. Filters duplicate mode controls from `configOptions`.
|
|
415
|
+
* 2. Sanitizes assistant streaming text notifications across chunk boundaries.
|
|
416
|
+
* 3. Flushes text still held in the sanitizer's look-ahead buffer at a turn boundary.
|
|
417
|
+
*
|
|
418
|
+
* Returns the messages to forward. A streaming chunk that sanitizes to nothing is
|
|
419
|
+
* dropped, and a turn boundary may yield an extra chunk carrying the flushed tail.
|
|
420
|
+
*/
|
|
421
|
+
function closeTurn(msg, sanitizer, state) {
|
|
422
|
+
filterConfigOptions(msg.result);
|
|
423
|
+
// A response ends the turn. Whatever is still buffered was a suspected harness tag
|
|
424
|
+
// that never materialized, so it is real text and must be emitted, not discarded.
|
|
425
|
+
const tail = sanitizer?.flush() ?? "";
|
|
426
|
+
sanitizer?.reset();
|
|
427
|
+
const sessionId = state?.lastSessionId;
|
|
428
|
+
return tail && sessionId ? [agentMessageChunk(sessionId, tail), msg] : [msg];
|
|
429
|
+
}
|
|
430
|
+
export function createSessionCache() {
|
|
431
|
+
return {
|
|
432
|
+
sessions: new Map(),
|
|
433
|
+
pendingSessionMetadata: new Map(),
|
|
434
|
+
cachedInitializeParams: undefined,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const defaultSessionCache = createSessionCache();
|
|
438
|
+
export function resetSessionCache() {
|
|
439
|
+
defaultSessionCache.sessions.clear();
|
|
440
|
+
defaultSessionCache.pendingSessionMetadata.clear();
|
|
441
|
+
defaultSessionCache.cachedInitializeParams = undefined;
|
|
442
|
+
}
|
|
443
|
+
export function getOrCreateSession(cache, sessionId) {
|
|
444
|
+
let session = cache.sessions.get(sessionId);
|
|
445
|
+
if (!session) {
|
|
446
|
+
session = {
|
|
447
|
+
sessionId,
|
|
448
|
+
lastConfigOptions: new Map(),
|
|
449
|
+
};
|
|
450
|
+
cache.sessions.set(sessionId, session);
|
|
451
|
+
}
|
|
452
|
+
return session;
|
|
453
|
+
}
|
|
454
|
+
function recordAllocatedSessionPrompt(id, sessionId, promptState) {
|
|
455
|
+
if (promptState.pendingSystemPrompts.has(id)) {
|
|
456
|
+
const systemPrompt = promptState.pendingSystemPrompts.get(id);
|
|
457
|
+
promptState.pendingSystemPrompts.delete(id);
|
|
458
|
+
promptState.sessionSystemPrompts.set(sessionId, systemPrompt);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function recordAllocatedSessionMeta(id, sessionId, cache) {
|
|
462
|
+
if (cache.pendingSessionMetadata.has(id)) {
|
|
463
|
+
const pending = cache.pendingSessionMetadata.get(id);
|
|
464
|
+
cache.pendingSessionMetadata.delete(id);
|
|
465
|
+
const session = getOrCreateSession(cache, sessionId);
|
|
466
|
+
if (pending?.cwd !== undefined)
|
|
467
|
+
session.cwd = pending.cwd;
|
|
468
|
+
if (pending?.mcpServers !== undefined)
|
|
469
|
+
session.mcpServers = pending.mcpServers;
|
|
470
|
+
if (pending?._meta !== undefined)
|
|
471
|
+
session._meta = pending._meta;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function recordAllocatedSession(msg, promptState, cache) {
|
|
475
|
+
if (!("result" in msg) || !msg.result || typeof msg.result !== "object")
|
|
476
|
+
return;
|
|
477
|
+
if (!("sessionId" in msg.result))
|
|
478
|
+
return;
|
|
479
|
+
const sessionId = msg.result.sessionId;
|
|
480
|
+
if (typeof sessionId !== "string" || !("id" in msg) || msg.id === null || msg.id === undefined) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
recordAllocatedSessionPrompt(msg.id, sessionId, promptState);
|
|
484
|
+
recordAllocatedSessionMeta(msg.id, sessionId, cache);
|
|
485
|
+
}
|
|
486
|
+
export function processInboundMessage(msg, sanitizer, state, promptState = defaultSystemPromptState, sessionCache = defaultSessionCache) {
|
|
487
|
+
if ("result" in msg && msg.result) {
|
|
488
|
+
recordAllocatedSession(msg, promptState, sessionCache);
|
|
489
|
+
return closeTurn(msg, sanitizer, state);
|
|
490
|
+
}
|
|
491
|
+
if (state) {
|
|
492
|
+
const sessionId = sessionIdOf(msg);
|
|
493
|
+
if (sessionId)
|
|
494
|
+
state.lastSessionId = sessionId;
|
|
495
|
+
}
|
|
496
|
+
if (sanitizer) {
|
|
497
|
+
return sanitizeInboundText(msg, sanitizer) ? [msg] : [];
|
|
498
|
+
}
|
|
499
|
+
fallbackSanitizeInboundText(msg);
|
|
500
|
+
return [msg];
|
|
501
|
+
}
|
|
502
|
+
function normalizeMcpHeaders(headers) {
|
|
503
|
+
if (!headers)
|
|
504
|
+
return undefined;
|
|
505
|
+
if (Array.isArray(headers)) {
|
|
506
|
+
return headers.flatMap((h) => {
|
|
507
|
+
if (!h || typeof h !== "object")
|
|
508
|
+
return [];
|
|
509
|
+
const name = typeof h.name === "string" ? h.name : typeof h.key === "string" ? h.key : "";
|
|
510
|
+
const value = typeof h.value === "string" ? h.value : "";
|
|
511
|
+
return name ? [{ name, value }] : [];
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (typeof headers === "object") {
|
|
515
|
+
return Object.entries(headers).flatMap(([name, val]) => typeof val === "string" ? [{ name, value: val }] : []);
|
|
516
|
+
}
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Rewrites Paseo MCP server declarations for agy_acp_server:
|
|
521
|
+
* 1. Routes the URL through a local proxy that normalizes the MCP protocol version.
|
|
522
|
+
* 2. Normalizes headers to array shape [{ name, value }].
|
|
523
|
+
*
|
|
524
|
+
* The transport type is passed through untouched. agy advertises
|
|
525
|
+
* `mcpCapabilities: {http: true, sse: true}` and issues streamable-HTTP POSTs either
|
|
526
|
+
* way, so declaring Paseo's streamable-HTTP endpoint as "sse" only misdescribed it.
|
|
527
|
+
*/
|
|
528
|
+
export async function rewriteMcpServers(servers, proxies) {
|
|
529
|
+
for (const s of servers) {
|
|
530
|
+
if (!s || typeof s !== "object")
|
|
531
|
+
continue;
|
|
532
|
+
const server = s;
|
|
533
|
+
if (server.url && proxies) {
|
|
534
|
+
server.url = await proxies.rewriteUrl(server.url);
|
|
535
|
+
}
|
|
536
|
+
const normalized = normalizeMcpHeaders(server.headers);
|
|
537
|
+
if (normalized) {
|
|
538
|
+
server.headers = normalized;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Processes an outbound ACP message sent from Paseo to `agy_acp_server`:
|
|
544
|
+
* 1. Rewrites MCP server declarations (URL proxying, headers normalization).
|
|
545
|
+
* 2. Maps legacy / generic mode IDs ("accept-edits" -> "auto_edit", "dangerously-skip-permissions" -> "yolo").
|
|
546
|
+
*/
|
|
547
|
+
function remapModeId(modeId) {
|
|
548
|
+
if (modeId === "accept-edits")
|
|
549
|
+
return "auto_edit";
|
|
550
|
+
if (modeId === "dangerously-skip-permissions")
|
|
551
|
+
return "yolo";
|
|
552
|
+
if (modeId === "plan")
|
|
553
|
+
return "default";
|
|
554
|
+
return modeId;
|
|
555
|
+
}
|
|
556
|
+
function captureSessionNewPrompt(msg, promptState) {
|
|
557
|
+
if (!("id" in msg) || msg.id === null || msg.id === undefined)
|
|
558
|
+
return;
|
|
559
|
+
const params = msg.params;
|
|
560
|
+
const systemPrompt = params?._meta?._paseo?.systemPrompt;
|
|
561
|
+
promptState.pendingSystemPrompts.set(msg.id, systemPrompt);
|
|
562
|
+
}
|
|
563
|
+
function injectSystemContext(prompt, systemPrompt) {
|
|
564
|
+
if (!Array.isArray(prompt))
|
|
565
|
+
return;
|
|
566
|
+
const textPart = prompt.find((p) => Boolean(p &&
|
|
567
|
+
typeof p === "object" &&
|
|
568
|
+
p.type === "text" &&
|
|
569
|
+
typeof p.text === "string"));
|
|
570
|
+
const formatted = `${formatSystemContext(systemPrompt)}
|
|
571
|
+
|
|
572
|
+
`;
|
|
573
|
+
if (textPart) {
|
|
574
|
+
textPart.text = `${formatted}${textPart.text}`;
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
prompt.unshift({ type: "text", text: formatted.trimEnd() });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function handleSessionPrompt(params, promptState) {
|
|
581
|
+
const sessionId = typeof params?.sessionId === "string" ? params.sessionId : null;
|
|
582
|
+
if (!sessionId)
|
|
583
|
+
return;
|
|
584
|
+
if (!promptState.sessionSystemPrompts.has(sessionId))
|
|
585
|
+
return;
|
|
586
|
+
if (promptState.injectedSessions.has(sessionId))
|
|
587
|
+
return;
|
|
588
|
+
promptState.injectedSessions.add(sessionId);
|
|
589
|
+
const systemPrompt = promptState.sessionSystemPrompts.get(sessionId);
|
|
590
|
+
injectSystemContext(params?.prompt, systemPrompt);
|
|
591
|
+
}
|
|
592
|
+
function captureSessionNewMetadata(msg, cache) {
|
|
593
|
+
if (!("id" in msg) || msg.id === null || msg.id === undefined)
|
|
594
|
+
return;
|
|
595
|
+
const params = msg.params;
|
|
596
|
+
cache.pendingSessionMetadata.set(msg.id, {
|
|
597
|
+
cwd: typeof params?.cwd === "string" ? params.cwd : undefined,
|
|
598
|
+
mcpServers: Array.isArray(params?.mcpServers) ? params?.mcpServers : undefined,
|
|
599
|
+
_meta: params?._meta,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function captureSessionLoadMetadata(msg, cache) {
|
|
603
|
+
const params = msg.params;
|
|
604
|
+
if (typeof params?.sessionId !== "string")
|
|
605
|
+
return;
|
|
606
|
+
const session = getOrCreateSession(cache, params.sessionId);
|
|
607
|
+
if (typeof params.cwd === "string")
|
|
608
|
+
session.cwd = params.cwd;
|
|
609
|
+
if (Array.isArray(params.mcpServers))
|
|
610
|
+
session.mcpServers = params.mcpServers;
|
|
611
|
+
if (params._meta !== undefined)
|
|
612
|
+
session._meta = params._meta;
|
|
613
|
+
}
|
|
614
|
+
function captureSetMode(msg, cache) {
|
|
615
|
+
const params = msg.params;
|
|
616
|
+
if (typeof params?.sessionId !== "string" || typeof params?.modeId !== "string")
|
|
617
|
+
return;
|
|
618
|
+
const session = getOrCreateSession(cache, params.sessionId);
|
|
619
|
+
session.lastModeId = params.modeId;
|
|
620
|
+
}
|
|
621
|
+
function captureSetConfigOption(msg, cache) {
|
|
622
|
+
const params = msg.params;
|
|
623
|
+
if (typeof params?.sessionId !== "string" || typeof params?.configId !== "string")
|
|
624
|
+
return;
|
|
625
|
+
const session = getOrCreateSession(cache, params.sessionId);
|
|
626
|
+
session.lastConfigOptions.set(params.configId, {
|
|
627
|
+
value: params.value,
|
|
628
|
+
type: typeof params.type === "string" ? params.type : undefined,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function cacheInitializeParams(msg, cache) {
|
|
632
|
+
if ("params" in msg) {
|
|
633
|
+
cache.cachedInitializeParams = msg.params;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
export function extractSessionId(msg) {
|
|
637
|
+
if (!("params" in msg) || !msg.params || typeof msg.params !== "object") {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
const sid = msg.params.sessionId;
|
|
641
|
+
return typeof sid === "string" ? sid : null;
|
|
642
|
+
}
|
|
643
|
+
async function adaptOutboundParams(params, proxies) {
|
|
644
|
+
if (params?.mcpServers && Array.isArray(params.mcpServers)) {
|
|
645
|
+
await rewriteMcpServers(params.mcpServers, proxies);
|
|
646
|
+
}
|
|
647
|
+
if (params?.modeId) {
|
|
648
|
+
params.modeId = remapModeId(params.modeId);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function dispatchOutboundMethod(msg, params, promptState, sessionCache) {
|
|
652
|
+
switch (msg.method) {
|
|
653
|
+
case "initialize":
|
|
654
|
+
cacheInitializeParams(msg, sessionCache);
|
|
655
|
+
break;
|
|
656
|
+
case "session/new":
|
|
657
|
+
captureSessionNewPrompt(msg, promptState);
|
|
658
|
+
captureSessionNewMetadata(msg, sessionCache);
|
|
659
|
+
break;
|
|
660
|
+
case "session/load":
|
|
661
|
+
captureSessionLoadMetadata(msg, sessionCache);
|
|
662
|
+
break;
|
|
663
|
+
case "session/set_mode":
|
|
664
|
+
captureSetMode(msg, sessionCache);
|
|
665
|
+
break;
|
|
666
|
+
case "session/set_config_option":
|
|
667
|
+
captureSetConfigOption(msg, sessionCache);
|
|
668
|
+
break;
|
|
669
|
+
case "session/prompt":
|
|
670
|
+
handleSessionPrompt(params, promptState);
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
export async function processOutboundMessage(msg, proxies, promptState = defaultSystemPromptState, sessionCache = defaultSessionCache) {
|
|
675
|
+
if (!("method" in msg))
|
|
676
|
+
return msg;
|
|
677
|
+
const params = msg.params;
|
|
678
|
+
await adaptOutboundParams(params, proxies);
|
|
679
|
+
dispatchOutboundMethod(msg, params, promptState, sessionCache);
|
|
680
|
+
return msg;
|
|
681
|
+
}
|
|
682
|
+
const ACP_TRACE_ENABLED = process.env.PASEO_AGY_TRACE === "1";
|
|
683
|
+
/**
|
|
684
|
+
* Logs ACP request/response ids when PASEO_AGY_TRACE=1.
|
|
685
|
+
*
|
|
686
|
+
* An ACP request that never receives a response leaves Paseo waiting out its 60s
|
|
687
|
+
* session RPC timeout with no indication of which call stalled; this names it.
|
|
688
|
+
*/
|
|
689
|
+
function traceAcp(direction, msg) {
|
|
690
|
+
if (!ACP_TRACE_ENABLED)
|
|
691
|
+
return;
|
|
692
|
+
const m = msg;
|
|
693
|
+
const id = m.id === undefined ? "-" : String(m.id);
|
|
694
|
+
const kind = m.method ? String(m.method) : m.error ? "error" : "result";
|
|
695
|
+
console.error(`[paseo-antigravity][acp] ${direction} id=${id} ${kind}`);
|
|
696
|
+
}
|
|
697
|
+
export function resolveHarnessPath(cmd) {
|
|
698
|
+
try {
|
|
699
|
+
const real = fs.realpathSync(cmd);
|
|
700
|
+
const sibling = path.join(path.dirname(real), "localharness_external");
|
|
701
|
+
if (fs.existsSync(sibling))
|
|
702
|
+
return sibling;
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// Ignore resolution errors
|
|
706
|
+
}
|
|
707
|
+
return undefined;
|
|
708
|
+
}
|
|
709
|
+
function defaultSpawnProcess(cmd, args) {
|
|
710
|
+
const env = {
|
|
711
|
+
...process.env,
|
|
712
|
+
AGY_ACP_FORCE_FILE_STORAGE: "1",
|
|
713
|
+
BROWSER: "false",
|
|
714
|
+
};
|
|
715
|
+
const harness = resolveHarnessPath(cmd);
|
|
716
|
+
if (harness) {
|
|
717
|
+
env.ANTIGRAVITY_HARNESS_PATH = harness;
|
|
718
|
+
}
|
|
719
|
+
return spawn(cmd, args, {
|
|
720
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
721
|
+
env,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
export class ChildSlot {
|
|
725
|
+
currentChild;
|
|
726
|
+
rlOut = null;
|
|
727
|
+
rlErr = null;
|
|
728
|
+
isRecycling = false;
|
|
729
|
+
isSwallowingUpdates = false;
|
|
730
|
+
suppressedResponseIds = new Set();
|
|
731
|
+
sessionCache = createSessionCache();
|
|
732
|
+
promptState = createSystemPromptState();
|
|
733
|
+
hangDetector;
|
|
734
|
+
state = { isClosed: false };
|
|
735
|
+
readableController = null;
|
|
736
|
+
cmd;
|
|
737
|
+
args;
|
|
738
|
+
spawnFn;
|
|
739
|
+
recycleTimeoutMs;
|
|
740
|
+
sanitizer = new StreamSanitizer();
|
|
741
|
+
inboundState = { lastSessionId: null };
|
|
742
|
+
pendingRequests = new Map();
|
|
743
|
+
activeRecycle = null;
|
|
744
|
+
constructor(params) {
|
|
745
|
+
this.cmd = params.cmd;
|
|
746
|
+
this.args = params.args;
|
|
747
|
+
this.currentChild = params.initialChild;
|
|
748
|
+
this.spawnFn = params.options?.spawnProcess ?? defaultSpawnProcess;
|
|
749
|
+
this.recycleTimeoutMs =
|
|
750
|
+
params.options?.recycleTimeoutMs ??
|
|
751
|
+
parseEnvMs(process.env.PASEO_AGY_RECYCLE_TIMEOUT_MS, DEFAULT_RECYCLE_TIMEOUT_MS);
|
|
752
|
+
const userOnHang = params.options?.onHangDeclared;
|
|
753
|
+
this.hangDetector = new HangDetector({
|
|
754
|
+
...params.options,
|
|
755
|
+
onHangDeclared: (nativeSessionId, promptId) => {
|
|
756
|
+
this.handleHangDeclared(nativeSessionId, promptId);
|
|
757
|
+
userOnHang?.(nativeSessionId, promptId);
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
this.bindChild(this.currentChild);
|
|
761
|
+
}
|
|
762
|
+
setReadableController(controller) {
|
|
763
|
+
this.readableController = controller;
|
|
764
|
+
}
|
|
765
|
+
parseJsonLine(line) {
|
|
766
|
+
try {
|
|
767
|
+
return JSON.parse(line);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
tryHandleInternalRecycle(msg) {
|
|
774
|
+
if (!("id" in msg) || msg.id === null || msg.id === undefined)
|
|
775
|
+
return false;
|
|
776
|
+
const idStr = String(msg.id);
|
|
777
|
+
if (!idStr.startsWith(RECYCLE_ID_PREFIX))
|
|
778
|
+
return false;
|
|
779
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
780
|
+
if (pending) {
|
|
781
|
+
clearTimeout(pending.timer);
|
|
782
|
+
this.pendingRequests.delete(msg.id);
|
|
783
|
+
if ("error" in msg && msg.error) {
|
|
784
|
+
pending.reject(new Error(msg.error.message || "Recycle request failed"));
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
pending.resolve(msg);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
tryHandleSuppressedResponse(msg) {
|
|
793
|
+
if (!("id" in msg) || msg.id === null || msg.id === undefined)
|
|
794
|
+
return false;
|
|
795
|
+
if (this.suppressedResponseIds.has(msg.id)) {
|
|
796
|
+
this.suppressedResponseIds.delete(msg.id);
|
|
797
|
+
console.error(`[paseo-antigravity] Dropped late response for hung prompt ${msg.id}`);
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
forwardInbound(msg) {
|
|
803
|
+
const list = processInboundMessage(msg, this.sanitizer, this.inboundState, this.promptState, this.sessionCache);
|
|
804
|
+
for (const processed of list) {
|
|
805
|
+
try {
|
|
806
|
+
this.readableController?.enqueue(processed);
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
// Controller may already be closed
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
handleStdoutLine(line) {
|
|
814
|
+
if (this.state.isClosed)
|
|
815
|
+
return;
|
|
816
|
+
const msg = this.parseJsonLine(line);
|
|
817
|
+
if (!msg)
|
|
818
|
+
return;
|
|
819
|
+
traceAcp("agy->paseo", msg);
|
|
820
|
+
if (this.tryHandleInternalRecycle(msg))
|
|
821
|
+
return;
|
|
822
|
+
if (this.tryHandleSuppressedResponse(msg))
|
|
823
|
+
return;
|
|
824
|
+
if (this.isSwallowingUpdates && "method" in msg && msg.method === "session/update")
|
|
825
|
+
return;
|
|
826
|
+
this.hangDetector.processInbound(msg);
|
|
827
|
+
this.forwardInbound(msg);
|
|
828
|
+
}
|
|
829
|
+
handleChildClose(err) {
|
|
830
|
+
if (this.isRecycling)
|
|
831
|
+
return;
|
|
832
|
+
if (this.state.isClosed)
|
|
833
|
+
return;
|
|
834
|
+
this.state.isClosed = true;
|
|
835
|
+
this.hangDetector.dispose();
|
|
836
|
+
this.rlErr?.close();
|
|
837
|
+
this.rlOut?.close();
|
|
838
|
+
try {
|
|
839
|
+
if (err)
|
|
840
|
+
this.readableController?.error(err);
|
|
841
|
+
else
|
|
842
|
+
this.readableController?.close();
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// Stream already closed
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
handleHangDeclared(nativeSessionId, promptId) {
|
|
849
|
+
console.error(`[paseo-antigravity] Session ${nativeSessionId} hung on prompt ${promptId}; turn synthetic end_turn emitted, scheduled process recycle`);
|
|
850
|
+
this.suppressedResponseIds.add(promptId);
|
|
851
|
+
const session = getOrCreateSession(this.sessionCache, nativeSessionId);
|
|
852
|
+
session.needsRecycle = true;
|
|
853
|
+
const syntheticMsg = {
|
|
854
|
+
jsonrpc: "2.0",
|
|
855
|
+
id: promptId,
|
|
856
|
+
result: { stopReason: "end_turn" },
|
|
857
|
+
};
|
|
858
|
+
try {
|
|
859
|
+
this.readableController?.enqueue(syntheticMsg);
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// Controller may be closed
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
sendInternalRequest(child, id, method, params) {
|
|
866
|
+
return new Promise((resolve, reject) => {
|
|
867
|
+
const timer = setTimeout(() => {
|
|
868
|
+
this.pendingRequests.delete(id);
|
|
869
|
+
reject(new Error(`Recycle internal request ${method} (${id}) timed out after ${this.recycleTimeoutMs}ms`));
|
|
870
|
+
}, this.recycleTimeoutMs);
|
|
871
|
+
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
872
|
+
const req = { jsonrpc: "2.0", id, method, ...(params !== undefined && { params }) };
|
|
873
|
+
if (!child.stdin?.writable) {
|
|
874
|
+
clearTimeout(timer);
|
|
875
|
+
this.pendingRequests.delete(id);
|
|
876
|
+
reject(new Error(`Child stdin not writable for ${method}`));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
child.stdin.write(JSON.stringify(req) + "\n", (err) => {
|
|
880
|
+
if (err) {
|
|
881
|
+
clearTimeout(timer);
|
|
882
|
+
this.pendingRequests.delete(id);
|
|
883
|
+
reject(err);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
async resyncSessionLoad(session, newChild) {
|
|
889
|
+
this.isSwallowingUpdates = true;
|
|
890
|
+
try {
|
|
891
|
+
const loadParams = { sessionId: session.sessionId };
|
|
892
|
+
if (session.cwd !== undefined)
|
|
893
|
+
loadParams.cwd = session.cwd;
|
|
894
|
+
if (session.mcpServers !== undefined)
|
|
895
|
+
loadParams.mcpServers = session.mcpServers;
|
|
896
|
+
if (session._meta !== undefined)
|
|
897
|
+
loadParams._meta = session._meta;
|
|
898
|
+
await this.sendInternalRequest(newChild, RECYCLE_LOAD_ID, "session/load", loadParams);
|
|
899
|
+
}
|
|
900
|
+
finally {
|
|
901
|
+
this.isSwallowingUpdates = false;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async resyncSessionMode(session, newChild) {
|
|
905
|
+
if (!session.lastModeId)
|
|
906
|
+
return;
|
|
907
|
+
await this.sendInternalRequest(newChild, RECYCLE_MODE_ID, "session/set_mode", {
|
|
908
|
+
sessionId: session.sessionId,
|
|
909
|
+
modeId: session.lastModeId,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
async resyncSessionConfig(session, newChild) {
|
|
913
|
+
if (!session.lastConfigOptions || session.lastConfigOptions.size === 0)
|
|
914
|
+
return;
|
|
915
|
+
for (const [configId, opt] of session.lastConfigOptions.entries()) {
|
|
916
|
+
const configParams = {
|
|
917
|
+
sessionId: session.sessionId,
|
|
918
|
+
configId,
|
|
919
|
+
value: opt.value,
|
|
920
|
+
};
|
|
921
|
+
if (opt.type !== undefined)
|
|
922
|
+
configParams.type = opt.type;
|
|
923
|
+
await this.sendInternalRequest(newChild, `${RECYCLE_CONFIG_PREFIX}${configId}`, "session/set_config_option", configParams);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async performResync(session, newChild) {
|
|
927
|
+
const initParams = this.sessionCache.cachedInitializeParams ?? {
|
|
928
|
+
protocolVersion: 1,
|
|
929
|
+
clientCapabilities: {},
|
|
930
|
+
clientInfo: { name: "paseo", version: "1.0.0" },
|
|
931
|
+
};
|
|
932
|
+
await this.sendInternalRequest(newChild, RECYCLE_INIT_ID, "initialize", initParams);
|
|
933
|
+
await this.resyncSessionLoad(session, newChild);
|
|
934
|
+
await this.resyncSessionMode(session, newChild);
|
|
935
|
+
await this.resyncSessionConfig(session, newChild);
|
|
936
|
+
}
|
|
937
|
+
cleanupOldChild() {
|
|
938
|
+
const oldChild = this.currentChild;
|
|
939
|
+
this.rlOut?.close();
|
|
940
|
+
this.rlErr?.close();
|
|
941
|
+
this.rlOut = null;
|
|
942
|
+
this.rlErr = null;
|
|
943
|
+
if (oldChild && !oldChild.killed) {
|
|
944
|
+
oldChild.kill("SIGKILL");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
handleRecycleFailure(err) {
|
|
948
|
+
console.error("[paseo-antigravity] Process recycle failed:", err);
|
|
949
|
+
this.state.isClosed = true;
|
|
950
|
+
this.hangDetector.dispose();
|
|
951
|
+
this.rlErr?.close();
|
|
952
|
+
this.rlOut?.close();
|
|
953
|
+
if (this.currentChild && !this.currentChild.killed) {
|
|
954
|
+
this.currentChild.kill("SIGKILL");
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
958
|
+
this.readableController?.error(errorObj);
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
// Controller may already be closed
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async recycleProcess(session) {
|
|
965
|
+
try {
|
|
966
|
+
this.isRecycling = true;
|
|
967
|
+
this.cleanupOldChild();
|
|
968
|
+
const newChild = await this.spawnFn(this.cmd, this.args);
|
|
969
|
+
this.currentChild = newChild;
|
|
970
|
+
this.bindChild(newChild);
|
|
971
|
+
await this.performResync(session, newChild);
|
|
972
|
+
session.needsRecycle = false;
|
|
973
|
+
this.hangDetector.clearHung(session.sessionId);
|
|
974
|
+
this.sanitizer.reset();
|
|
975
|
+
this.isRecycling = false;
|
|
976
|
+
}
|
|
977
|
+
catch (err) {
|
|
978
|
+
this.handleRecycleFailure(err);
|
|
979
|
+
throw err;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async writeToChild(msg) {
|
|
983
|
+
if (this.state.isClosed || !this.currentChild.stdin?.writable) {
|
|
984
|
+
throw new Error("Antigravity ACP stream is closed");
|
|
985
|
+
}
|
|
986
|
+
if (!this.currentChild.stdin.write(JSON.stringify(msg) + "\n")) {
|
|
987
|
+
await once(this.currentChild.stdin, "drain");
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async write(msg, proxies) {
|
|
991
|
+
if (this.state.isClosed) {
|
|
992
|
+
throw new Error("Antigravity ACP stream is closed");
|
|
993
|
+
}
|
|
994
|
+
const targetSessionId = extractSessionId(msg);
|
|
995
|
+
const session = targetSessionId ? this.sessionCache.sessions.get(targetSessionId) : undefined;
|
|
996
|
+
if (session?.needsRecycle) {
|
|
997
|
+
if (!this.activeRecycle) {
|
|
998
|
+
this.activeRecycle = this.recycleProcess(session);
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
await this.activeRecycle;
|
|
1002
|
+
}
|
|
1003
|
+
finally {
|
|
1004
|
+
this.activeRecycle = null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const processed = await processOutboundMessage(msg, proxies, this.promptState, this.sessionCache);
|
|
1008
|
+
this.hangDetector.processOutbound(processed);
|
|
1009
|
+
traceAcp("paseo->agy", processed);
|
|
1010
|
+
await this.writeToChild(processed);
|
|
1011
|
+
}
|
|
1012
|
+
close() {
|
|
1013
|
+
if (this.state.isClosed)
|
|
1014
|
+
return;
|
|
1015
|
+
this.state.isClosed = true;
|
|
1016
|
+
this.hangDetector.dispose();
|
|
1017
|
+
this.rlErr?.close();
|
|
1018
|
+
this.rlOut?.close();
|
|
1019
|
+
this.currentChild.stdin?.end();
|
|
1020
|
+
if (!this.currentChild.killed) {
|
|
1021
|
+
this.currentChild.kill("SIGTERM");
|
|
1022
|
+
}
|
|
1023
|
+
try {
|
|
1024
|
+
this.readableController?.close();
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
// Controller already closed
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
abort() {
|
|
1031
|
+
if (this.state.isClosed)
|
|
1032
|
+
return;
|
|
1033
|
+
this.state.isClosed = true;
|
|
1034
|
+
this.hangDetector.dispose();
|
|
1035
|
+
this.rlErr?.close();
|
|
1036
|
+
this.rlOut?.close();
|
|
1037
|
+
this.currentChild.stdin?.end();
|
|
1038
|
+
if (!this.currentChild.killed) {
|
|
1039
|
+
this.currentChild.kill("SIGKILL");
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
this.readableController?.close();
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
// Controller already closed
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
bindChild(child) {
|
|
1049
|
+
if (child.stdout) {
|
|
1050
|
+
this.rlOut = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
1051
|
+
this.rlOut.on("line", (line) => this.handleStdoutLine(line));
|
|
1052
|
+
}
|
|
1053
|
+
if (child.stderr) {
|
|
1054
|
+
child.stderr.on("data", (chunk) => {
|
|
1055
|
+
process.stderr.write(chunk);
|
|
1056
|
+
});
|
|
1057
|
+
this.rlErr = readline.createInterface({ input: child.stderr, crlfDelay: Infinity });
|
|
1058
|
+
this.rlErr.on("line", (line) => {
|
|
1059
|
+
this.hangDetector.processStderrLine(line);
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
child.on("close", () => this.handleChildClose());
|
|
1063
|
+
child.on("error", (err) => this.handleChildClose(err));
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
export function createAcpReadableStream(slot) {
|
|
1067
|
+
return new ReadableStream({
|
|
1068
|
+
start(controller) {
|
|
1069
|
+
slot.setReadableController(controller);
|
|
1070
|
+
},
|
|
1071
|
+
cancel() {
|
|
1072
|
+
slot.close();
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
export function createAcpWritableStream(slot, proxies) {
|
|
1077
|
+
return new WritableStream({
|
|
1078
|
+
async write(msg) {
|
|
1079
|
+
await slot.write(msg, proxies);
|
|
1080
|
+
},
|
|
1081
|
+
close() {
|
|
1082
|
+
slot.close();
|
|
1083
|
+
},
|
|
1084
|
+
abort() {
|
|
1085
|
+
slot.abort();
|
|
1086
|
+
},
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
async function resolveBinaryCommand() {
|
|
1090
|
+
const [initialCmd, ...args] = agyCommand();
|
|
1091
|
+
let cmd = initialCmd;
|
|
1092
|
+
if (!fs.existsSync(cmd)) {
|
|
1093
|
+
try {
|
|
1094
|
+
cmd = await ensureAntigravityBinary();
|
|
1095
|
+
}
|
|
1096
|
+
catch (err) {
|
|
1097
|
+
console.error("[paseo-antigravity] Failed to auto-download binary:", err);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (fs.existsSync(cmd)) {
|
|
1101
|
+
try {
|
|
1102
|
+
cmd = ensurePatchedBinary(cmd);
|
|
1103
|
+
}
|
|
1104
|
+
catch (err) {
|
|
1105
|
+
console.error("[paseo-antigravity] Failed to patch binary:", err);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return [cmd, ...args];
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Creates an in-process ACP connector stream that launches the official `agy_acp_server` binary,
|
|
1112
|
+
* automatically downloading it if not present, and applies mode option filtering, outbound MCP adaptation,
|
|
1113
|
+
* text sanitization, hang detection, and transparent process recycling.
|
|
1114
|
+
*
|
|
1115
|
+
* MCP proxies come from a process-wide pool rather than being owned by this stream.
|
|
1116
|
+
* Antigravity persists the rewritten MCP URL into its per-session database, so a proxy
|
|
1117
|
+
* torn down with the stream would leave resumed sessions pointing at a dead port.
|
|
1118
|
+
*/
|
|
1119
|
+
export function createAntigravityConnector(proxies = sharedMcpProxyPool, options) {
|
|
1120
|
+
return async () => {
|
|
1121
|
+
const [cmd, ...args] = await resolveBinaryCommand();
|
|
1122
|
+
ensureAuthenticated();
|
|
1123
|
+
const spawnFn = options?.spawnProcess ?? defaultSpawnProcess;
|
|
1124
|
+
const initialChild = await spawnFn(cmd, args);
|
|
1125
|
+
const slot = new ChildSlot({
|
|
1126
|
+
cmd,
|
|
1127
|
+
args,
|
|
1128
|
+
initialChild,
|
|
1129
|
+
proxies,
|
|
1130
|
+
options,
|
|
1131
|
+
});
|
|
1132
|
+
return {
|
|
1133
|
+
readable: createAcpReadableStream(slot),
|
|
1134
|
+
writable: createAcpWritableStream(slot, proxies),
|
|
1135
|
+
};
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
//# sourceMappingURL=connector.js.map
|