@rallycry/conveyor-agent 2.13.0 → 2.14.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/dist/{chunk-SBAEYHPL.js → chunk-N6QGELGE.js} +465 -58
- package/dist/chunk-N6QGELGE.js.map +1 -0
- package/dist/cli.js +78 -54
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +85 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-SBAEYHPL.js.map +0 -1
|
@@ -15,7 +15,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
15
15
|
this.config = config;
|
|
16
16
|
}
|
|
17
17
|
connect() {
|
|
18
|
-
return new Promise((
|
|
18
|
+
return new Promise((resolve2, reject) => {
|
|
19
19
|
let settled = false;
|
|
20
20
|
let attempts = 0;
|
|
21
21
|
const maxInitialAttempts = 30;
|
|
@@ -61,7 +61,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
61
61
|
this.socket.on("connect", () => {
|
|
62
62
|
if (!settled) {
|
|
63
63
|
settled = true;
|
|
64
|
-
|
|
64
|
+
resolve2();
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
67
|
this.socket.io.on("reconnect_attempt", () => {
|
|
@@ -76,13 +76,13 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
76
76
|
fetchChatMessages(limit) {
|
|
77
77
|
const socket = this.socket;
|
|
78
78
|
if (!socket) throw new Error("Not connected");
|
|
79
|
-
return new Promise((
|
|
79
|
+
return new Promise((resolve2, reject) => {
|
|
80
80
|
socket.emit(
|
|
81
81
|
"agentRunner:getChatMessages",
|
|
82
82
|
{ taskId: this.config.taskId, limit },
|
|
83
83
|
(response) => {
|
|
84
84
|
if (response.success && response.data) {
|
|
85
|
-
|
|
85
|
+
resolve2(response.data);
|
|
86
86
|
} else {
|
|
87
87
|
reject(new Error(response.error ?? "Failed to fetch chat messages"));
|
|
88
88
|
}
|
|
@@ -93,13 +93,13 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
93
93
|
fetchTaskContext() {
|
|
94
94
|
const socket = this.socket;
|
|
95
95
|
if (!socket) throw new Error("Not connected");
|
|
96
|
-
return new Promise((
|
|
96
|
+
return new Promise((resolve2, reject) => {
|
|
97
97
|
socket.emit(
|
|
98
98
|
"agentRunner:getTaskContext",
|
|
99
99
|
{ taskId: this.config.taskId },
|
|
100
100
|
(response) => {
|
|
101
101
|
if (response.success && response.data) {
|
|
102
|
-
|
|
102
|
+
resolve2(response.data);
|
|
103
103
|
} else {
|
|
104
104
|
reject(new Error(response.error ?? "Failed to fetch task context"));
|
|
105
105
|
}
|
|
@@ -142,13 +142,13 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
142
142
|
createPR(params) {
|
|
143
143
|
const socket = this.socket;
|
|
144
144
|
if (!socket) throw new Error("Not connected");
|
|
145
|
-
return new Promise((
|
|
145
|
+
return new Promise((resolve2, reject) => {
|
|
146
146
|
socket.emit(
|
|
147
147
|
"agentRunner:createPR",
|
|
148
148
|
{ taskId: this.config.taskId, ...params },
|
|
149
149
|
(response) => {
|
|
150
150
|
if (response.success && response.data) {
|
|
151
|
-
|
|
151
|
+
resolve2(response.data);
|
|
152
152
|
} else {
|
|
153
153
|
reject(new Error(response.error ?? "Failed to create pull request"));
|
|
154
154
|
}
|
|
@@ -163,8 +163,8 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
163
163
|
requestId,
|
|
164
164
|
questions
|
|
165
165
|
});
|
|
166
|
-
return new Promise((
|
|
167
|
-
this.pendingQuestionResolvers.set(requestId,
|
|
166
|
+
return new Promise((resolve2) => {
|
|
167
|
+
this.pendingQuestionResolvers.set(requestId, resolve2);
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
cancelPendingQuestions() {
|
|
@@ -224,6 +224,45 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
224
224
|
sendTypingStop() {
|
|
225
225
|
this.sendEvent({ type: "agent_typing_stop" });
|
|
226
226
|
}
|
|
227
|
+
createSubtask(data) {
|
|
228
|
+
const socket = this.socket;
|
|
229
|
+
if (!socket) throw new Error("Not connected");
|
|
230
|
+
return new Promise((resolve2, reject) => {
|
|
231
|
+
socket.emit(
|
|
232
|
+
"agentRunner:createSubtask",
|
|
233
|
+
data,
|
|
234
|
+
(response) => {
|
|
235
|
+
if (response.success && response.data) resolve2(response.data);
|
|
236
|
+
else reject(new Error(response.error ?? "Failed to create subtask"));
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
updateSubtask(subtaskId, fields) {
|
|
242
|
+
if (!this.socket) throw new Error("Not connected");
|
|
243
|
+
this.socket.emit("agentRunner:updateSubtask", {
|
|
244
|
+
subtaskId,
|
|
245
|
+
fields
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
deleteSubtask(subtaskId) {
|
|
249
|
+
if (!this.socket) throw new Error("Not connected");
|
|
250
|
+
this.socket.emit("agentRunner:deleteSubtask", { subtaskId });
|
|
251
|
+
}
|
|
252
|
+
listSubtasks() {
|
|
253
|
+
const socket = this.socket;
|
|
254
|
+
if (!socket) throw new Error("Not connected");
|
|
255
|
+
return new Promise((resolve2, reject) => {
|
|
256
|
+
socket.emit(
|
|
257
|
+
"agentRunner:listSubtasks",
|
|
258
|
+
{},
|
|
259
|
+
(response) => {
|
|
260
|
+
if (response.success && response.data) resolve2(response.data);
|
|
261
|
+
else reject(new Error(response.error ?? "Failed to list subtasks"));
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
227
266
|
disconnect() {
|
|
228
267
|
this.flushEvents();
|
|
229
268
|
this.socket?.disconnect();
|
|
@@ -265,7 +304,7 @@ async function loadConveyorConfig(workspaceDir) {
|
|
|
265
304
|
return null;
|
|
266
305
|
}
|
|
267
306
|
function runSetupCommand(cmd, cwd, onOutput) {
|
|
268
|
-
return new Promise((
|
|
307
|
+
return new Promise((resolve2, reject) => {
|
|
269
308
|
const child = spawn("sh", ["-c", cmd], {
|
|
270
309
|
cwd,
|
|
271
310
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -279,7 +318,7 @@ function runSetupCommand(cmd, cwd, onOutput) {
|
|
|
279
318
|
});
|
|
280
319
|
child.on("close", (code) => {
|
|
281
320
|
if (code === 0) {
|
|
282
|
-
|
|
321
|
+
resolve2();
|
|
283
322
|
} else {
|
|
284
323
|
reject(new Error(`Setup command exited with code ${code}`));
|
|
285
324
|
}
|
|
@@ -349,14 +388,17 @@ function ensureWorktree(projectDir, taskId, branch) {
|
|
|
349
388
|
if (existsSync(worktreePath)) {
|
|
350
389
|
if (branch) {
|
|
351
390
|
try {
|
|
352
|
-
execSync2(`git checkout
|
|
391
|
+
execSync2(`git checkout --detach origin/${branch}`, {
|
|
392
|
+
cwd: worktreePath,
|
|
393
|
+
stdio: "ignore"
|
|
394
|
+
});
|
|
353
395
|
} catch {
|
|
354
396
|
}
|
|
355
397
|
}
|
|
356
398
|
return worktreePath;
|
|
357
399
|
}
|
|
358
|
-
const
|
|
359
|
-
execSync2(`git worktree add "${worktreePath}" ${
|
|
400
|
+
const ref = branch ? `origin/${branch}` : "HEAD";
|
|
401
|
+
execSync2(`git worktree add --detach "${worktreePath}" ${ref}`, {
|
|
360
402
|
cwd: projectDir,
|
|
361
403
|
stdio: "ignore"
|
|
362
404
|
});
|
|
@@ -385,6 +427,7 @@ import { randomUUID } from "crypto";
|
|
|
385
427
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
386
428
|
|
|
387
429
|
// src/prompt-builder.ts
|
|
430
|
+
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["InProgress", "ReviewPR", "ReviewDev", "ReviewLive"]);
|
|
388
431
|
function findLastAgentMessageIndex(history) {
|
|
389
432
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
390
433
|
if (history[i].role === "assistant") return i;
|
|
@@ -394,7 +437,7 @@ function findLastAgentMessageIndex(history) {
|
|
|
394
437
|
function detectRelaunchScenario(context) {
|
|
395
438
|
const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
|
|
396
439
|
if (lastAgentIdx === -1) return "fresh";
|
|
397
|
-
const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId;
|
|
440
|
+
const hasPriorWork = !!context.githubPRUrl || !!context.claudeSessionId || ACTIVE_STATUSES.has(context.status ?? "");
|
|
398
441
|
if (!hasPriorWork) return "fresh";
|
|
399
442
|
const messagesAfterAgent = context.chatHistory.slice(lastAgentIdx + 1);
|
|
400
443
|
const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
|
|
@@ -424,12 +467,13 @@ You are the project manager for this task.`,
|
|
|
424
467
|
const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
|
|
425
468
|
parts.push(
|
|
426
469
|
`You have been relaunched with new feedback.`,
|
|
427
|
-
`Work on the git branch "${context.githubBranch}".`,
|
|
470
|
+
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
428
471
|
`
|
|
429
472
|
New messages since your last run:`,
|
|
430
473
|
...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
|
|
431
474
|
`
|
|
432
|
-
Address the requested changes.
|
|
475
|
+
Address the requested changes. Do NOT re-investigate the codebase from scratch or write a new plan \u2014 review the feedback and implement the changes directly.`,
|
|
476
|
+
`Commit and push your updates.`
|
|
433
477
|
);
|
|
434
478
|
if (context.githubPRUrl) {
|
|
435
479
|
parts.push(
|
|
@@ -443,7 +487,8 @@ Address the requested changes. Commit and push your updates.`
|
|
|
443
487
|
} else {
|
|
444
488
|
parts.push(
|
|
445
489
|
`You were relaunched but no new instructions have been given since your last run.`,
|
|
446
|
-
`Work on the git branch "${context.githubBranch}".`,
|
|
490
|
+
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
491
|
+
`Run \`git log --oneline -10\` to review what you already committed.`,
|
|
447
492
|
`Review the current state of the codebase and verify everything is working correctly.`,
|
|
448
493
|
`Post a brief status update to the chat, then wait for further instructions.`
|
|
449
494
|
);
|
|
@@ -515,7 +560,7 @@ function buildInstructions(mode, context, scenario) {
|
|
|
515
560
|
parts.push(
|
|
516
561
|
`Begin executing the task plan above immediately.`,
|
|
517
562
|
`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.`,
|
|
518
|
-
`Work on the git branch "${context.githubBranch}".`,
|
|
563
|
+
`Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
|
|
519
564
|
`Post a brief message to chat when you begin meaningful implementation, and again when the PR is ready.`,
|
|
520
565
|
`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.`
|
|
521
566
|
);
|
|
@@ -530,9 +575,9 @@ function buildInstructions(mode, context, scenario) {
|
|
|
530
575
|
} else {
|
|
531
576
|
parts.push(
|
|
532
577
|
`You were relaunched but no new instructions have been given since your last run.`,
|
|
533
|
-
`Work on the git branch "${context.githubBranch}".`,
|
|
534
|
-
`
|
|
535
|
-
`Post a brief status update to the chat summarizing
|
|
578
|
+
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
579
|
+
`Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`,
|
|
580
|
+
`Post a brief status update to the chat summarizing where things stand.`,
|
|
536
581
|
`Then wait for further instructions \u2014 do NOT redo work that was already completed.`
|
|
537
582
|
);
|
|
538
583
|
if (context.githubPRUrl) {
|
|
@@ -554,13 +599,15 @@ Review these messages and wait for the team to provide instructions before takin
|
|
|
554
599
|
);
|
|
555
600
|
} else {
|
|
556
601
|
parts.push(
|
|
557
|
-
`You
|
|
558
|
-
`Work on the git branch "${context.githubBranch}".`,
|
|
602
|
+
`You have been relaunched to address feedback on your previous work.`,
|
|
603
|
+
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
604
|
+
`Start by running \`git log --oneline -10\` and \`git diff HEAD~3 HEAD --stat\` to review what you already committed.`,
|
|
559
605
|
`
|
|
560
606
|
New messages since your last run:`,
|
|
561
607
|
...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
|
|
562
608
|
`
|
|
563
|
-
Address the requested changes.
|
|
609
|
+
Address the requested changes directly. Do NOT re-investigate the codebase from scratch or write a new plan \u2014 go straight to implementing the feedback.`,
|
|
610
|
+
`Commit and push your updates.`
|
|
564
611
|
);
|
|
565
612
|
if (context.githubPRUrl) {
|
|
566
613
|
parts.push(
|
|
@@ -617,7 +664,14 @@ IMPORTANT \u2014 Skip all environment verification. Do NOT run any of the follow
|
|
|
617
664
|
`- bun dev, npm start, or any dev server startup commands`,
|
|
618
665
|
`- pwd, ls, echo, or exploratory shell commands to "check" the environment`,
|
|
619
666
|
`Only run these if you encounter a specific error that requires it.`,
|
|
620
|
-
`Start reading the task plan and writing code immediately
|
|
667
|
+
`Start reading the task plan and writing code immediately.`,
|
|
668
|
+
`
|
|
669
|
+
Git safety \u2014 STRICT rules:`,
|
|
670
|
+
`- NEVER run \`git checkout main\`, \`git checkout dev\`, or switch to any branch other than \`${context.githubBranch}\`.`,
|
|
671
|
+
`- NEVER create new branches (no \`git checkout -b\`, \`git switch -c\`, etc.).`,
|
|
672
|
+
`- This branch was created from \`${context.baseBranch}\`. PRs will automatically target that branch.`,
|
|
673
|
+
`- If \`git push\` fails with "non-fast-forward" or similar, run \`git pull --rebase origin ${context.githubBranch}\` and retry. If that also fails, use \`git push --force-with-lease origin ${context.githubBranch}\`.`,
|
|
674
|
+
`- If you encounter merge conflicts during rebase, resolve them in place \u2014 do NOT abandon the branch.`
|
|
621
675
|
];
|
|
622
676
|
if (setupLog.length > 0) {
|
|
623
677
|
parts.push(
|
|
@@ -675,7 +729,7 @@ function buildCommonTools(connection, config) {
|
|
|
675
729
|
);
|
|
676
730
|
}
|
|
677
731
|
},
|
|
678
|
-
{ annotations: {
|
|
732
|
+
{ annotations: { readOnlyHint: true } }
|
|
679
733
|
),
|
|
680
734
|
tool(
|
|
681
735
|
"post_to_chat",
|
|
@@ -709,7 +763,7 @@ function buildCommonTools(connection, config) {
|
|
|
709
763
|
return textResult(`Task ID: ${config.taskId} - could not fetch updated plan.`);
|
|
710
764
|
}
|
|
711
765
|
},
|
|
712
|
-
{ annotations: {
|
|
766
|
+
{ annotations: { readOnlyHint: true } }
|
|
713
767
|
)
|
|
714
768
|
];
|
|
715
769
|
}
|
|
@@ -730,6 +784,74 @@ function buildPmTools(connection) {
|
|
|
730
784
|
return textResult("Failed to update task.");
|
|
731
785
|
}
|
|
732
786
|
}
|
|
787
|
+
),
|
|
788
|
+
tool(
|
|
789
|
+
"create_subtask",
|
|
790
|
+
"Create a subtask under the current parent task. Use for breaking complex tasks into smaller pieces.",
|
|
791
|
+
{
|
|
792
|
+
title: z.string().describe("Subtask title"),
|
|
793
|
+
description: z.string().optional().describe("Brief description"),
|
|
794
|
+
plan: z.string().optional().describe("Implementation plan in markdown"),
|
|
795
|
+
ordinal: z.number().optional().describe("Step/order number (0-based)"),
|
|
796
|
+
storyPointValue: z.number().optional().describe("Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique)")
|
|
797
|
+
},
|
|
798
|
+
async (params) => {
|
|
799
|
+
try {
|
|
800
|
+
const result = await connection.createSubtask(params);
|
|
801
|
+
return textResult(`Subtask created with ID: ${result.id}`);
|
|
802
|
+
} catch (error) {
|
|
803
|
+
return textResult(
|
|
804
|
+
`Failed to create subtask: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
),
|
|
809
|
+
tool(
|
|
810
|
+
"update_subtask",
|
|
811
|
+
"Update an existing subtask's fields",
|
|
812
|
+
{
|
|
813
|
+
subtaskId: z.string().describe("The subtask ID to update"),
|
|
814
|
+
title: z.string().optional(),
|
|
815
|
+
description: z.string().optional(),
|
|
816
|
+
plan: z.string().optional(),
|
|
817
|
+
ordinal: z.number().optional(),
|
|
818
|
+
storyPointValue: z.number().optional()
|
|
819
|
+
},
|
|
820
|
+
async ({ subtaskId, ...fields }) => {
|
|
821
|
+
try {
|
|
822
|
+
await Promise.resolve(connection.updateSubtask(subtaskId, fields));
|
|
823
|
+
return textResult("Subtask updated.");
|
|
824
|
+
} catch (error) {
|
|
825
|
+
return textResult(`Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
),
|
|
829
|
+
tool(
|
|
830
|
+
"delete_subtask",
|
|
831
|
+
"Delete a subtask",
|
|
832
|
+
{ subtaskId: z.string().describe("The subtask ID to delete") },
|
|
833
|
+
async ({ subtaskId }) => {
|
|
834
|
+
try {
|
|
835
|
+
await Promise.resolve(connection.deleteSubtask(subtaskId));
|
|
836
|
+
return textResult("Subtask deleted.");
|
|
837
|
+
} catch (error) {
|
|
838
|
+
return textResult(`Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
),
|
|
842
|
+
tool(
|
|
843
|
+
"list_subtasks",
|
|
844
|
+
"List all subtasks under the current parent task",
|
|
845
|
+
{},
|
|
846
|
+
async () => {
|
|
847
|
+
try {
|
|
848
|
+
const subtasks = await connection.listSubtasks();
|
|
849
|
+
return textResult(JSON.stringify(subtasks, null, 2));
|
|
850
|
+
} catch {
|
|
851
|
+
return textResult("Failed to list subtasks.");
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
{ annotations: { readOnlyHint: true } }
|
|
733
855
|
)
|
|
734
856
|
];
|
|
735
857
|
}
|
|
@@ -812,6 +934,7 @@ async function processAssistantEvent(event, host, turnToolCalls) {
|
|
|
812
934
|
function handleResultEvent(event, host, context, startTime) {
|
|
813
935
|
const resultEvent = event;
|
|
814
936
|
let totalCostUsd = 0;
|
|
937
|
+
let deltaCost = 0;
|
|
815
938
|
let retriable = false;
|
|
816
939
|
if (resultEvent.subtype === "success") {
|
|
817
940
|
totalCostUsd = "total_cost_usd" in resultEvent ? resultEvent.total_cost_usd : 0;
|
|
@@ -820,15 +943,18 @@ function handleResultEvent(event, host, context, startTime) {
|
|
|
820
943
|
if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
|
|
821
944
|
retriable = true;
|
|
822
945
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
946
|
+
const lastCost = context._lastReportedCostUsd ?? 0;
|
|
947
|
+
deltaCost = totalCostUsd - lastCost;
|
|
948
|
+
context._lastReportedCostUsd = totalCostUsd;
|
|
949
|
+
host.connection.sendEvent({ type: "completed", summary, costUsd: deltaCost, durationMs });
|
|
950
|
+
if (deltaCost > 0 && context.agentId) {
|
|
951
|
+
const estimatedDeltaTokens = Math.round(deltaCost * 1e5);
|
|
826
952
|
host.connection.trackSpending({
|
|
827
953
|
agentId: context.agentId,
|
|
828
|
-
inputTokens: Math.round(
|
|
829
|
-
outputTokens: Math.round(
|
|
830
|
-
totalTokens:
|
|
831
|
-
totalCostUsd,
|
|
954
|
+
inputTokens: Math.round(estimatedDeltaTokens * 0.7),
|
|
955
|
+
outputTokens: Math.round(estimatedDeltaTokens * 0.3),
|
|
956
|
+
totalTokens: estimatedDeltaTokens,
|
|
957
|
+
totalCostUsd: deltaCost,
|
|
832
958
|
onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
833
959
|
});
|
|
834
960
|
}
|
|
@@ -840,16 +966,16 @@ function handleResultEvent(event, host, context, startTime) {
|
|
|
840
966
|
}
|
|
841
967
|
host.connection.sendEvent({ type: "error", message: errorMsg });
|
|
842
968
|
}
|
|
843
|
-
return { totalCostUsd, retriable };
|
|
969
|
+
return { totalCostUsd, deltaCost, retriable };
|
|
844
970
|
}
|
|
845
971
|
async function emitResultEvent(event, host, context, startTime) {
|
|
846
972
|
const result = handleResultEvent(event, host, context, startTime);
|
|
847
973
|
const durationMs = Date.now() - startTime;
|
|
848
|
-
if (result.
|
|
974
|
+
if (result.deltaCost > 0 && context.agentId) {
|
|
849
975
|
await host.callbacks.onEvent({
|
|
850
976
|
type: "completed",
|
|
851
977
|
summary: "Task completed.",
|
|
852
|
-
costUsd: result.
|
|
978
|
+
costUsd: result.deltaCost,
|
|
853
979
|
durationMs
|
|
854
980
|
});
|
|
855
981
|
} else {
|
|
@@ -885,6 +1011,9 @@ async function processEvents(events, context, host) {
|
|
|
885
1011
|
if (sessionId && !sessionIdStored) {
|
|
886
1012
|
sessionIdStored = true;
|
|
887
1013
|
host.connection.storeSessionId(sessionId);
|
|
1014
|
+
if (sessionId !== context.claudeSessionId) {
|
|
1015
|
+
context._lastReportedCostUsd = 0;
|
|
1016
|
+
}
|
|
888
1017
|
}
|
|
889
1018
|
await host.callbacks.onEvent({
|
|
890
1019
|
type: "thinking",
|
|
@@ -941,8 +1070,8 @@ function buildCanUseTool(host) {
|
|
|
941
1070
|
input: JSON.stringify(input)
|
|
942
1071
|
});
|
|
943
1072
|
const answerPromise = host.connection.askUserQuestion(requestId, questions);
|
|
944
|
-
const timeoutPromise = new Promise((
|
|
945
|
-
setTimeout(() =>
|
|
1073
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
1074
|
+
setTimeout(() => resolve2(null), QUESTION_TIMEOUT_MS);
|
|
946
1075
|
});
|
|
947
1076
|
const answers = await Promise.race([answerPromise, timeoutPromise]);
|
|
948
1077
|
host.connection.emitStatus("running");
|
|
@@ -1020,23 +1149,27 @@ ${followUpContent}` : followUpContent;
|
|
|
1020
1149
|
async function runWithRetry(initialQuery, context, host, options) {
|
|
1021
1150
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
1022
1151
|
if (host.isStopped()) return;
|
|
1023
|
-
const agentQuery = attempt === 0 ? initialQuery :
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1152
|
+
const agentQuery = attempt === 0 ? initialQuery : (() => {
|
|
1153
|
+
context._lastReportedCostUsd = 0;
|
|
1154
|
+
return query({
|
|
1155
|
+
prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
|
|
1156
|
+
options: { ...options, resume: void 0 }
|
|
1157
|
+
});
|
|
1158
|
+
})();
|
|
1027
1159
|
try {
|
|
1028
1160
|
const { retriable } = await processEvents(agentQuery, context, host);
|
|
1029
1161
|
if (!retriable || host.isStopped()) return;
|
|
1030
1162
|
} catch (error) {
|
|
1031
1163
|
const isStaleSession = error instanceof Error && error.message.includes("No conversation found with session ID");
|
|
1032
1164
|
if (isStaleSession && context.claudeSessionId) {
|
|
1165
|
+
context.claudeSessionId = null;
|
|
1166
|
+
context._lastReportedCostUsd = 0;
|
|
1033
1167
|
host.connection.storeSessionId("");
|
|
1034
|
-
const freshCtx = { ...context, claudeSessionId: null };
|
|
1035
1168
|
const freshQuery = query({
|
|
1036
|
-
prompt: host.createInputStream(buildInitialPrompt(host.config.mode,
|
|
1169
|
+
prompt: host.createInputStream(buildInitialPrompt(host.config.mode, context)),
|
|
1037
1170
|
options: { ...options, resume: void 0 }
|
|
1038
1171
|
});
|
|
1039
|
-
return runWithRetry(freshQuery,
|
|
1172
|
+
return runWithRetry(freshQuery, context, host, options);
|
|
1040
1173
|
}
|
|
1041
1174
|
const isApiError = error instanceof Error && API_ERROR_PATTERN.test(error.message);
|
|
1042
1175
|
if (!isApiError) throw error;
|
|
@@ -1058,13 +1191,13 @@ async function runWithRetry(initialQuery, context, host, options) {
|
|
|
1058
1191
|
});
|
|
1059
1192
|
host.connection.emitStatus("waiting_for_input");
|
|
1060
1193
|
await host.callbacks.onStatusChange("waiting_for_input");
|
|
1061
|
-
await new Promise((
|
|
1062
|
-
const timer = setTimeout(
|
|
1194
|
+
await new Promise((resolve2) => {
|
|
1195
|
+
const timer = setTimeout(resolve2, delayMs);
|
|
1063
1196
|
const checkStopped = setInterval(() => {
|
|
1064
1197
|
if (host.isStopped()) {
|
|
1065
1198
|
clearTimeout(timer);
|
|
1066
1199
|
clearInterval(checkStopped);
|
|
1067
|
-
|
|
1200
|
+
resolve2();
|
|
1068
1201
|
}
|
|
1069
1202
|
}, 1e3);
|
|
1070
1203
|
setTimeout(() => clearInterval(checkStopped), delayMs + 100);
|
|
@@ -1160,6 +1293,9 @@ var AgentRunner = class _AgentRunner {
|
|
|
1160
1293
|
this.connection.disconnect();
|
|
1161
1294
|
return;
|
|
1162
1295
|
}
|
|
1296
|
+
if (this.taskContext.claudeSessionId && this.taskContext._existingSpendingTotal !== null) {
|
|
1297
|
+
this.taskContext._lastReportedCostUsd = this.taskContext._existingSpendingTotal;
|
|
1298
|
+
}
|
|
1163
1299
|
if (process.env.CODESPACES === "true" && this.taskContext.baseBranch) {
|
|
1164
1300
|
const result = cleanDevcontainerFromGit(
|
|
1165
1301
|
this.config.workspaceDir,
|
|
@@ -1328,25 +1464,25 @@ ${f.content}
|
|
|
1328
1464
|
parent_tool_use_id: null
|
|
1329
1465
|
};
|
|
1330
1466
|
if (this.inputResolver) {
|
|
1331
|
-
const
|
|
1467
|
+
const resolve2 = this.inputResolver;
|
|
1332
1468
|
this.inputResolver = null;
|
|
1333
|
-
|
|
1469
|
+
resolve2(msg);
|
|
1334
1470
|
} else {
|
|
1335
1471
|
this.pendingMessages.push(msg);
|
|
1336
1472
|
}
|
|
1337
1473
|
}
|
|
1338
1474
|
waitForMessage() {
|
|
1339
|
-
return new Promise((
|
|
1475
|
+
return new Promise((resolve2) => {
|
|
1340
1476
|
const checkStopped = setInterval(() => {
|
|
1341
1477
|
if (this.stopped) {
|
|
1342
1478
|
clearInterval(checkStopped);
|
|
1343
1479
|
this.inputResolver = null;
|
|
1344
|
-
|
|
1480
|
+
resolve2(null);
|
|
1345
1481
|
}
|
|
1346
1482
|
}, 1e3);
|
|
1347
1483
|
this.inputResolver = (msg) => {
|
|
1348
1484
|
clearInterval(checkStopped);
|
|
1349
|
-
|
|
1485
|
+
resolve2(msg);
|
|
1350
1486
|
};
|
|
1351
1487
|
});
|
|
1352
1488
|
}
|
|
@@ -1478,6 +1614,275 @@ ${f.content}
|
|
|
1478
1614
|
}
|
|
1479
1615
|
};
|
|
1480
1616
|
|
|
1617
|
+
// src/project-connection.ts
|
|
1618
|
+
import { io as io2 } from "socket.io-client";
|
|
1619
|
+
var ProjectConnection = class {
|
|
1620
|
+
socket = null;
|
|
1621
|
+
config;
|
|
1622
|
+
taskAssignmentCallback = null;
|
|
1623
|
+
stopTaskCallback = null;
|
|
1624
|
+
shutdownCallback = null;
|
|
1625
|
+
constructor(config) {
|
|
1626
|
+
this.config = config;
|
|
1627
|
+
}
|
|
1628
|
+
connect() {
|
|
1629
|
+
return new Promise((resolve2, reject) => {
|
|
1630
|
+
let settled = false;
|
|
1631
|
+
let attempts = 0;
|
|
1632
|
+
const maxInitialAttempts = 30;
|
|
1633
|
+
this.socket = io2(this.config.apiUrl, {
|
|
1634
|
+
auth: { projectToken: this.config.projectToken },
|
|
1635
|
+
transports: ["websocket"],
|
|
1636
|
+
reconnection: true,
|
|
1637
|
+
reconnectionAttempts: Infinity,
|
|
1638
|
+
reconnectionDelay: 2e3,
|
|
1639
|
+
reconnectionDelayMax: 3e4,
|
|
1640
|
+
randomizationFactor: 0.3,
|
|
1641
|
+
extraHeaders: {
|
|
1642
|
+
"ngrok-skip-browser-warning": "true"
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
this.socket.on("projectRunner:assignTask", (data) => {
|
|
1646
|
+
if (this.taskAssignmentCallback) {
|
|
1647
|
+
this.taskAssignmentCallback(data);
|
|
1648
|
+
}
|
|
1649
|
+
});
|
|
1650
|
+
this.socket.on("projectRunner:stopTask", (data) => {
|
|
1651
|
+
if (this.stopTaskCallback) {
|
|
1652
|
+
this.stopTaskCallback(data);
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
this.socket.on("projectRunner:shutdown", () => {
|
|
1656
|
+
if (this.shutdownCallback) {
|
|
1657
|
+
this.shutdownCallback();
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
this.socket.on("connect", () => {
|
|
1661
|
+
if (!settled) {
|
|
1662
|
+
settled = true;
|
|
1663
|
+
resolve2();
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
this.socket.io.on("reconnect_attempt", () => {
|
|
1667
|
+
attempts++;
|
|
1668
|
+
if (!settled && attempts >= maxInitialAttempts) {
|
|
1669
|
+
settled = true;
|
|
1670
|
+
reject(new Error(`Failed to connect after ${maxInitialAttempts} attempts`));
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
onTaskAssignment(callback) {
|
|
1676
|
+
this.taskAssignmentCallback = callback;
|
|
1677
|
+
}
|
|
1678
|
+
onStopTask(callback) {
|
|
1679
|
+
this.stopTaskCallback = callback;
|
|
1680
|
+
}
|
|
1681
|
+
onShutdown(callback) {
|
|
1682
|
+
this.shutdownCallback = callback;
|
|
1683
|
+
}
|
|
1684
|
+
sendHeartbeat() {
|
|
1685
|
+
if (!this.socket) return;
|
|
1686
|
+
this.socket.emit("projectRunner:heartbeat", {});
|
|
1687
|
+
}
|
|
1688
|
+
emitTaskStarted(taskId) {
|
|
1689
|
+
if (!this.socket) return;
|
|
1690
|
+
this.socket.emit("projectRunner:taskStarted", { taskId });
|
|
1691
|
+
}
|
|
1692
|
+
emitTaskStopped(taskId, reason) {
|
|
1693
|
+
if (!this.socket) return;
|
|
1694
|
+
this.socket.emit("projectRunner:taskStopped", { taskId, reason });
|
|
1695
|
+
}
|
|
1696
|
+
disconnect() {
|
|
1697
|
+
this.socket?.disconnect();
|
|
1698
|
+
this.socket = null;
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
// src/project-runner.ts
|
|
1703
|
+
import { fork } from "child_process";
|
|
1704
|
+
import { execSync as execSync4 } from "child_process";
|
|
1705
|
+
import * as path from "path";
|
|
1706
|
+
import { fileURLToPath } from "url";
|
|
1707
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
1708
|
+
var __dirname = path.dirname(__filename);
|
|
1709
|
+
var HEARTBEAT_INTERVAL_MS2 = 3e4;
|
|
1710
|
+
var MAX_CONCURRENT = 3;
|
|
1711
|
+
var STOP_TIMEOUT_MS = 3e4;
|
|
1712
|
+
var ProjectRunner = class {
|
|
1713
|
+
connection;
|
|
1714
|
+
projectDir;
|
|
1715
|
+
activeAgents = /* @__PURE__ */ new Map();
|
|
1716
|
+
heartbeatTimer = null;
|
|
1717
|
+
stopping = false;
|
|
1718
|
+
resolveLifecycle = null;
|
|
1719
|
+
constructor(config) {
|
|
1720
|
+
this.projectDir = config.projectDir;
|
|
1721
|
+
this.connection = new ProjectConnection({
|
|
1722
|
+
apiUrl: config.conveyorApiUrl,
|
|
1723
|
+
projectToken: config.projectToken,
|
|
1724
|
+
projectId: config.projectId
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
async start() {
|
|
1728
|
+
await this.connection.connect();
|
|
1729
|
+
this.connection.onTaskAssignment((assignment) => {
|
|
1730
|
+
void this.handleAssignment(assignment);
|
|
1731
|
+
});
|
|
1732
|
+
this.connection.onStopTask((data) => {
|
|
1733
|
+
this.handleStopTask(data.taskId);
|
|
1734
|
+
});
|
|
1735
|
+
this.connection.onShutdown(() => {
|
|
1736
|
+
console.log("[project-runner] Received shutdown signal from server");
|
|
1737
|
+
void this.stop();
|
|
1738
|
+
});
|
|
1739
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1740
|
+
this.connection.sendHeartbeat();
|
|
1741
|
+
}, HEARTBEAT_INTERVAL_MS2);
|
|
1742
|
+
console.log("[project-runner] Connected, waiting for task assignments...");
|
|
1743
|
+
await new Promise((resolve2) => {
|
|
1744
|
+
this.resolveLifecycle = resolve2;
|
|
1745
|
+
process.on("SIGTERM", () => void this.stop());
|
|
1746
|
+
process.on("SIGINT", () => void this.stop());
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
async handleAssignment(assignment) {
|
|
1750
|
+
const { taskId, taskToken, apiUrl, mode, branch, devBranch } = assignment;
|
|
1751
|
+
const shortId = taskId.slice(0, 8);
|
|
1752
|
+
if (this.activeAgents.has(taskId)) {
|
|
1753
|
+
console.log(`[project-runner] Task ${shortId} already running, skipping`);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (this.activeAgents.size >= MAX_CONCURRENT) {
|
|
1757
|
+
console.log(
|
|
1758
|
+
`[project-runner] Max concurrent agents (${MAX_CONCURRENT}) reached, rejecting task ${shortId}`
|
|
1759
|
+
);
|
|
1760
|
+
this.connection.emitTaskStopped(taskId, "max_concurrent_reached");
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
try {
|
|
1764
|
+
try {
|
|
1765
|
+
execSync4("git fetch origin", { cwd: this.projectDir, stdio: "ignore" });
|
|
1766
|
+
} catch {
|
|
1767
|
+
console.log(`[task:${shortId}] Warning: git fetch failed`);
|
|
1768
|
+
}
|
|
1769
|
+
const worktreePath = ensureWorktree(this.projectDir, taskId, devBranch);
|
|
1770
|
+
if (branch && branch !== devBranch) {
|
|
1771
|
+
try {
|
|
1772
|
+
execSync4(`git checkout ${branch}`, { cwd: worktreePath, stdio: "ignore" });
|
|
1773
|
+
} catch {
|
|
1774
|
+
try {
|
|
1775
|
+
execSync4(`git checkout -b ${branch}`, { cwd: worktreePath, stdio: "ignore" });
|
|
1776
|
+
} catch {
|
|
1777
|
+
console.log(`[task:${shortId}] Warning: could not checkout branch ${branch}`);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
const cliPath = path.resolve(__dirname, "cli.js");
|
|
1782
|
+
const child = fork(cliPath, [], {
|
|
1783
|
+
env: {
|
|
1784
|
+
...process.env,
|
|
1785
|
+
CONVEYOR_API_URL: apiUrl,
|
|
1786
|
+
CONVEYOR_TASK_TOKEN: taskToken,
|
|
1787
|
+
CONVEYOR_TASK_ID: taskId,
|
|
1788
|
+
CONVEYOR_MODE: mode,
|
|
1789
|
+
CONVEYOR_WORKSPACE: worktreePath,
|
|
1790
|
+
CONVEYOR_USE_WORKTREE: "false"
|
|
1791
|
+
},
|
|
1792
|
+
cwd: worktreePath,
|
|
1793
|
+
stdio: ["pipe", "pipe", "pipe", "ipc"]
|
|
1794
|
+
});
|
|
1795
|
+
child.stdout?.on("data", (data) => {
|
|
1796
|
+
const lines = data.toString().trimEnd().split("\n");
|
|
1797
|
+
for (const line of lines) {
|
|
1798
|
+
console.log(`[task:${shortId}] ${line}`);
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
child.stderr?.on("data", (data) => {
|
|
1802
|
+
const lines = data.toString().trimEnd().split("\n");
|
|
1803
|
+
for (const line of lines) {
|
|
1804
|
+
console.error(`[task:${shortId}] ${line}`);
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
this.activeAgents.set(taskId, { process: child, worktreePath, mode });
|
|
1808
|
+
this.connection.emitTaskStarted(taskId);
|
|
1809
|
+
console.log(`[project-runner] Started task ${shortId} in ${mode} mode at ${worktreePath}`);
|
|
1810
|
+
child.on("exit", (code) => {
|
|
1811
|
+
this.activeAgents.delete(taskId);
|
|
1812
|
+
const reason = code === 0 ? "completed" : `exited with code ${code}`;
|
|
1813
|
+
this.connection.emitTaskStopped(taskId, reason);
|
|
1814
|
+
console.log(`[project-runner] Task ${shortId} ${reason}`);
|
|
1815
|
+
if (code === 0) {
|
|
1816
|
+
try {
|
|
1817
|
+
removeWorktree(this.projectDir, taskId);
|
|
1818
|
+
} catch {
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
console.error(
|
|
1824
|
+
`[project-runner] Failed to start task ${shortId}:`,
|
|
1825
|
+
error instanceof Error ? error.message : error
|
|
1826
|
+
);
|
|
1827
|
+
this.connection.emitTaskStopped(
|
|
1828
|
+
taskId,
|
|
1829
|
+
`start_failed: ${error instanceof Error ? error.message : "Unknown"}`
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
handleStopTask(taskId) {
|
|
1834
|
+
const agent = this.activeAgents.get(taskId);
|
|
1835
|
+
if (!agent) return;
|
|
1836
|
+
const shortId = taskId.slice(0, 8);
|
|
1837
|
+
console.log(`[project-runner] Stopping task ${shortId}`);
|
|
1838
|
+
agent.process.kill("SIGTERM");
|
|
1839
|
+
const timer = setTimeout(() => {
|
|
1840
|
+
if (this.activeAgents.has(taskId)) {
|
|
1841
|
+
agent.process.kill("SIGKILL");
|
|
1842
|
+
}
|
|
1843
|
+
}, STOP_TIMEOUT_MS);
|
|
1844
|
+
agent.process.on("exit", () => {
|
|
1845
|
+
clearTimeout(timer);
|
|
1846
|
+
try {
|
|
1847
|
+
removeWorktree(this.projectDir, taskId);
|
|
1848
|
+
} catch {
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
async stop() {
|
|
1853
|
+
if (this.stopping) return;
|
|
1854
|
+
this.stopping = true;
|
|
1855
|
+
console.log("[project-runner] Shutting down...");
|
|
1856
|
+
if (this.heartbeatTimer) {
|
|
1857
|
+
clearInterval(this.heartbeatTimer);
|
|
1858
|
+
this.heartbeatTimer = null;
|
|
1859
|
+
}
|
|
1860
|
+
const stopPromises = [...this.activeAgents.keys()].map(
|
|
1861
|
+
(taskId) => new Promise((resolve2) => {
|
|
1862
|
+
const agent = this.activeAgents.get(taskId);
|
|
1863
|
+
if (!agent) {
|
|
1864
|
+
resolve2();
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
agent.process.on("exit", () => {
|
|
1868
|
+
resolve2();
|
|
1869
|
+
});
|
|
1870
|
+
this.handleStopTask(taskId);
|
|
1871
|
+
})
|
|
1872
|
+
);
|
|
1873
|
+
await Promise.race([
|
|
1874
|
+
Promise.all(stopPromises),
|
|
1875
|
+
new Promise((resolve2) => setTimeout(resolve2, 6e4))
|
|
1876
|
+
]);
|
|
1877
|
+
this.connection.disconnect();
|
|
1878
|
+
console.log("[project-runner] Shutdown complete");
|
|
1879
|
+
if (this.resolveLifecycle) {
|
|
1880
|
+
this.resolveLifecycle();
|
|
1881
|
+
this.resolveLifecycle = null;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
|
|
1481
1886
|
export {
|
|
1482
1887
|
ConveyorConnection,
|
|
1483
1888
|
loadConveyorConfig,
|
|
@@ -1485,6 +1890,8 @@ export {
|
|
|
1485
1890
|
runStartCommand,
|
|
1486
1891
|
ensureWorktree,
|
|
1487
1892
|
removeWorktree,
|
|
1488
|
-
AgentRunner
|
|
1893
|
+
AgentRunner,
|
|
1894
|
+
ProjectConnection,
|
|
1895
|
+
ProjectRunner
|
|
1489
1896
|
};
|
|
1490
|
-
//# sourceMappingURL=chunk-
|
|
1897
|
+
//# sourceMappingURL=chunk-N6QGELGE.js.map
|