quadwork 1.11.1 → 1.12.1
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/README.md +7 -0
- package/bin/quadwork.js +3 -1
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +1 -1
- package/out/__next._full.txt +2 -2
- package/out/__next._head.txt +1 -1
- package/out/__next._index.txt +2 -2
- package/out/__next._tree.txt +2 -2
- package/out/_next/static/chunks/0nk3kw~5j75~v.css +2 -0
- package/out/_next/static/chunks/0wuucfn72wx0t.js +1 -0
- package/out/_next/static/chunks/{0a5314ra5t9bs.js → 11h7y0f5o9.hx.js} +1 -1
- package/out/_next/static/chunks/{0ge87xt6a9j~..js → 13w.n.3zipzvz.js} +8 -8
- package/out/_not-found/__next._full.txt +2 -2
- package/out/_not-found/__next._head.txt +1 -1
- package/out/_not-found/__next._index.txt +2 -2
- package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/out/_not-found/__next._not-found.txt +1 -1
- package/out/_not-found/__next._tree.txt +2 -2
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +2 -2
- package/out/app-shell/__next._full.txt +2 -2
- package/out/app-shell/__next._head.txt +1 -1
- package/out/app-shell/__next._index.txt +2 -2
- package/out/app-shell/__next._tree.txt +2 -2
- package/out/app-shell/__next.app-shell.__PAGE__.txt +1 -1
- package/out/app-shell/__next.app-shell.txt +1 -1
- package/out/app-shell.html +1 -1
- package/out/app-shell.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/project/_/__next._full.txt +3 -3
- package/out/project/_/__next._head.txt +1 -1
- package/out/project/_/__next._index.txt +2 -2
- package/out/project/_/__next._tree.txt +2 -2
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +2 -2
- package/out/project/_/__next.project.$d$id.txt +1 -1
- package/out/project/_/__next.project.txt +1 -1
- package/out/project/_/queue/__next._full.txt +2 -2
- package/out/project/_/queue/__next._head.txt +1 -1
- package/out/project/_/queue/__next._index.txt +2 -2
- package/out/project/_/queue/__next._tree.txt +2 -2
- package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.txt +1 -1
- package/out/project/_/queue/__next.project.txt +1 -1
- package/out/project/_/queue.html +1 -1
- package/out/project/_/queue.txt +2 -2
- package/out/project/_.html +1 -1
- package/out/project/_.txt +3 -3
- package/out/settings/__next._full.txt +2 -2
- package/out/settings/__next._head.txt +1 -1
- package/out/settings/__next._index.txt +2 -2
- package/out/settings/__next._tree.txt +2 -2
- package/out/settings/__next.settings.__PAGE__.txt +1 -1
- package/out/settings/__next.settings.txt +1 -1
- package/out/settings.html +1 -1
- package/out/settings.txt +2 -2
- package/out/setup/__next._full.txt +3 -3
- package/out/setup/__next._head.txt +1 -1
- package/out/setup/__next._index.txt +2 -2
- package/out/setup/__next._tree.txt +2 -2
- package/out/setup/__next.setup.__PAGE__.txt +2 -2
- package/out/setup/__next.setup.txt +1 -1
- package/out/setup.html +1 -1
- package/out/setup.txt +3 -3
- package/package.json +7 -7
- package/server/__tests__/rate-limit-handling.test.js +168 -0
- package/server/agentchattr-registry.js +17 -0
- package/server/config.js +3 -1
- package/server/index.js +64 -23
- package/server/routes.js +192 -74
- package/out/_next/static/chunks/09h0i4gh79na..js +0 -1
- package/out/_next/static/chunks/0a4.d381szseh.css +0 -2
- /package/out/_next/static/{QmshV04af9o06krSyFHwf → FC6TKxnxD2CZcQuW_um5N}/_buildManifest.js +0 -0
- /package/out/_next/static/{QmshV04af9o06krSyFHwf → FC6TKxnxD2CZcQuW_um5N}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{QmshV04af9o06krSyFHwf → FC6TKxnxD2CZcQuW_um5N}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #554 — Verify rate-limit-aware caching and backoff in server/routes.js.
|
|
3
|
+
*
|
|
4
|
+
* Tests verify:
|
|
5
|
+
* 1. Rate limit state variables exist and are initialised
|
|
6
|
+
* 2. adaptiveTTL returns extended TTLs when rate is low/critical
|
|
7
|
+
* 3. cachedGhEndpoint serves stale data with _rateLimited flag
|
|
8
|
+
* 4. GitHub endpoints use the cached helper
|
|
9
|
+
* 5. /api/github/rate-limit endpoint is registered
|
|
10
|
+
* 6. Batch progress handler has rate-limit guard
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
|
|
16
|
+
const ROUTES_PATH = path.join(__dirname, "..", "routes.js");
|
|
17
|
+
const src = fs.readFileSync(ROUTES_PATH, "utf-8");
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// 1. Rate limit state and constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
describe("#554 rate limit infrastructure (code analysis)", () => {
|
|
24
|
+
test("_rateLimit state object is defined with expected fields", () => {
|
|
25
|
+
expect(src).toContain("const _rateLimit = {");
|
|
26
|
+
expect(src).toContain("remaining:");
|
|
27
|
+
expect(src).toContain("resetAt:");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("RATE_LIMIT_LOW_THRESHOLD and RATE_LIMIT_CRITICAL are defined", () => {
|
|
31
|
+
expect(src).toMatch(/RATE_LIMIT_LOW_THRESHOLD\s*=\s*\d+/);
|
|
32
|
+
expect(src).toMatch(/RATE_LIMIT_CRITICAL\s*=\s*\d+/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("refreshRateLimit calls gh api rate_limit", () => {
|
|
36
|
+
expect(src).toContain("gh");
|
|
37
|
+
expect(src).toContain("api");
|
|
38
|
+
expect(src).toContain("rate_limit");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("startRateLimitPolling is called at module load", () => {
|
|
42
|
+
expect(src).toContain("startRateLimitPolling()");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// 2. Adaptive TTL
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
describe("#554 adaptive TTL logic", () => {
|
|
51
|
+
test("adaptiveTTL function is defined", () => {
|
|
52
|
+
expect(src).toContain("function adaptiveTTL(baseTTL)");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("adaptiveTTL returns Infinity when critically rate-limited", () => {
|
|
56
|
+
expect(src).toMatch(/isRateLimited\(\).*Infinity/s);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("adaptiveTTL extends TTL when rate is low", () => {
|
|
60
|
+
expect(src).toMatch(/isRateLow\(\).*120[_,]?000/s);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// 3. Cached endpoint helper
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
describe("#554 cachedGhEndpoint helper", () => {
|
|
69
|
+
test("cachedGhEndpoint function is defined", () => {
|
|
70
|
+
expect(src).toContain("function cachedGhEndpoint(");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("serves stale data with _rateLimited flag when critical", () => {
|
|
74
|
+
const fnStart = src.indexOf("function cachedGhEndpoint(");
|
|
75
|
+
const fnBody = src.slice(fnStart, fnStart + 800);
|
|
76
|
+
expect(fnBody).toContain("_rateLimited");
|
|
77
|
+
expect(fnBody).toContain("_stale");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("uses adaptiveTTL for cache checks", () => {
|
|
81
|
+
const fnStart = src.indexOf("function cachedGhEndpoint(");
|
|
82
|
+
const fnBody = src.slice(fnStart, fnStart + 600);
|
|
83
|
+
expect(fnBody).toContain("adaptiveTTL");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// 4. GitHub endpoints use cached helper
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
describe("#554 GitHub endpoints use cachedGhEndpoint", () => {
|
|
92
|
+
test("/api/github/issues uses cachedGhEndpoint", () => {
|
|
93
|
+
const section = src.slice(
|
|
94
|
+
src.indexOf('"/api/github/issues"'),
|
|
95
|
+
src.indexOf('"/api/github/issues"') + 300,
|
|
96
|
+
);
|
|
97
|
+
expect(section).toContain("cachedGhEndpoint");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("/api/github/prs uses cachedGhEndpoint", () => {
|
|
101
|
+
const section = src.slice(
|
|
102
|
+
src.indexOf('"/api/github/prs"'),
|
|
103
|
+
src.indexOf('"/api/github/prs"') + 300,
|
|
104
|
+
);
|
|
105
|
+
expect(section).toContain("cachedGhEndpoint");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("/api/github/closed-issues uses cachedGhEndpoint", () => {
|
|
109
|
+
const section = src.slice(
|
|
110
|
+
src.indexOf('"/api/github/closed-issues"'),
|
|
111
|
+
src.indexOf('"/api/github/closed-issues"') + 500,
|
|
112
|
+
);
|
|
113
|
+
expect(section).toContain("cachedGhEndpoint");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("/api/github/merged-prs uses cachedGhEndpoint", () => {
|
|
117
|
+
const section = src.slice(
|
|
118
|
+
src.indexOf('"/api/github/merged-prs"'),
|
|
119
|
+
src.indexOf('"/api/github/merged-prs"') + 500,
|
|
120
|
+
);
|
|
121
|
+
expect(section).toContain("cachedGhEndpoint");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// 5. Rate limit API endpoint
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
describe("#554 /api/github/rate-limit endpoint", () => {
|
|
130
|
+
test("endpoint is registered", () => {
|
|
131
|
+
expect(src).toContain('"/api/github/rate-limit"');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("returns remaining, limit, resetInMinutes, low, critical fields", () => {
|
|
135
|
+
const idx = src.indexOf('"/api/github/rate-limit"');
|
|
136
|
+
const section = src.slice(idx, idx + 500);
|
|
137
|
+
expect(section).toContain("remaining");
|
|
138
|
+
expect(section).toContain("limit");
|
|
139
|
+
expect(section).toContain("resetInMinutes");
|
|
140
|
+
expect(section).toContain("low");
|
|
141
|
+
expect(section).toContain("critical");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// 6. Batch progress rate limit guard
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
describe("#554 batch progress rate limit awareness", () => {
|
|
150
|
+
test("batch progress handler checks isRateLimited before gh calls", () => {
|
|
151
|
+
const batchStart = src.indexOf('"/api/batch-progress"');
|
|
152
|
+
const batchSection = src.slice(batchStart, batchStart + 600);
|
|
153
|
+
expect(batchSection).toContain("isRateLimited()");
|
|
154
|
+
expect(batchSection).toContain("_rateLimited");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("batch progress uses adaptiveTTL for cache", () => {
|
|
158
|
+
const batchStart = src.indexOf('"/api/batch-progress"');
|
|
159
|
+
const batchSection = src.slice(batchStart, batchStart + 400);
|
|
160
|
+
expect(batchSection).toContain("adaptiveTTL");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("projects endpoint uses adaptiveTTL for cache", () => {
|
|
164
|
+
const projStart = src.indexOf('"/api/projects"');
|
|
165
|
+
const projSection = src.slice(projStart, projStart + 400);
|
|
166
|
+
expect(projSection).toContain("adaptiveTTL");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -162,9 +162,26 @@ function stopHeartbeat(handle) {
|
|
|
162
162
|
if (handle) clearInterval(handle);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Register an agent with retries and exponential backoff.
|
|
167
|
+
* Returns {name, token, slot} on success, null after all attempts fail.
|
|
168
|
+
* Uses registerAgent internally so registerAgent.lastError is populated.
|
|
169
|
+
*/
|
|
170
|
+
async function registerAgentWithRetry(serverPort, base, label = null, { force = false, attempts = 3, delayMs = 2000 } = {}) {
|
|
171
|
+
for (let i = 0; i < attempts; i++) {
|
|
172
|
+
const result = await registerAgent(serverPort, base, label, { force });
|
|
173
|
+
if (result) return result;
|
|
174
|
+
if (i < attempts - 1) {
|
|
175
|
+
await new Promise((res) => setTimeout(res, delayMs * Math.pow(2, i)));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
165
181
|
module.exports = {
|
|
166
182
|
waitForAgentChattrReady,
|
|
167
183
|
registerAgent,
|
|
184
|
+
registerAgentWithRetry,
|
|
168
185
|
deregisterAgent,
|
|
169
186
|
startHeartbeat,
|
|
170
187
|
stopHeartbeat,
|
package/server/config.js
CHANGED
|
@@ -55,7 +55,9 @@ function sanitizeOperatorName(value) {
|
|
|
55
55
|
return truncated;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// Migration: rename old agent keys to new ones
|
|
58
|
+
// Migration: rename old agent keys to new ones.
|
|
59
|
+
// Keep this map — it migrates pre-v1.8 configs on startup so existing
|
|
60
|
+
// installs transition to the canonical head/dev/re1/re2 slugs.
|
|
59
61
|
const AGENT_KEY_MAP = { t1: "head", t2a: "re1", t2b: "re2", t3: "dev", reviewer1: "re1", reviewer2: "re2" };
|
|
60
62
|
|
|
61
63
|
function migrateAgentKeys(config) {
|
package/server/index.js
CHANGED
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
patchAgentchattrConfigForTelegramBridge,
|
|
14
14
|
projectAgentchattrConfigPath,
|
|
15
15
|
} = routes;
|
|
16
|
-
const { waitForAgentChattrReady, registerAgent, deregisterAgent, startHeartbeat, stopHeartbeat } = require("./agentchattr-registry");
|
|
16
|
+
const { waitForAgentChattrReady, registerAgent, registerAgentWithRetry, deregisterAgent, startHeartbeat, stopHeartbeat } = require("./agentchattr-registry");
|
|
17
17
|
const { patchAgentchattrCss } = require("./install-agentchattr");
|
|
18
18
|
const { startQueueWatcher, stopQueueWatcher } = require("./queue-watcher");
|
|
19
19
|
|
|
@@ -356,7 +356,15 @@ async function buildAgentArgs(projectId, agentId) {
|
|
|
356
356
|
// write that into the per-agent MCP config file.
|
|
357
357
|
const chattrInfo = resolveProjectChattr(projectId);
|
|
358
358
|
acServerPort = Number(new URL(chattrInfo.url).port) || 8300;
|
|
359
|
-
|
|
359
|
+
// #565: extend timeout to 30s — first setup may need AC to install
|
|
360
|
+
// (git clone + venv + pip install) before it can bind a port.
|
|
361
|
+
const acReady = await waitForAgentChattrReady(acServerPort, 30000);
|
|
362
|
+
if (!acReady) {
|
|
363
|
+
console.warn(`[#565] Agent ${agentId}: AC not reachable on port ${acServerPort} after 30s. Spawning without chat integration.`);
|
|
364
|
+
// #565: preserve acServerPort and acInjectMode so deferred
|
|
365
|
+
// recovery in spawnAgentPty can retry registration later.
|
|
366
|
+
return { args, acRegistrationName: null, acServerPort, acRegistrationToken: null, acInjectMode: injectMode, acMcpHttpPort: mcpHttpPort || null };
|
|
367
|
+
}
|
|
360
368
|
// #242: best-effort deregister any stale registration of the
|
|
361
369
|
// canonical name (left over by a crashed previous QuadWork
|
|
362
370
|
// session) so the fresh register lands at slot 1 instead of
|
|
@@ -370,16 +378,18 @@ async function buildAgentArgs(projectId, agentId) {
|
|
|
370
378
|
clearPersistedAgentToken(projectId, agentId);
|
|
371
379
|
}
|
|
372
380
|
// #478: force-replace so AC expires any ghost slots for this base
|
|
373
|
-
|
|
381
|
+
// #565: retry with backoff and degrade gracefully if AC is not ready
|
|
382
|
+
const registration = await registerAgentWithRetry(acServerPort, agentId, agentCfg.display_name || null, { force: true });
|
|
374
383
|
if (!registration) {
|
|
375
|
-
|
|
384
|
+
console.warn(`[#565] Agent ${agentId}: AC registration failed after retries (${registerAgent.lastError}). Spawning without chat integration.`);
|
|
385
|
+
} else {
|
|
386
|
+
acRegistrationName = registration.name;
|
|
387
|
+
acRegistrationToken = registration.token;
|
|
388
|
+
writePersistedAgentToken(projectId, agentId, registration.token);
|
|
389
|
+
const mcpConfigPath = writeMcpConfigFile(projectId, agentId, mcpHttpPort, registration.token);
|
|
390
|
+
const flag = agentCfg.mcp_flag || "--mcp-config";
|
|
391
|
+
args.push(flag, mcpConfigPath);
|
|
376
392
|
}
|
|
377
|
-
acRegistrationName = registration.name;
|
|
378
|
-
acRegistrationToken = registration.token;
|
|
379
|
-
writePersistedAgentToken(projectId, agentId, registration.token);
|
|
380
|
-
const mcpConfigPath = writeMcpConfigFile(projectId, agentId, mcpHttpPort, registration.token);
|
|
381
|
-
const flag = agentCfg.mcp_flag || "--mcp-config";
|
|
382
|
-
args.push(flag, mcpConfigPath);
|
|
383
393
|
} else if (injectMode === "proxy_flag") {
|
|
384
394
|
// Codex: register with AgentChattr first (#240) so the proxy
|
|
385
395
|
// injects a real per-agent token, not the global session token.
|
|
@@ -387,7 +397,14 @@ async function buildAgentArgs(projectId, agentId) {
|
|
|
387
397
|
// projects without a per-project agentchattr_url still work.
|
|
388
398
|
const chattrInfo = resolveProjectChattr(projectId);
|
|
389
399
|
acServerPort = Number(new URL(chattrInfo.url).port) || 8300;
|
|
390
|
-
|
|
400
|
+
// #565: extend timeout to 30s for first-setup scenario
|
|
401
|
+
const acReady = await waitForAgentChattrReady(acServerPort, 30000);
|
|
402
|
+
if (!acReady) {
|
|
403
|
+
console.warn(`[#565] Agent ${agentId}: AC not reachable on port ${acServerPort} after 30s. Spawning without chat integration.`);
|
|
404
|
+
// #565: preserve acServerPort and acInjectMode so deferred
|
|
405
|
+
// recovery in spawnAgentPty can retry registration later.
|
|
406
|
+
return { args, acRegistrationName: null, acServerPort, acRegistrationToken: null, acInjectMode: injectMode, acMcpHttpPort: mcpHttpPort || null };
|
|
407
|
+
}
|
|
391
408
|
// #242: best-effort deregister stale canonical name first using
|
|
392
409
|
// the persisted bearer token from a previous session.
|
|
393
410
|
const stalePersistedToken = readPersistedAgentToken(projectId, agentId);
|
|
@@ -396,17 +413,19 @@ async function buildAgentArgs(projectId, agentId) {
|
|
|
396
413
|
clearPersistedAgentToken(projectId, agentId);
|
|
397
414
|
}
|
|
398
415
|
// #478: force-replace so AC expires any ghost slots for this base
|
|
399
|
-
|
|
416
|
+
// #565: retry with backoff and degrade gracefully if AC is not ready
|
|
417
|
+
const registration = await registerAgentWithRetry(acServerPort, agentId, agentCfg.display_name || null, { force: true });
|
|
400
418
|
if (!registration) {
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
419
|
+
console.warn(`[#565] Agent ${agentId}: AC registration failed after retries (${registerAgent.lastError}). Spawning without chat integration.`);
|
|
420
|
+
} else {
|
|
421
|
+
acRegistrationName = registration.name;
|
|
422
|
+
acRegistrationToken = registration.token;
|
|
423
|
+
writePersistedAgentToken(projectId, agentId, registration.token);
|
|
424
|
+
const upstreamUrl = `http://127.0.0.1:${mcpHttpPort}`;
|
|
425
|
+
const proxyUrl = await startMcpProxy(projectId, agentId, upstreamUrl, registration.token);
|
|
426
|
+
if (proxyUrl) {
|
|
427
|
+
args.push("-c", `mcp_servers.agentchattr.url="${proxyUrl}"`);
|
|
428
|
+
}
|
|
410
429
|
}
|
|
411
430
|
}
|
|
412
431
|
}
|
|
@@ -526,11 +545,14 @@ async function spawnAgentPty(project, agent) {
|
|
|
526
545
|
if (!cwd) return { ok: false, error: `Unknown agent: ${key}` };
|
|
527
546
|
|
|
528
547
|
const command = resolveAgentCommand(project, agent) || (process.env.SHELL || "/bin/zsh");
|
|
529
|
-
const built = await buildAgentArgs(project, agent);
|
|
530
|
-
const args = built.args;
|
|
531
548
|
const extraEnv = buildAgentEnv(project, agent);
|
|
532
549
|
|
|
533
550
|
try {
|
|
551
|
+
// #565: buildAgentArgs is inside try-catch so registration failures
|
|
552
|
+
// cannot crash the server as an unhandled rejection.
|
|
553
|
+
const built = await buildAgentArgs(project, agent);
|
|
554
|
+
const args = built.args;
|
|
555
|
+
|
|
534
556
|
const term = pty.spawn(command, args, {
|
|
535
557
|
name: "xterm-256color",
|
|
536
558
|
cols: 120,
|
|
@@ -611,6 +633,25 @@ async function spawnAgentPty(project, agent) {
|
|
|
611
633
|
}
|
|
612
634
|
}
|
|
613
635
|
|
|
636
|
+
// #565: deferred restart — if the agent spawned without AC
|
|
637
|
+
// registration (AC wasn't ready or registration failed), wait for
|
|
638
|
+
// AC to come up then stop + respawn the agent so it gets the full
|
|
639
|
+
// MCP CLI args (--mcp-config / -c mcp_servers...url) that can only
|
|
640
|
+
// be set at process launch time.
|
|
641
|
+
if (!session.acRegistrationName && session.acServerPort && session.acInjectMode) {
|
|
642
|
+
const deferredRestart = async () => {
|
|
643
|
+
const ready = await waitForAgentChattrReady(session.acServerPort, 60000);
|
|
644
|
+
if (!ready) return;
|
|
645
|
+
// Guard: agent may have been stopped manually while we waited.
|
|
646
|
+
const current = agentSessions.get(key);
|
|
647
|
+
if (!current || !current.term || current.state !== "running") return;
|
|
648
|
+
console.log(`[#565] Agent ${agent}: AC is now reachable — restarting agent to gain chat integration.`);
|
|
649
|
+
await stopAgentSession(key);
|
|
650
|
+
await spawnAgentPty(project, agent);
|
|
651
|
+
};
|
|
652
|
+
deferredRestart().catch(() => {});
|
|
653
|
+
}
|
|
654
|
+
|
|
614
655
|
term.onExit(({ exitCode }) => {
|
|
615
656
|
const current = agentSessions.get(key);
|
|
616
657
|
if (current && current.term === term) {
|