@rallycry/conveyor-agent 2.7.0 → 2.8.0
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/chunk-LPOTXSKX.js +1408 -0
- package/dist/chunk-LPOTXSKX.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +6 -33
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-JWIGEYRD.js +0 -1397
- package/dist/chunk-JWIGEYRD.js.map +0 -1
package/dist/chunk-JWIGEYRD.js
DELETED
|
@@ -1,1397 +0,0 @@
|
|
|
1
|
-
// src/connection.ts
|
|
2
|
-
import { io } from "socket.io-client";
|
|
3
|
-
var ConveyorConnection = class _ConveyorConnection {
|
|
4
|
-
socket = null;
|
|
5
|
-
config;
|
|
6
|
-
eventBuffer = [];
|
|
7
|
-
flushTimer = null;
|
|
8
|
-
static EVENT_BATCH_MS = 500;
|
|
9
|
-
earlyMessages = [];
|
|
10
|
-
earlyStop = false;
|
|
11
|
-
chatMessageCallback = null;
|
|
12
|
-
stopCallback = null;
|
|
13
|
-
pendingQuestionResolvers = /* @__PURE__ */ new Map();
|
|
14
|
-
constructor(config) {
|
|
15
|
-
this.config = config;
|
|
16
|
-
}
|
|
17
|
-
connect() {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
let settled = false;
|
|
20
|
-
let attempts = 0;
|
|
21
|
-
const maxInitialAttempts = 30;
|
|
22
|
-
this.socket = io(this.config.conveyorApiUrl, {
|
|
23
|
-
auth: { taskToken: this.config.taskToken, runnerMode: this.config.mode ?? "task" },
|
|
24
|
-
transports: ["websocket"],
|
|
25
|
-
reconnection: true,
|
|
26
|
-
reconnectionAttempts: Infinity,
|
|
27
|
-
reconnectionDelay: 2e3,
|
|
28
|
-
reconnectionDelayMax: 3e4,
|
|
29
|
-
randomizationFactor: 0.3,
|
|
30
|
-
extraHeaders: {
|
|
31
|
-
"ngrok-skip-browser-warning": "true"
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
this.socket.on("agentRunner:incomingMessage", (msg) => {
|
|
35
|
-
if (this.chatMessageCallback) {
|
|
36
|
-
this.chatMessageCallback(msg);
|
|
37
|
-
} else {
|
|
38
|
-
this.earlyMessages.push(msg);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
this.socket.on("agentRunner:stop", () => {
|
|
42
|
-
if (this.stopCallback) {
|
|
43
|
-
this.stopCallback();
|
|
44
|
-
} else {
|
|
45
|
-
this.earlyStop = true;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
this.socket.on(
|
|
49
|
-
"agentRunner:questionAnswer",
|
|
50
|
-
(data) => {
|
|
51
|
-
const resolver = this.pendingQuestionResolvers.get(data.requestId);
|
|
52
|
-
if (resolver) {
|
|
53
|
-
this.pendingQuestionResolvers.delete(data.requestId);
|
|
54
|
-
resolver(data.answers);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
);
|
|
58
|
-
this.socket.on("connect", () => {
|
|
59
|
-
if (!settled) {
|
|
60
|
-
settled = true;
|
|
61
|
-
resolve();
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
this.socket.io.on("reconnect_attempt", () => {
|
|
65
|
-
attempts++;
|
|
66
|
-
if (!settled && attempts >= maxInitialAttempts) {
|
|
67
|
-
settled = true;
|
|
68
|
-
reject(new Error(`Failed to connect after ${maxInitialAttempts} attempts`));
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
fetchChatMessages(limit) {
|
|
74
|
-
const socket = this.socket;
|
|
75
|
-
if (!socket) throw new Error("Not connected");
|
|
76
|
-
return new Promise((resolve, reject) => {
|
|
77
|
-
socket.emit(
|
|
78
|
-
"agentRunner:getChatMessages",
|
|
79
|
-
{ taskId: this.config.taskId, limit },
|
|
80
|
-
(response) => {
|
|
81
|
-
if (response.success && response.data) {
|
|
82
|
-
resolve(response.data);
|
|
83
|
-
} else {
|
|
84
|
-
reject(new Error(response.error ?? "Failed to fetch chat messages"));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
fetchTaskContext() {
|
|
91
|
-
const socket = this.socket;
|
|
92
|
-
if (!socket) throw new Error("Not connected");
|
|
93
|
-
return new Promise((resolve, reject) => {
|
|
94
|
-
socket.emit(
|
|
95
|
-
"agentRunner:getTaskContext",
|
|
96
|
-
{ taskId: this.config.taskId },
|
|
97
|
-
(response) => {
|
|
98
|
-
if (response.success && response.data) {
|
|
99
|
-
resolve(response.data);
|
|
100
|
-
} else {
|
|
101
|
-
reject(new Error(response.error ?? "Failed to fetch task context"));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
sendEvent(event) {
|
|
108
|
-
if (!this.socket) throw new Error("Not connected");
|
|
109
|
-
this.eventBuffer.push({ taskId: this.config.taskId, event });
|
|
110
|
-
if (!this.flushTimer) {
|
|
111
|
-
this.flushTimer = setTimeout(() => this.flushEvents(), _ConveyorConnection.EVENT_BATCH_MS);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
flushEvents() {
|
|
115
|
-
if (this.flushTimer) {
|
|
116
|
-
clearTimeout(this.flushTimer);
|
|
117
|
-
this.flushTimer = null;
|
|
118
|
-
}
|
|
119
|
-
if (!this.socket || this.eventBuffer.length === 0) return;
|
|
120
|
-
for (const entry of this.eventBuffer) {
|
|
121
|
-
this.socket.emit("agentRunner:event", entry);
|
|
122
|
-
}
|
|
123
|
-
this.eventBuffer = [];
|
|
124
|
-
}
|
|
125
|
-
updateStatus(status) {
|
|
126
|
-
if (!this.socket) throw new Error("Not connected");
|
|
127
|
-
this.socket.emit("agentRunner:statusUpdate", {
|
|
128
|
-
taskId: this.config.taskId,
|
|
129
|
-
status
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
postChatMessage(content) {
|
|
133
|
-
if (!this.socket) throw new Error("Not connected");
|
|
134
|
-
this.socket.emit("agentRunner:chatMessage", {
|
|
135
|
-
taskId: this.config.taskId,
|
|
136
|
-
content
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
createPR(params) {
|
|
140
|
-
const socket = this.socket;
|
|
141
|
-
if (!socket) throw new Error("Not connected");
|
|
142
|
-
return new Promise((resolve, reject) => {
|
|
143
|
-
socket.emit(
|
|
144
|
-
"agentRunner:createPR",
|
|
145
|
-
{ taskId: this.config.taskId, ...params },
|
|
146
|
-
(response) => {
|
|
147
|
-
if (response.success && response.data) {
|
|
148
|
-
resolve(response.data);
|
|
149
|
-
} else {
|
|
150
|
-
reject(new Error(response.error ?? "Failed to create pull request"));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
askUserQuestion(requestId, questions) {
|
|
157
|
-
if (!this.socket) throw new Error("Not connected");
|
|
158
|
-
this.socket.emit("agentRunner:askUserQuestion", {
|
|
159
|
-
taskId: this.config.taskId,
|
|
160
|
-
requestId,
|
|
161
|
-
questions
|
|
162
|
-
});
|
|
163
|
-
return new Promise((resolve) => {
|
|
164
|
-
this.pendingQuestionResolvers.set(requestId, resolve);
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
cancelPendingQuestions() {
|
|
168
|
-
this.pendingQuestionResolvers.clear();
|
|
169
|
-
}
|
|
170
|
-
storeSessionId(sessionId) {
|
|
171
|
-
if (!this.socket) return;
|
|
172
|
-
this.socket.emit("agentRunner:storeSessionId", {
|
|
173
|
-
taskId: this.config.taskId,
|
|
174
|
-
sessionId
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
updateTaskFields(fields) {
|
|
178
|
-
if (!this.socket) throw new Error("Not connected");
|
|
179
|
-
this.socket.emit("agentRunner:updateTaskFields", {
|
|
180
|
-
taskId: this.config.taskId,
|
|
181
|
-
fields
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
onChatMessage(callback) {
|
|
185
|
-
this.chatMessageCallback = callback;
|
|
186
|
-
for (const msg of this.earlyMessages) {
|
|
187
|
-
callback(msg);
|
|
188
|
-
}
|
|
189
|
-
this.earlyMessages = [];
|
|
190
|
-
}
|
|
191
|
-
onStopRequested(callback) {
|
|
192
|
-
this.stopCallback = callback;
|
|
193
|
-
if (this.earlyStop) {
|
|
194
|
-
callback();
|
|
195
|
-
this.earlyStop = false;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
trackSpending(params) {
|
|
199
|
-
if (!this.socket) throw new Error("Not connected");
|
|
200
|
-
this.socket.emit("agentRunner:trackSpending", {
|
|
201
|
-
taskId: this.config.taskId,
|
|
202
|
-
...params
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
emitStatus(status) {
|
|
206
|
-
if (!this.socket) return;
|
|
207
|
-
this.socket.emit("agentRunner:statusUpdate", {
|
|
208
|
-
taskId: this.config.taskId,
|
|
209
|
-
status
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
sendHeartbeat() {
|
|
213
|
-
if (!this.socket) return;
|
|
214
|
-
this.socket.emit("agentRunner:heartbeat", {
|
|
215
|
-
taskId: this.config.taskId
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
sendTypingStart() {
|
|
219
|
-
this.sendEvent({ type: "agent_typing_start" });
|
|
220
|
-
}
|
|
221
|
-
sendTypingStop() {
|
|
222
|
-
this.sendEvent({ type: "agent_typing_stop" });
|
|
223
|
-
}
|
|
224
|
-
disconnect() {
|
|
225
|
-
this.flushEvents();
|
|
226
|
-
this.socket?.disconnect();
|
|
227
|
-
this.socket = null;
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// src/setup.ts
|
|
232
|
-
import { execSync } from "child_process";
|
|
233
|
-
import { spawn } from "child_process";
|
|
234
|
-
import { readFile } from "fs/promises";
|
|
235
|
-
import { join } from "path";
|
|
236
|
-
var CONVEYOR_CONFIG_PATH = ".conveyor/config.json";
|
|
237
|
-
var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
|
|
238
|
-
async function loadForwardPorts(workspaceDir) {
|
|
239
|
-
try {
|
|
240
|
-
const raw = await readFile(join(workspaceDir, DEVCONTAINER_PATH), "utf-8");
|
|
241
|
-
const parsed = JSON.parse(raw);
|
|
242
|
-
return parsed.forwardPorts ?? [];
|
|
243
|
-
} catch {
|
|
244
|
-
return [];
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
async function loadConveyorConfig(workspaceDir) {
|
|
248
|
-
try {
|
|
249
|
-
const raw = await readFile(join(workspaceDir, CONVEYOR_CONFIG_PATH), "utf-8");
|
|
250
|
-
const parsed = JSON.parse(raw);
|
|
251
|
-
if (parsed.setupCommand || parsed.startCommand) return parsed;
|
|
252
|
-
} catch {
|
|
253
|
-
}
|
|
254
|
-
try {
|
|
255
|
-
const raw = await readFile(join(workspaceDir, DEVCONTAINER_PATH), "utf-8");
|
|
256
|
-
const parsed = JSON.parse(raw);
|
|
257
|
-
if (parsed.conveyor && (parsed.conveyor.startCommand || parsed.conveyor.setupCommand)) {
|
|
258
|
-
return parsed.conveyor;
|
|
259
|
-
}
|
|
260
|
-
} catch {
|
|
261
|
-
}
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
function runSetupCommand(cmd, cwd, onOutput) {
|
|
265
|
-
return new Promise((resolve, reject) => {
|
|
266
|
-
const child = spawn("sh", ["-c", cmd], {
|
|
267
|
-
cwd,
|
|
268
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
269
|
-
env: { ...process.env }
|
|
270
|
-
});
|
|
271
|
-
child.stdout.on("data", (chunk) => {
|
|
272
|
-
onOutput("stdout", chunk.toString());
|
|
273
|
-
});
|
|
274
|
-
child.stderr.on("data", (chunk) => {
|
|
275
|
-
onOutput("stderr", chunk.toString());
|
|
276
|
-
});
|
|
277
|
-
child.on("close", (code) => {
|
|
278
|
-
if (code === 0) {
|
|
279
|
-
resolve();
|
|
280
|
-
} else {
|
|
281
|
-
reject(new Error(`Setup command exited with code ${code}`));
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
child.on("error", (err) => {
|
|
285
|
-
reject(err);
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
function runStartCommand(cmd, cwd, onOutput) {
|
|
290
|
-
const child = spawn("sh", ["-c", cmd], {
|
|
291
|
-
cwd,
|
|
292
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
293
|
-
detached: true,
|
|
294
|
-
env: { ...process.env }
|
|
295
|
-
});
|
|
296
|
-
child.stdout.on("data", (chunk) => {
|
|
297
|
-
onOutput("stdout", chunk.toString());
|
|
298
|
-
});
|
|
299
|
-
child.stderr.on("data", (chunk) => {
|
|
300
|
-
onOutput("stderr", chunk.toString());
|
|
301
|
-
});
|
|
302
|
-
child.unref();
|
|
303
|
-
return child;
|
|
304
|
-
}
|
|
305
|
-
function cleanDevcontainerFromGit(workspaceDir, taskBranch, baseBranch) {
|
|
306
|
-
const git = (cmd) => execSync(cmd, { cwd: workspaceDir, encoding: "utf-8", timeout: 3e4 }).trim();
|
|
307
|
-
try {
|
|
308
|
-
git(`git fetch origin ${baseBranch}`);
|
|
309
|
-
} catch {
|
|
310
|
-
return { cleaned: false, message: `Failed to fetch origin/${baseBranch}` };
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
git(`git diff --quiet origin/${baseBranch} -- ${DEVCONTAINER_PATH}`);
|
|
314
|
-
return { cleaned: false, message: "devcontainer.json already matches base" };
|
|
315
|
-
} catch {
|
|
316
|
-
}
|
|
317
|
-
try {
|
|
318
|
-
const ahead = parseInt(git(`git rev-list --count origin/${baseBranch}..HEAD`), 10);
|
|
319
|
-
if (ahead <= 1) {
|
|
320
|
-
git(`git reset --hard origin/${baseBranch}`);
|
|
321
|
-
} else {
|
|
322
|
-
git(`git checkout origin/${baseBranch} -- ${DEVCONTAINER_PATH}`);
|
|
323
|
-
git(`git add ${DEVCONTAINER_PATH}`);
|
|
324
|
-
try {
|
|
325
|
-
git(`git diff --cached --quiet -- ${DEVCONTAINER_PATH}`);
|
|
326
|
-
return { cleaned: false, message: "devcontainer.json already clean in working tree" };
|
|
327
|
-
} catch {
|
|
328
|
-
git(`git commit -m "chore: reset devcontainer config"`);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
git(`git push --force-with-lease origin ${taskBranch}`);
|
|
332
|
-
return { cleaned: true, message: "devcontainer.json cleaned from git history" };
|
|
333
|
-
} catch (err) {
|
|
334
|
-
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
335
|
-
return { cleaned: false, message: `Git cleanup failed: ${msg}` };
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// src/worktree.ts
|
|
340
|
-
import { execSync as execSync2 } from "child_process";
|
|
341
|
-
import { existsSync } from "fs";
|
|
342
|
-
import { join as join2 } from "path";
|
|
343
|
-
var WORKTREE_DIR = ".worktrees";
|
|
344
|
-
function ensureWorktree(projectDir, taskId, branch) {
|
|
345
|
-
const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
|
|
346
|
-
if (existsSync(worktreePath)) {
|
|
347
|
-
if (branch) {
|
|
348
|
-
try {
|
|
349
|
-
execSync2(`git checkout ${branch}`, { cwd: worktreePath, stdio: "ignore" });
|
|
350
|
-
} catch {
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return worktreePath;
|
|
354
|
-
}
|
|
355
|
-
const target = branch ?? "HEAD";
|
|
356
|
-
execSync2(`git worktree add "${worktreePath}" ${target}`, {
|
|
357
|
-
cwd: projectDir,
|
|
358
|
-
stdio: "ignore"
|
|
359
|
-
});
|
|
360
|
-
return worktreePath;
|
|
361
|
-
}
|
|
362
|
-
function removeWorktree(projectDir, taskId) {
|
|
363
|
-
const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
|
|
364
|
-
if (!existsSync(worktreePath)) return;
|
|
365
|
-
try {
|
|
366
|
-
execSync2(`git worktree remove "${worktreePath}" --force`, {
|
|
367
|
-
cwd: projectDir,
|
|
368
|
-
stdio: "ignore"
|
|
369
|
-
});
|
|
370
|
-
} catch {
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// src/runner.ts
|
|
375
|
-
import { randomUUID } from "crypto";
|
|
376
|
-
import { execSync as execSync3 } from "child_process";
|
|
377
|
-
import { readdirSync, statSync, readFileSync } from "fs";
|
|
378
|
-
import { homedir } from "os";
|
|
379
|
-
import { join as join3 } from "path";
|
|
380
|
-
import {
|
|
381
|
-
query,
|
|
382
|
-
tool,
|
|
383
|
-
createSdkMcpServer
|
|
384
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
385
|
-
import { z } from "zod";
|
|
386
|
-
var API_ERROR_PATTERN = /API Error: [45]\d\d/;
|
|
387
|
-
var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
|
|
388
|
-
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
389
|
-
var PM_DENIED_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit", "Bash"]);
|
|
390
|
-
var AgentRunner = class _AgentRunner {
|
|
391
|
-
config;
|
|
392
|
-
connection;
|
|
393
|
-
callbacks;
|
|
394
|
-
_state = "connecting";
|
|
395
|
-
stopped = false;
|
|
396
|
-
inputResolver = null;
|
|
397
|
-
pendingMessages = [];
|
|
398
|
-
currentTurnToolCalls = [];
|
|
399
|
-
setupLog = [];
|
|
400
|
-
heartbeatTimer = null;
|
|
401
|
-
taskContext = null;
|
|
402
|
-
planFileSnapshot = /* @__PURE__ */ new Map();
|
|
403
|
-
worktreeActive = false;
|
|
404
|
-
static MAX_SETUP_LOG_LINES = 50;
|
|
405
|
-
constructor(config, callbacks) {
|
|
406
|
-
this.config = config;
|
|
407
|
-
this.connection = new ConveyorConnection(config);
|
|
408
|
-
this.callbacks = callbacks;
|
|
409
|
-
}
|
|
410
|
-
get state() {
|
|
411
|
-
return this._state;
|
|
412
|
-
}
|
|
413
|
-
async setState(status) {
|
|
414
|
-
this._state = status;
|
|
415
|
-
this.connection.emitStatus(status);
|
|
416
|
-
await this.callbacks.onStatusChange(status);
|
|
417
|
-
}
|
|
418
|
-
startHeartbeat() {
|
|
419
|
-
this.heartbeatTimer = setInterval(() => {
|
|
420
|
-
if (!this.stopped) {
|
|
421
|
-
this.connection.sendHeartbeat();
|
|
422
|
-
}
|
|
423
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
424
|
-
}
|
|
425
|
-
stopHeartbeat() {
|
|
426
|
-
if (this.heartbeatTimer) {
|
|
427
|
-
clearInterval(this.heartbeatTimer);
|
|
428
|
-
this.heartbeatTimer = null;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
// oxlint-disable-next-line max-lines-per-function
|
|
432
|
-
async start() {
|
|
433
|
-
await this.setState("connecting");
|
|
434
|
-
await this.connection.connect();
|
|
435
|
-
this.connection.onStopRequested(() => {
|
|
436
|
-
this.stop();
|
|
437
|
-
});
|
|
438
|
-
this.connection.onChatMessage((message) => {
|
|
439
|
-
this.injectHumanMessage(message.content);
|
|
440
|
-
});
|
|
441
|
-
await this.setState("connected");
|
|
442
|
-
this.connection.sendEvent({
|
|
443
|
-
type: "connected",
|
|
444
|
-
taskId: this.config.taskId
|
|
445
|
-
});
|
|
446
|
-
this.startHeartbeat();
|
|
447
|
-
if (this.config.mode !== "pm" && process.env.CODESPACES === "true") {
|
|
448
|
-
const setupOk = await this.runSetupSafe();
|
|
449
|
-
if (!setupOk) {
|
|
450
|
-
this.stopHeartbeat();
|
|
451
|
-
await this.setState("error");
|
|
452
|
-
this.connection.disconnect();
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
this.initRtk();
|
|
457
|
-
if (this.config.mode === "pm" || process.env.CONVEYOR_USE_WORKTREE === "true") {
|
|
458
|
-
try {
|
|
459
|
-
const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
|
|
460
|
-
this.config = { ...this.config, workspaceDir: worktreePath };
|
|
461
|
-
this.worktreeActive = true;
|
|
462
|
-
this.setupLog.push(`[conveyor] Using worktree: ${worktreePath}`);
|
|
463
|
-
} catch (error) {
|
|
464
|
-
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
465
|
-
this.setupLog.push(`[conveyor] Worktree creation failed, using shared workspace: ${msg}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
await this.setState("fetching_context");
|
|
469
|
-
try {
|
|
470
|
-
this.taskContext = await this.connection.fetchTaskContext();
|
|
471
|
-
} catch (error) {
|
|
472
|
-
const message = error instanceof Error ? error.message : "Failed to fetch task context";
|
|
473
|
-
this.connection.sendEvent({ type: "error", message });
|
|
474
|
-
await this.callbacks.onEvent({ type: "error", message });
|
|
475
|
-
this.stopHeartbeat();
|
|
476
|
-
await this.setState("error");
|
|
477
|
-
this.connection.disconnect();
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
if (process.env.CODESPACES === "true" && this.taskContext.baseBranch) {
|
|
481
|
-
const result = cleanDevcontainerFromGit(
|
|
482
|
-
this.config.workspaceDir,
|
|
483
|
-
this.taskContext.githubBranch,
|
|
484
|
-
this.taskContext.baseBranch
|
|
485
|
-
);
|
|
486
|
-
if (result.cleaned) {
|
|
487
|
-
this.setupLog.push(`[conveyor] ${result.message}`);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (!this.worktreeActive && this.taskContext.useWorktree) {
|
|
491
|
-
try {
|
|
492
|
-
const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
|
|
493
|
-
this.config = { ...this.config, workspaceDir: worktreePath };
|
|
494
|
-
this.worktreeActive = true;
|
|
495
|
-
this.setupLog.push(`[conveyor] Using worktree (from task config): ${worktreePath}`);
|
|
496
|
-
} catch (error) {
|
|
497
|
-
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
498
|
-
this.setupLog.push(`[conveyor] Worktree creation failed, using shared workspace: ${msg}`);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
if (this.worktreeActive && this.taskContext.githubBranch) {
|
|
502
|
-
try {
|
|
503
|
-
const branch = this.taskContext.githubBranch;
|
|
504
|
-
execSync3(`git fetch origin ${branch} && git checkout ${branch}`, {
|
|
505
|
-
cwd: this.config.workspaceDir,
|
|
506
|
-
stdio: "ignore"
|
|
507
|
-
});
|
|
508
|
-
} catch {
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
const isPm = this.config.mode === "pm";
|
|
512
|
-
if (isPm) {
|
|
513
|
-
await this.setState("idle");
|
|
514
|
-
} else {
|
|
515
|
-
await this.setState("running");
|
|
516
|
-
await this.runSdkQuery(this.taskContext);
|
|
517
|
-
if (!this.stopped) {
|
|
518
|
-
await this.setState("idle");
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
while (!this.stopped) {
|
|
522
|
-
if (this._state === "idle") {
|
|
523
|
-
const msg = await this.waitForUserContent();
|
|
524
|
-
if (!msg) break;
|
|
525
|
-
await this.setState("running");
|
|
526
|
-
await this.runSdkQuery(this.taskContext, msg);
|
|
527
|
-
if (!this.stopped) {
|
|
528
|
-
await this.setState("idle");
|
|
529
|
-
}
|
|
530
|
-
} else if (this._state === "error") {
|
|
531
|
-
await this.setState("idle");
|
|
532
|
-
} else {
|
|
533
|
-
break;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
this.stopHeartbeat();
|
|
537
|
-
await this.setState("finished");
|
|
538
|
-
this.connection.disconnect();
|
|
539
|
-
}
|
|
540
|
-
/**
|
|
541
|
-
* Wraps project setup in try/catch. On failure, posts error to chat
|
|
542
|
-
* directly (no SDK needed) and streams to CLI. Returns false if setup failed.
|
|
543
|
-
*/
|
|
544
|
-
async runSetupSafe() {
|
|
545
|
-
await this.setState("setup");
|
|
546
|
-
const ports = await loadForwardPorts(this.config.workspaceDir);
|
|
547
|
-
if (ports.length > 0 && process.env.CODESPACE_NAME) {
|
|
548
|
-
const visibility = ports.map((p) => `${p}:public`).join(" ");
|
|
549
|
-
runStartCommand(
|
|
550
|
-
`gh codespace ports visibility ${visibility} -c "${process.env.CODESPACE_NAME}" 2>/dev/null`,
|
|
551
|
-
this.config.workspaceDir,
|
|
552
|
-
() => void 0
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
const config = await loadConveyorConfig(this.config.workspaceDir);
|
|
556
|
-
if (!config) {
|
|
557
|
-
this.connection.sendEvent({ type: "setup_complete" });
|
|
558
|
-
await this.callbacks.onEvent({ type: "setup_complete" });
|
|
559
|
-
return true;
|
|
560
|
-
}
|
|
561
|
-
try {
|
|
562
|
-
await this.executeSetupConfig(config);
|
|
563
|
-
const setupEvent = {
|
|
564
|
-
type: "setup_complete",
|
|
565
|
-
previewPort: config.previewPort ?? void 0
|
|
566
|
-
};
|
|
567
|
-
this.connection.sendEvent(setupEvent);
|
|
568
|
-
await this.callbacks.onEvent(setupEvent);
|
|
569
|
-
return true;
|
|
570
|
-
} catch (error) {
|
|
571
|
-
const message = error instanceof Error ? error.message : "Setup failed";
|
|
572
|
-
this.connection.sendEvent({ type: "setup_error", message });
|
|
573
|
-
await this.callbacks.onEvent({ type: "setup_error", message });
|
|
574
|
-
this.connection.postChatMessage(
|
|
575
|
-
`Environment setup failed: ${message}
|
|
576
|
-
The agent cannot start until this is resolved.`
|
|
577
|
-
);
|
|
578
|
-
return false;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
pushSetupLog(line) {
|
|
582
|
-
this.setupLog.push(line);
|
|
583
|
-
if (this.setupLog.length > _AgentRunner.MAX_SETUP_LOG_LINES) {
|
|
584
|
-
this.setupLog.splice(0, this.setupLog.length - _AgentRunner.MAX_SETUP_LOG_LINES);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
async executeSetupConfig(config) {
|
|
588
|
-
if (config.setupCommand) {
|
|
589
|
-
this.pushSetupLog(`$ ${config.setupCommand}`);
|
|
590
|
-
await runSetupCommand(config.setupCommand, this.config.workspaceDir, (stream, data) => {
|
|
591
|
-
this.connection.sendEvent({ type: "setup_output", stream, data });
|
|
592
|
-
for (const line of data.split("\n").filter(Boolean)) {
|
|
593
|
-
this.pushSetupLog(`[${stream}] ${line}`);
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
this.pushSetupLog("(exit 0)");
|
|
597
|
-
}
|
|
598
|
-
if (config.startCommand) {
|
|
599
|
-
this.pushSetupLog(`$ ${config.startCommand} & (background)`);
|
|
600
|
-
runStartCommand(config.startCommand, this.config.workspaceDir, (stream, data) => {
|
|
601
|
-
this.connection.sendEvent({ type: "start_command_output", stream, data });
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
initRtk() {
|
|
606
|
-
try {
|
|
607
|
-
execSync3("rtk --version", { stdio: "ignore" });
|
|
608
|
-
execSync3("rtk init --global --auto-patch", { stdio: "ignore" });
|
|
609
|
-
} catch {
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
injectHumanMessage(content) {
|
|
613
|
-
const msg = {
|
|
614
|
-
type: "user",
|
|
615
|
-
session_id: "",
|
|
616
|
-
message: { role: "user", content },
|
|
617
|
-
parent_tool_use_id: null
|
|
618
|
-
};
|
|
619
|
-
if (this.inputResolver) {
|
|
620
|
-
const resolve = this.inputResolver;
|
|
621
|
-
this.inputResolver = null;
|
|
622
|
-
resolve(msg);
|
|
623
|
-
} else {
|
|
624
|
-
this.pendingMessages.push(msg);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
waitForMessage() {
|
|
628
|
-
return new Promise((resolve) => {
|
|
629
|
-
const checkStopped = setInterval(() => {
|
|
630
|
-
if (this.stopped) {
|
|
631
|
-
clearInterval(checkStopped);
|
|
632
|
-
this.inputResolver = null;
|
|
633
|
-
resolve(null);
|
|
634
|
-
}
|
|
635
|
-
}, 1e3);
|
|
636
|
-
this.inputResolver = (msg) => {
|
|
637
|
-
clearInterval(checkStopped);
|
|
638
|
-
resolve(msg);
|
|
639
|
-
};
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Wait for the next user message content string. Drains pendingMessages first.
|
|
644
|
-
* Returns null if stopped.
|
|
645
|
-
*/
|
|
646
|
-
async waitForUserContent() {
|
|
647
|
-
if (this.pendingMessages.length > 0) {
|
|
648
|
-
const next = this.pendingMessages.shift();
|
|
649
|
-
return next.message.content;
|
|
650
|
-
}
|
|
651
|
-
const msg = await this.waitForMessage();
|
|
652
|
-
if (!msg) return null;
|
|
653
|
-
return msg.message.content;
|
|
654
|
-
}
|
|
655
|
-
async *createInputStream(initialPrompt) {
|
|
656
|
-
const makeUserMessage = (content) => ({
|
|
657
|
-
type: "user",
|
|
658
|
-
session_id: "",
|
|
659
|
-
message: { role: "user", content },
|
|
660
|
-
parent_tool_use_id: null
|
|
661
|
-
});
|
|
662
|
-
yield makeUserMessage(initialPrompt);
|
|
663
|
-
while (!this.stopped) {
|
|
664
|
-
if (this.pendingMessages.length > 0) {
|
|
665
|
-
const next = this.pendingMessages.shift();
|
|
666
|
-
if (next) {
|
|
667
|
-
yield next;
|
|
668
|
-
}
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
this.connection.emitStatus("waiting_for_input");
|
|
672
|
-
await this.callbacks.onStatusChange("waiting_for_input");
|
|
673
|
-
const msg = await this.waitForMessage();
|
|
674
|
-
if (!msg) break;
|
|
675
|
-
this.connection.emitStatus("running");
|
|
676
|
-
await this.callbacks.onStatusChange("running");
|
|
677
|
-
yield msg;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
findLastAgentMessageIndex(history) {
|
|
681
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
682
|
-
if (history[i].role === "assistant") return i;
|
|
683
|
-
}
|
|
684
|
-
return -1;
|
|
685
|
-
}
|
|
686
|
-
detectRelaunchScenario(context) {
|
|
687
|
-
const lastAgentIdx = this.findLastAgentMessageIndex(context.chatHistory);
|
|
688
|
-
if (lastAgentIdx === -1) return "fresh";
|
|
689
|
-
const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId;
|
|
690
|
-
if (!hasPriorWork) return "fresh";
|
|
691
|
-
const messagesAfterAgent = context.chatHistory.slice(lastAgentIdx + 1);
|
|
692
|
-
const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
|
|
693
|
-
return hasNewUserMessages ? "feedback_relaunch" : "idle_relaunch";
|
|
694
|
-
}
|
|
695
|
-
// eslint-disable-next-line max-lines-per-function
|
|
696
|
-
buildInitialPrompt(context) {
|
|
697
|
-
const parts = [];
|
|
698
|
-
const scenario = this.detectRelaunchScenario(context);
|
|
699
|
-
if (context.claudeSessionId && scenario !== "fresh") {
|
|
700
|
-
if (this.config.mode === "pm") {
|
|
701
|
-
const lastAgentIdx = this.findLastAgentMessageIndex(context.chatHistory);
|
|
702
|
-
const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
|
|
703
|
-
if (newMessages.length > 0) {
|
|
704
|
-
parts.push(
|
|
705
|
-
`You have been relaunched. Here are new messages since your last session:`,
|
|
706
|
-
...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`)
|
|
707
|
-
);
|
|
708
|
-
} else {
|
|
709
|
-
parts.push(`You have been relaunched. No new messages since your last session.`);
|
|
710
|
-
}
|
|
711
|
-
parts.push(
|
|
712
|
-
`
|
|
713
|
-
You are the project manager for this task.`,
|
|
714
|
-
`Review the context above and wait for the team to provide instructions before taking action.`
|
|
715
|
-
);
|
|
716
|
-
} else if (scenario === "feedback_relaunch") {
|
|
717
|
-
const lastAgentIdx = this.findLastAgentMessageIndex(context.chatHistory);
|
|
718
|
-
const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
|
|
719
|
-
parts.push(
|
|
720
|
-
`You have been relaunched with new feedback.`,
|
|
721
|
-
`Work on the git branch "${context.githubBranch}".`,
|
|
722
|
-
`
|
|
723
|
-
New messages since your last run:`,
|
|
724
|
-
...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
|
|
725
|
-
`
|
|
726
|
-
Address the requested changes. Commit and push your updates.`
|
|
727
|
-
);
|
|
728
|
-
if (context.githubPRUrl) {
|
|
729
|
-
parts.push(
|
|
730
|
-
`An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch. Do NOT create a new PR.`
|
|
731
|
-
);
|
|
732
|
-
} else {
|
|
733
|
-
parts.push(
|
|
734
|
-
`When finished, use the create_pull_request tool to open a PR. Do NOT use gh CLI.`
|
|
735
|
-
);
|
|
736
|
-
}
|
|
737
|
-
} else {
|
|
738
|
-
parts.push(
|
|
739
|
-
`You were relaunched but no new instructions have been given since your last run.`,
|
|
740
|
-
`Work on the git branch "${context.githubBranch}".`,
|
|
741
|
-
`Review the current state of the codebase and verify everything is working correctly.`,
|
|
742
|
-
`Post a brief status update to the chat, then wait for further instructions.`
|
|
743
|
-
);
|
|
744
|
-
if (context.githubPRUrl) {
|
|
745
|
-
parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
return parts.join("\n");
|
|
749
|
-
}
|
|
750
|
-
parts.push(`# Task: ${context.title}`);
|
|
751
|
-
if (context.description) {
|
|
752
|
-
parts.push(`
|
|
753
|
-
## Description
|
|
754
|
-
${context.description}`);
|
|
755
|
-
}
|
|
756
|
-
if (context.plan) {
|
|
757
|
-
parts.push(`
|
|
758
|
-
## Plan
|
|
759
|
-
${context.plan}`);
|
|
760
|
-
}
|
|
761
|
-
if (context.files && context.files.length > 0) {
|
|
762
|
-
parts.push(`
|
|
763
|
-
## Attached Files`);
|
|
764
|
-
for (const file of context.files) {
|
|
765
|
-
parts.push(`- **${file.fileName}** (${file.mimeType}): ${file.downloadUrl}`);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
if (context.repoRefs && context.repoRefs.length > 0) {
|
|
769
|
-
parts.push(`
|
|
770
|
-
## Repository References`);
|
|
771
|
-
for (const ref of context.repoRefs) {
|
|
772
|
-
const icon = ref.refType === "folder" ? "folder" : "file";
|
|
773
|
-
parts.push(`- [${icon}] \`${ref.path}\``);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
if (context.chatHistory.length > 0) {
|
|
777
|
-
const relevant = context.chatHistory.slice(-20);
|
|
778
|
-
parts.push(`
|
|
779
|
-
## Recent Chat Context`);
|
|
780
|
-
for (const msg of relevant) {
|
|
781
|
-
const sender = msg.userName ?? msg.role;
|
|
782
|
-
parts.push(`[${sender}]: ${msg.content}`);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
parts.push(`
|
|
786
|
-
## Instructions`);
|
|
787
|
-
if (scenario === "fresh") {
|
|
788
|
-
if (this.config.mode === "pm") {
|
|
789
|
-
parts.push(
|
|
790
|
-
`You are the project manager for this task.`,
|
|
791
|
-
`The task details are provided above. Wait for the team to ask questions or provide additional requirements before starting to plan.`
|
|
792
|
-
);
|
|
793
|
-
} else {
|
|
794
|
-
parts.push(
|
|
795
|
-
`Begin executing the task plan above immediately.`,
|
|
796
|
-
`Your FIRST action should be reading the relevant source files mentioned in the plan, then writing code. Do NOT run install, build, lint, test, or dev server commands first \u2014 the environment is already set up.`,
|
|
797
|
-
`Work on the git branch "${context.githubBranch}".`,
|
|
798
|
-
`Post a brief message to chat when you begin meaningful implementation, and again when the PR is ready.`,
|
|
799
|
-
`When finished, commit your changes, push the branch, and use the create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
} else if (scenario === "idle_relaunch") {
|
|
803
|
-
if (this.config.mode === "pm") {
|
|
804
|
-
parts.push(
|
|
805
|
-
`You were relaunched but no new instructions have been given since your last run.`,
|
|
806
|
-
`You are the project manager for this task.`,
|
|
807
|
-
`Wait for the team to provide instructions before taking action.`
|
|
808
|
-
);
|
|
809
|
-
} else {
|
|
810
|
-
parts.push(
|
|
811
|
-
`You were relaunched but no new instructions have been given since your last run.`,
|
|
812
|
-
`Work on the git branch "${context.githubBranch}".`,
|
|
813
|
-
`Review the current state of the codebase and verify everything is working correctly (e.g. tests pass, the web server starts on port 3000).`,
|
|
814
|
-
`Post a brief status update to the chat summarizing the current state.`,
|
|
815
|
-
`Then wait for further instructions \u2014 do NOT redo work that was already completed.`
|
|
816
|
-
);
|
|
817
|
-
if (context.githubPRUrl) {
|
|
818
|
-
parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
} else {
|
|
822
|
-
const lastAgentIdx = this.findLastAgentMessageIndex(context.chatHistory);
|
|
823
|
-
const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
|
|
824
|
-
if (this.config.mode === "pm") {
|
|
825
|
-
parts.push(
|
|
826
|
-
`You were relaunched with new feedback since your last run.`,
|
|
827
|
-
`You are the project manager for this task.`,
|
|
828
|
-
`
|
|
829
|
-
New messages since your last run:`,
|
|
830
|
-
...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
|
|
831
|
-
`
|
|
832
|
-
Review these messages and wait for the team to provide instructions before taking action.`
|
|
833
|
-
);
|
|
834
|
-
} else {
|
|
835
|
-
parts.push(
|
|
836
|
-
`You were relaunched with new feedback since your last run.`,
|
|
837
|
-
`Work on the git branch "${context.githubBranch}".`,
|
|
838
|
-
`
|
|
839
|
-
New messages since your last run:`,
|
|
840
|
-
...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
|
|
841
|
-
`
|
|
842
|
-
Address the requested changes. Commit and push your updates.`
|
|
843
|
-
);
|
|
844
|
-
if (context.githubPRUrl) {
|
|
845
|
-
parts.push(
|
|
846
|
-
`An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch to update it. Do NOT create a new PR.`
|
|
847
|
-
);
|
|
848
|
-
} else {
|
|
849
|
-
parts.push(
|
|
850
|
-
`When finished, use the create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
|
|
851
|
-
);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
return parts.join("\n");
|
|
856
|
-
}
|
|
857
|
-
buildSystemPrompt(context) {
|
|
858
|
-
const parts = this.config.mode === "pm" ? [
|
|
859
|
-
`You are an AI project manager helping to plan tasks for the "${context.title}" project.`,
|
|
860
|
-
`You are running locally with full access to the repository.`,
|
|
861
|
-
`
|
|
862
|
-
Environment (ready, no setup required):`,
|
|
863
|
-
`- Repository is cloned at your current working directory.`,
|
|
864
|
-
`- You can read files to understand the codebase before writing task plans.`,
|
|
865
|
-
`- Check the dev branch (e.g. run: git fetch && git checkout dev || git checkout main) to understand the current state of the codebase that agents will branch off of.`
|
|
866
|
-
] : [
|
|
867
|
-
`You are an AI agent working on a task for the "${context.title}" project.`,
|
|
868
|
-
`You are running inside a GitHub Codespace with full access to the repository.`,
|
|
869
|
-
`
|
|
870
|
-
Environment (fully ready \u2014 do NOT verify or set up):`,
|
|
871
|
-
`- Repository is cloned at your current working directory.`,
|
|
872
|
-
`- Branch \`${context.githubBranch}\` is already checked out.`,
|
|
873
|
-
`- All dependencies are installed, database is migrated, and the dev server is running.`,
|
|
874
|
-
`- Git is configured. Commit and push directly to this branch.`,
|
|
875
|
-
`
|
|
876
|
-
IMPORTANT \u2014 Skip all environment verification. Do NOT run any of the following:`,
|
|
877
|
-
`- bun/npm install, pip install, or any dependency installation`,
|
|
878
|
-
`- bun build, bun lint, bun test, bun typecheck, or any build/check commands as a "first step"`,
|
|
879
|
-
`- bun db:generate, bun db:push, prisma migrate, or any database setup`,
|
|
880
|
-
`- bun dev, npm start, or any dev server startup commands`,
|
|
881
|
-
`- pwd, ls, echo, or exploratory shell commands to "check" the environment`,
|
|
882
|
-
`Only run these if you encounter a specific error that requires it.`,
|
|
883
|
-
`Start reading the task plan and writing code immediately.`
|
|
884
|
-
];
|
|
885
|
-
if (this.setupLog.length > 0) {
|
|
886
|
-
parts.push(
|
|
887
|
-
`
|
|
888
|
-
Environment setup log (already executed before you started \u2014 proof that setup succeeded):`,
|
|
889
|
-
"```",
|
|
890
|
-
...this.setupLog,
|
|
891
|
-
"```"
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
if (context.agentInstructions) {
|
|
895
|
-
parts.push(`
|
|
896
|
-
Agent Instructions:
|
|
897
|
-
${context.agentInstructions}`);
|
|
898
|
-
}
|
|
899
|
-
if (this.config.instructions) {
|
|
900
|
-
parts.push(`
|
|
901
|
-
Additional Instructions:
|
|
902
|
-
${this.config.instructions}`);
|
|
903
|
-
}
|
|
904
|
-
parts.push(
|
|
905
|
-
`
|
|
906
|
-
You have access to Conveyor MCP tools to interact with the task management system.`,
|
|
907
|
-
`Use the post_to_chat tool to communicate progress or ask questions.`,
|
|
908
|
-
`Use the read_task_chat tool to check for new messages from the team.`
|
|
909
|
-
);
|
|
910
|
-
if (this.config.mode !== "pm") {
|
|
911
|
-
parts.push(
|
|
912
|
-
`Use the create_pull_request tool to open PRs \u2014 do NOT use gh CLI or shell commands for PR creation.`
|
|
913
|
-
);
|
|
914
|
-
}
|
|
915
|
-
return parts.join("\n");
|
|
916
|
-
}
|
|
917
|
-
// oxlint-disable-next-line typescript/explicit-function-return-type, max-lines-per-function
|
|
918
|
-
createConveyorMcpServer(context) {
|
|
919
|
-
const connection = this.connection;
|
|
920
|
-
const config = this.config;
|
|
921
|
-
const textResult = (text) => ({
|
|
922
|
-
content: [{ type: "text", text }]
|
|
923
|
-
});
|
|
924
|
-
const commonTools = [
|
|
925
|
-
tool(
|
|
926
|
-
"read_task_chat",
|
|
927
|
-
"Read recent messages from the task chat to see team feedback or instructions",
|
|
928
|
-
{
|
|
929
|
-
limit: z.number().optional().describe("Number of recent messages to fetch (default 20)")
|
|
930
|
-
},
|
|
931
|
-
async ({ limit }) => {
|
|
932
|
-
try {
|
|
933
|
-
const messages = await connection.fetchChatMessages(limit);
|
|
934
|
-
return textResult(JSON.stringify(messages, null, 2));
|
|
935
|
-
} catch {
|
|
936
|
-
return textResult(
|
|
937
|
-
JSON.stringify({
|
|
938
|
-
note: "Could not fetch live chat. Chat history was provided in the initial context."
|
|
939
|
-
})
|
|
940
|
-
);
|
|
941
|
-
}
|
|
942
|
-
},
|
|
943
|
-
{ annotations: { readOnly: true } }
|
|
944
|
-
),
|
|
945
|
-
tool(
|
|
946
|
-
"post_to_chat",
|
|
947
|
-
"Post a message to the task chat visible to all team members",
|
|
948
|
-
{ message: z.string().describe("The message to post to the team") },
|
|
949
|
-
({ message }) => {
|
|
950
|
-
connection.postChatMessage(message);
|
|
951
|
-
return Promise.resolve(textResult("Message posted to task chat."));
|
|
952
|
-
}
|
|
953
|
-
),
|
|
954
|
-
tool(
|
|
955
|
-
"update_task_status",
|
|
956
|
-
"Update the task status on the Kanban board",
|
|
957
|
-
{
|
|
958
|
-
status: z.enum(["InProgress", "ReviewPR", "Complete"]).describe("The new status for the task")
|
|
959
|
-
},
|
|
960
|
-
({ status }) => {
|
|
961
|
-
connection.updateStatus(status);
|
|
962
|
-
return Promise.resolve(textResult(`Task status updated to ${status}.`));
|
|
963
|
-
}
|
|
964
|
-
),
|
|
965
|
-
tool(
|
|
966
|
-
"get_task_plan",
|
|
967
|
-
"Re-read the latest task plan in case it was updated",
|
|
968
|
-
{},
|
|
969
|
-
async () => {
|
|
970
|
-
try {
|
|
971
|
-
const ctx = await connection.fetchTaskContext();
|
|
972
|
-
return textResult(ctx.plan ?? "No plan available.");
|
|
973
|
-
} catch {
|
|
974
|
-
return textResult(`Task ID: ${config.taskId} - could not fetch updated plan.`);
|
|
975
|
-
}
|
|
976
|
-
},
|
|
977
|
-
{ annotations: { readOnly: true } }
|
|
978
|
-
)
|
|
979
|
-
];
|
|
980
|
-
const modeTools = config.mode === "pm" ? [
|
|
981
|
-
tool(
|
|
982
|
-
"update_task",
|
|
983
|
-
"Save the finalized task plan and/or description",
|
|
984
|
-
{
|
|
985
|
-
plan: z.string().optional().describe("The task plan in markdown"),
|
|
986
|
-
description: z.string().optional().describe("Updated task description")
|
|
987
|
-
},
|
|
988
|
-
async ({ plan, description }) => {
|
|
989
|
-
try {
|
|
990
|
-
connection.updateTaskFields({ plan, description });
|
|
991
|
-
return textResult("Task updated successfully.");
|
|
992
|
-
} catch {
|
|
993
|
-
return textResult("Failed to update task.");
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
)
|
|
997
|
-
] : [
|
|
998
|
-
tool(
|
|
999
|
-
"create_pull_request",
|
|
1000
|
-
"Create a GitHub pull request for this task. Use this instead of gh CLI or git commands to create PRs.",
|
|
1001
|
-
{
|
|
1002
|
-
title: z.string().describe("The PR title"),
|
|
1003
|
-
body: z.string().describe("The PR description/body in markdown")
|
|
1004
|
-
},
|
|
1005
|
-
async ({ title, body }) => {
|
|
1006
|
-
try {
|
|
1007
|
-
const result = await connection.createPR({ title, body });
|
|
1008
|
-
connection.sendEvent({
|
|
1009
|
-
type: "pr_created",
|
|
1010
|
-
url: result.url,
|
|
1011
|
-
number: result.number
|
|
1012
|
-
});
|
|
1013
|
-
return textResult(`Pull request #${result.number} created: ${result.url}`);
|
|
1014
|
-
} catch (error) {
|
|
1015
|
-
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
1016
|
-
return textResult(`Failed to create pull request: ${msg}`);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
)
|
|
1020
|
-
];
|
|
1021
|
-
return createSdkMcpServer({
|
|
1022
|
-
name: "conveyor",
|
|
1023
|
-
tools: [...commonTools, ...modeTools]
|
|
1024
|
-
});
|
|
1025
|
-
void context;
|
|
1026
|
-
}
|
|
1027
|
-
// oxlint-disable-next-line max-lines-per-function, complexity
|
|
1028
|
-
async processEvents(events, context) {
|
|
1029
|
-
const startTime = Date.now();
|
|
1030
|
-
let totalCostUsd = 0;
|
|
1031
|
-
let sessionIdStored = false;
|
|
1032
|
-
let isTyping = false;
|
|
1033
|
-
let retriable = false;
|
|
1034
|
-
for await (const event of events) {
|
|
1035
|
-
if (this.stopped) break;
|
|
1036
|
-
switch (event.type) {
|
|
1037
|
-
case "system": {
|
|
1038
|
-
if (event.subtype === "init") {
|
|
1039
|
-
const sessionId = event.session_id;
|
|
1040
|
-
if (sessionId && !sessionIdStored) {
|
|
1041
|
-
sessionIdStored = true;
|
|
1042
|
-
this.connection.storeSessionId(sessionId);
|
|
1043
|
-
}
|
|
1044
|
-
await this.callbacks.onEvent({
|
|
1045
|
-
type: "thinking",
|
|
1046
|
-
message: `Agent initialized (model: ${event.model})`
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
break;
|
|
1050
|
-
}
|
|
1051
|
-
case "assistant": {
|
|
1052
|
-
if (!isTyping) {
|
|
1053
|
-
setTimeout(() => {
|
|
1054
|
-
this.connection.sendTypingStart();
|
|
1055
|
-
}, 200);
|
|
1056
|
-
isTyping = true;
|
|
1057
|
-
}
|
|
1058
|
-
const msg = event.message;
|
|
1059
|
-
const content = msg.content;
|
|
1060
|
-
const turnTextParts = [];
|
|
1061
|
-
for (const block of content) {
|
|
1062
|
-
const blockType = block.type;
|
|
1063
|
-
if (blockType === "text") {
|
|
1064
|
-
const text = block.text;
|
|
1065
|
-
turnTextParts.push(text);
|
|
1066
|
-
this.connection.sendEvent({ type: "message", content: text });
|
|
1067
|
-
await this.callbacks.onEvent({ type: "message", content: text });
|
|
1068
|
-
} else if (blockType === "tool_use") {
|
|
1069
|
-
const name = block.name;
|
|
1070
|
-
const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
|
|
1071
|
-
const isContentTool = ["edit", "write"].includes(name.toLowerCase());
|
|
1072
|
-
const inputLimit = isContentTool ? 1e4 : 500;
|
|
1073
|
-
const summary = {
|
|
1074
|
-
tool: name,
|
|
1075
|
-
input: inputStr.slice(0, inputLimit),
|
|
1076
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1077
|
-
};
|
|
1078
|
-
this.currentTurnToolCalls.push(summary);
|
|
1079
|
-
this.connection.sendEvent({
|
|
1080
|
-
type: "tool_use",
|
|
1081
|
-
tool: name,
|
|
1082
|
-
input: inputStr
|
|
1083
|
-
});
|
|
1084
|
-
await this.callbacks.onEvent({
|
|
1085
|
-
type: "tool_use",
|
|
1086
|
-
tool: name,
|
|
1087
|
-
input: inputStr
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
if (turnTextParts.length > 0) {
|
|
1092
|
-
this.connection.postChatMessage(turnTextParts.join("\n\n"));
|
|
1093
|
-
}
|
|
1094
|
-
if (this.currentTurnToolCalls.length > 0) {
|
|
1095
|
-
this.connection.sendEvent({
|
|
1096
|
-
type: "turn_end",
|
|
1097
|
-
toolCalls: [...this.currentTurnToolCalls]
|
|
1098
|
-
});
|
|
1099
|
-
this.currentTurnToolCalls = [];
|
|
1100
|
-
}
|
|
1101
|
-
if (this.config.mode === "pm") {
|
|
1102
|
-
this.syncPlanFile();
|
|
1103
|
-
}
|
|
1104
|
-
break;
|
|
1105
|
-
}
|
|
1106
|
-
case "result": {
|
|
1107
|
-
if (isTyping) {
|
|
1108
|
-
this.connection.sendTypingStop();
|
|
1109
|
-
isTyping = false;
|
|
1110
|
-
}
|
|
1111
|
-
const resultEvent = event;
|
|
1112
|
-
if (resultEvent.subtype === "success") {
|
|
1113
|
-
totalCostUsd = "total_cost_usd" in resultEvent ? resultEvent.total_cost_usd : 0;
|
|
1114
|
-
const durationMs = Date.now() - startTime;
|
|
1115
|
-
const summary = "result" in resultEvent ? String(resultEvent.result) : "Task completed.";
|
|
1116
|
-
if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
|
|
1117
|
-
retriable = true;
|
|
1118
|
-
}
|
|
1119
|
-
this.connection.sendEvent({
|
|
1120
|
-
type: "completed",
|
|
1121
|
-
summary,
|
|
1122
|
-
costUsd: totalCostUsd,
|
|
1123
|
-
durationMs
|
|
1124
|
-
});
|
|
1125
|
-
if (totalCostUsd > 0 && context.agentId) {
|
|
1126
|
-
const estimatedTotalTokens = Math.round(totalCostUsd * 1e5);
|
|
1127
|
-
const estimatedInputTokens = Math.round(estimatedTotalTokens * 0.7);
|
|
1128
|
-
const estimatedOutputTokens = Math.round(estimatedTotalTokens * 0.3);
|
|
1129
|
-
this.connection.trackSpending({
|
|
1130
|
-
agentId: context.agentId,
|
|
1131
|
-
inputTokens: estimatedInputTokens,
|
|
1132
|
-
outputTokens: estimatedOutputTokens,
|
|
1133
|
-
totalTokens: estimatedTotalTokens,
|
|
1134
|
-
totalCostUsd,
|
|
1135
|
-
onSubscription: this.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
await this.callbacks.onEvent({
|
|
1139
|
-
type: "completed",
|
|
1140
|
-
summary,
|
|
1141
|
-
costUsd: totalCostUsd,
|
|
1142
|
-
durationMs
|
|
1143
|
-
});
|
|
1144
|
-
} else {
|
|
1145
|
-
const errors = "errors" in resultEvent ? resultEvent.errors : [];
|
|
1146
|
-
const errorMsg = errors.length > 0 ? errors.join(", ") : `Agent stopped: ${resultEvent.subtype}`;
|
|
1147
|
-
if (API_ERROR_PATTERN.test(errorMsg)) {
|
|
1148
|
-
retriable = true;
|
|
1149
|
-
}
|
|
1150
|
-
this.connection.sendEvent({ type: "error", message: errorMsg });
|
|
1151
|
-
await this.callbacks.onEvent({ type: "error", message: errorMsg });
|
|
1152
|
-
}
|
|
1153
|
-
break;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
if (isTyping) {
|
|
1158
|
-
this.connection.sendTypingStop();
|
|
1159
|
-
}
|
|
1160
|
-
return { retriable };
|
|
1161
|
-
}
|
|
1162
|
-
/**
|
|
1163
|
-
* Snapshot current plan files so syncPlanFile can distinguish files created
|
|
1164
|
-
* by THIS session from ones created by a concurrent agent.
|
|
1165
|
-
*/
|
|
1166
|
-
snapshotPlanFiles() {
|
|
1167
|
-
const plansDir = join3(homedir(), ".claude", "plans");
|
|
1168
|
-
this.planFileSnapshot.clear();
|
|
1169
|
-
try {
|
|
1170
|
-
for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
|
|
1171
|
-
try {
|
|
1172
|
-
const stat = statSync(join3(plansDir, file));
|
|
1173
|
-
this.planFileSnapshot.set(file, stat.mtimeMs);
|
|
1174
|
-
} catch {
|
|
1175
|
-
continue;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
} catch {
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* After a PM-mode query, scan ~/.claude/plans/ for plan files that were
|
|
1183
|
-
* created or modified by THIS session (not present in the pre-query snapshot)
|
|
1184
|
-
* and push the newest one to the task.
|
|
1185
|
-
*/
|
|
1186
|
-
syncPlanFile() {
|
|
1187
|
-
const plansDir = join3(homedir(), ".claude", "plans");
|
|
1188
|
-
let files;
|
|
1189
|
-
try {
|
|
1190
|
-
files = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
|
|
1191
|
-
} catch {
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
let newest = null;
|
|
1195
|
-
for (const file of files) {
|
|
1196
|
-
const fullPath = join3(plansDir, file);
|
|
1197
|
-
try {
|
|
1198
|
-
const stat = statSync(fullPath);
|
|
1199
|
-
const prevMtime = this.planFileSnapshot.get(file);
|
|
1200
|
-
const isNew = prevMtime === void 0 || stat.mtimeMs > prevMtime;
|
|
1201
|
-
if (isNew && (!newest || stat.mtimeMs > newest.mtime)) {
|
|
1202
|
-
newest = { path: fullPath, mtime: stat.mtimeMs };
|
|
1203
|
-
}
|
|
1204
|
-
} catch {
|
|
1205
|
-
continue;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
if (newest) {
|
|
1209
|
-
const content = readFileSync(newest.path, "utf-8").trim();
|
|
1210
|
-
if (content) {
|
|
1211
|
-
this.connection.updateTaskFields({ plan: content });
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
/**
|
|
1216
|
-
* Runs one SDK query cycle. For PM mode this is a single-turn query per
|
|
1217
|
-
* user message. For task mode this uses the long-running input stream
|
|
1218
|
-
* (initial execution) or a single follow-up prompt.
|
|
1219
|
-
*/
|
|
1220
|
-
// oxlint-disable-next-line max-lines-per-function
|
|
1221
|
-
async runSdkQuery(context, followUpContent) {
|
|
1222
|
-
if (this.stopped) return;
|
|
1223
|
-
const settings = context.agentSettings ?? this.config.agentSettings ?? {};
|
|
1224
|
-
const systemPromptText = this.buildSystemPrompt(context);
|
|
1225
|
-
const conveyorMcp = this.createConveyorMcpServer(context);
|
|
1226
|
-
const systemPrompt = {
|
|
1227
|
-
type: "preset",
|
|
1228
|
-
preset: "claude_code",
|
|
1229
|
-
append: systemPromptText || void 0
|
|
1230
|
-
};
|
|
1231
|
-
const settingSources = settings.settingSources ?? ["user", "project"];
|
|
1232
|
-
const model = context.model || this.config.model;
|
|
1233
|
-
const pmDisallowedTools = this.config.mode === "pm" ? ["TodoWrite", "TodoRead"] : [];
|
|
1234
|
-
const disallowedTools = [...settings.disallowedTools ?? [], ...pmDisallowedTools];
|
|
1235
|
-
const isPm = this.config.mode === "pm";
|
|
1236
|
-
if (isPm) {
|
|
1237
|
-
this.snapshotPlanFiles();
|
|
1238
|
-
}
|
|
1239
|
-
const QUESTION_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1240
|
-
const canUseTool = async (toolName, input) => {
|
|
1241
|
-
if (toolName === "AskUserQuestion") {
|
|
1242
|
-
const questions = input.questions;
|
|
1243
|
-
const requestId = randomUUID();
|
|
1244
|
-
this.connection.emitStatus("waiting_for_input");
|
|
1245
|
-
this.connection.sendEvent({
|
|
1246
|
-
type: "tool_use",
|
|
1247
|
-
tool: "AskUserQuestion",
|
|
1248
|
-
input: JSON.stringify(input)
|
|
1249
|
-
});
|
|
1250
|
-
const answerPromise = this.connection.askUserQuestion(requestId, questions);
|
|
1251
|
-
const timeoutPromise = new Promise(
|
|
1252
|
-
(resolve) => setTimeout(() => resolve(null), QUESTION_TIMEOUT_MS)
|
|
1253
|
-
);
|
|
1254
|
-
const answers = await Promise.race([answerPromise, timeoutPromise]);
|
|
1255
|
-
this.connection.emitStatus("running");
|
|
1256
|
-
if (!answers) {
|
|
1257
|
-
return {
|
|
1258
|
-
behavior: "deny",
|
|
1259
|
-
message: "User did not respond to clarifying questions in time. Proceed with your best judgment."
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
return {
|
|
1263
|
-
behavior: "allow",
|
|
1264
|
-
updatedInput: { questions: input.questions, answers }
|
|
1265
|
-
};
|
|
1266
|
-
}
|
|
1267
|
-
if (isPm && PM_DENIED_TOOLS.has(toolName)) {
|
|
1268
|
-
return {
|
|
1269
|
-
behavior: "deny",
|
|
1270
|
-
message: "PM mode is plan-only. Use the update_task tool to save your plan."
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
return { behavior: "allow", updatedInput: input };
|
|
1274
|
-
};
|
|
1275
|
-
const commonOptions = {
|
|
1276
|
-
model,
|
|
1277
|
-
systemPrompt,
|
|
1278
|
-
settingSources,
|
|
1279
|
-
cwd: this.config.workspaceDir,
|
|
1280
|
-
permissionMode: isPm ? "plan" : "bypassPermissions",
|
|
1281
|
-
allowDangerouslySkipPermissions: !isPm,
|
|
1282
|
-
canUseTool,
|
|
1283
|
-
tools: { type: "preset", preset: "claude_code" },
|
|
1284
|
-
mcpServers: { conveyor: conveyorMcp },
|
|
1285
|
-
maxTurns: settings.maxTurns,
|
|
1286
|
-
effort: settings.effort,
|
|
1287
|
-
thinking: settings.thinking,
|
|
1288
|
-
betas: settings.betas,
|
|
1289
|
-
maxBudgetUsd: settings.maxBudgetUsd ?? 50,
|
|
1290
|
-
disallowedTools: disallowedTools.length > 0 ? disallowedTools : void 0,
|
|
1291
|
-
enableFileCheckpointing: settings.enableFileCheckpointing
|
|
1292
|
-
};
|
|
1293
|
-
const resume = context.claudeSessionId ?? void 0;
|
|
1294
|
-
if (followUpContent) {
|
|
1295
|
-
const prompt = isPm ? `${this.buildInitialPrompt(context)}
|
|
1296
|
-
|
|
1297
|
-
---
|
|
1298
|
-
|
|
1299
|
-
The team says:
|
|
1300
|
-
${followUpContent}` : followUpContent;
|
|
1301
|
-
const agentQuery = query({
|
|
1302
|
-
prompt,
|
|
1303
|
-
options: { ...commonOptions, resume }
|
|
1304
|
-
});
|
|
1305
|
-
await this.runWithRetry(agentQuery, context, commonOptions);
|
|
1306
|
-
} else if (isPm) {
|
|
1307
|
-
return;
|
|
1308
|
-
} else {
|
|
1309
|
-
const initialPrompt = this.buildInitialPrompt(context);
|
|
1310
|
-
const agentQuery = query({
|
|
1311
|
-
prompt: this.createInputStream(initialPrompt),
|
|
1312
|
-
options: { ...commonOptions, resume }
|
|
1313
|
-
});
|
|
1314
|
-
await this.runWithRetry(agentQuery, context, commonOptions);
|
|
1315
|
-
}
|
|
1316
|
-
if (isPm) {
|
|
1317
|
-
this.syncPlanFile();
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Run a query with retry logic for transient API errors.
|
|
1322
|
-
*/
|
|
1323
|
-
async runWithRetry(initialQuery, context, options) {
|
|
1324
|
-
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
1325
|
-
if (this.stopped) return;
|
|
1326
|
-
const agentQuery = attempt === 0 ? initialQuery : query({
|
|
1327
|
-
prompt: this.createInputStream(this.buildInitialPrompt(context)),
|
|
1328
|
-
options: { ...options, resume: void 0 }
|
|
1329
|
-
});
|
|
1330
|
-
try {
|
|
1331
|
-
const { retriable } = await this.processEvents(agentQuery, context);
|
|
1332
|
-
if (!retriable || this.stopped) return;
|
|
1333
|
-
} catch (error) {
|
|
1334
|
-
const isStaleSession = error instanceof Error && error.message.includes("No conversation found with session ID");
|
|
1335
|
-
if (isStaleSession && context.claudeSessionId) {
|
|
1336
|
-
this.connection.storeSessionId("");
|
|
1337
|
-
const freshCtx = { ...context, claudeSessionId: null };
|
|
1338
|
-
const freshQuery = query({
|
|
1339
|
-
prompt: this.createInputStream(this.buildInitialPrompt(freshCtx)),
|
|
1340
|
-
options: { ...options, resume: void 0 }
|
|
1341
|
-
});
|
|
1342
|
-
return this.runWithRetry(freshQuery, freshCtx, options);
|
|
1343
|
-
}
|
|
1344
|
-
const isApiError = error instanceof Error && API_ERROR_PATTERN.test(error.message);
|
|
1345
|
-
if (!isApiError) throw error;
|
|
1346
|
-
}
|
|
1347
|
-
if (attempt >= RETRY_DELAYS_MS.length) {
|
|
1348
|
-
this.connection.postChatMessage(
|
|
1349
|
-
`Agent shutting down after ${RETRY_DELAYS_MS.length} failed retry attempts due to API errors. The task will resume automatically when the codespace restarts.`
|
|
1350
|
-
);
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
const delayMs = RETRY_DELAYS_MS[attempt];
|
|
1354
|
-
const delayMin = Math.round(delayMs / 6e4);
|
|
1355
|
-
this.connection.postChatMessage(
|
|
1356
|
-
`API error encountered. Retrying in ${delayMin} minute${delayMin > 1 ? "s" : ""}... (attempt ${attempt + 1}/${RETRY_DELAYS_MS.length})`
|
|
1357
|
-
);
|
|
1358
|
-
this.connection.sendEvent({
|
|
1359
|
-
type: "error",
|
|
1360
|
-
message: `API error, retrying in ${delayMin}m (${attempt + 1}/${RETRY_DELAYS_MS.length})`
|
|
1361
|
-
});
|
|
1362
|
-
this.connection.emitStatus("waiting_for_input");
|
|
1363
|
-
await this.callbacks.onStatusChange("waiting_for_input");
|
|
1364
|
-
await new Promise((resolve) => {
|
|
1365
|
-
const timer = setTimeout(resolve, delayMs);
|
|
1366
|
-
const checkStopped = setInterval(() => {
|
|
1367
|
-
if (this.stopped) {
|
|
1368
|
-
clearTimeout(timer);
|
|
1369
|
-
clearInterval(checkStopped);
|
|
1370
|
-
resolve();
|
|
1371
|
-
}
|
|
1372
|
-
}, 1e3);
|
|
1373
|
-
setTimeout(() => clearInterval(checkStopped), delayMs + 100);
|
|
1374
|
-
});
|
|
1375
|
-
this.connection.emitStatus("running");
|
|
1376
|
-
await this.callbacks.onStatusChange("running");
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
stop() {
|
|
1380
|
-
this.stopped = true;
|
|
1381
|
-
if (this.inputResolver) {
|
|
1382
|
-
this.inputResolver(null);
|
|
1383
|
-
this.inputResolver = null;
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
};
|
|
1387
|
-
|
|
1388
|
-
export {
|
|
1389
|
-
ConveyorConnection,
|
|
1390
|
-
loadConveyorConfig,
|
|
1391
|
-
runSetupCommand,
|
|
1392
|
-
runStartCommand,
|
|
1393
|
-
ensureWorktree,
|
|
1394
|
-
removeWorktree,
|
|
1395
|
-
AgentRunner
|
|
1396
|
-
};
|
|
1397
|
-
//# sourceMappingURL=chunk-JWIGEYRD.js.map
|