@elizaos/plugin-agent-orchestrator 0.3.19 → 0.4.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/actions/start-coding-task.d.ts.map +1 -1
- package/dist/index.js +367 -133
- package/dist/index.js.map +10 -9
- package/dist/services/ansi-utils.d.ts.map +1 -1
- package/dist/services/session-event-queue.d.ts +25 -0
- package/dist/services/session-event-queue.d.ts.map +1 -0
- package/dist/services/swarm-coordinator-prompts.d.ts.map +1 -1
- package/dist/services/swarm-coordinator.d.ts +24 -0
- package/dist/services/swarm-coordinator.d.ts.map +1 -1
- package/dist/services/swarm-decision-loop.d.ts.map +1 -1
- package/dist/services/swarm-history.d.ts +27 -0
- package/dist/services/swarm-history.d.ts.map +1 -0
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -34,8 +34,14 @@ function cleanForChat(raw) {
|
|
|
34
34
|
return false;
|
|
35
35
|
if (STATUS_LINE.test(trimmed))
|
|
36
36
|
return false;
|
|
37
|
+
if (TOOL_MARKER_LINE.test(trimmed))
|
|
38
|
+
return false;
|
|
39
|
+
if (GIT_NOISE_LINE.test(trimmed))
|
|
40
|
+
return false;
|
|
37
41
|
if (!/[a-zA-Z0-9]/.test(trimmed))
|
|
38
42
|
return false;
|
|
43
|
+
if (trimmed.length <= 3)
|
|
44
|
+
return false;
|
|
39
45
|
return true;
|
|
40
46
|
}).map((line) => line.replace(/ {2,}/g, " ").trim()).filter((line) => line.length > 0).join(`
|
|
41
47
|
`).replace(/\n{3,}/g, `
|
|
@@ -83,7 +89,7 @@ function captureTaskResponse(sessionId, buffers, markers) {
|
|
|
83
89
|
return cleanForChat(responseLines.join(`
|
|
84
90
|
`));
|
|
85
91
|
}
|
|
86
|
-
var CURSOR_MOVEMENT, CURSOR_POSITION, ERASE, OSC, ALL_ANSI, CONTROL_CHARS, ORPHAN_SGR, LONG_SPACES, TUI_DECORATIVE, LOADING_LINE, STATUS_LINE;
|
|
92
|
+
var CURSOR_MOVEMENT, CURSOR_POSITION, ERASE, OSC, ALL_ANSI, CONTROL_CHARS, ORPHAN_SGR, LONG_SPACES, TUI_DECORATIVE, LOADING_LINE, STATUS_LINE, TOOL_MARKER_LINE, GIT_NOISE_LINE;
|
|
87
93
|
var init_ansi_utils = __esm(() => {
|
|
88
94
|
CURSOR_MOVEMENT = /\x1b\[\d*[CDABGdEF]/g;
|
|
89
95
|
CURSOR_POSITION = /\x1b\[\d*(?:;\d+)?[Hf]/g;
|
|
@@ -94,8 +100,10 @@ var init_ansi_utils = __esm(() => {
|
|
|
94
100
|
ORPHAN_SGR = /\[[\d;]*m/g;
|
|
95
101
|
LONG_SPACES = / {3,}/g;
|
|
96
102
|
TUI_DECORATIVE = /[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❮❯▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✽✻✶✳✢⏺←→↑↓⬆⬇◆▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥·⎿✔◼]/g;
|
|
97
|
-
LOADING_LINE = /^\s*(?:thinking|
|
|
103
|
+
LOADING_LINE = /^\s*(?:[A-Z][a-z]+(?:-[a-z]+)?(?:ing|ed)\w*|thinking|Loading|processing)(?:…|\.{3})(?:\s*\(.*\)|\s+for\s+\d+[smh](?:\s+\d+[smh])*)?\s*$|^\s*(?:[A-Z][a-z]+(?:-[a-z]+)?(?:ing|ed)\w*|thinking|Loading|processing)\s+for\s+\d+[smh](?:\s+\d+[smh])*\s*$/;
|
|
98
104
|
STATUS_LINE = /^\s*(?:\d+[smh]\s+\d+s?\s*·|↓\s*[\d.]+k?\s*tokens|·\s*↓|esc\s+to\s+interrupt|[Uu]pdate available|ate available|Run:\s+brew|brew\s+upgrade|\d+\s+files?\s+\+\d+\s+-\d+|ctrl\+\w|\+\d+\s+lines|Wrote\s+\d+\s+lines\s+to|\?\s+for\s+shortcuts|Cooked for|Baked for|Cogitated for)/i;
|
|
105
|
+
TOOL_MARKER_LINE = /^\s*(?:Bash|Write|Read|Edit|Glob|Grep|Search|TodoWrite|Agent)\s*\(.*\)\s*$/;
|
|
106
|
+
GIT_NOISE_LINE = /^\s*(?:On branch\s+\w|Your branch is|modified:|new file:|deleted:|renamed:|Untracked files:|Changes (?:not staged|to be committed)|\d+\s+files?\s+changed.*(?:insertion|deletion))/i;
|
|
99
107
|
});
|
|
100
108
|
|
|
101
109
|
// src/services/trajectory-context.ts
|
|
@@ -191,8 +199,7 @@ ${recentOutput.slice(-3000)}
|
|
|
191
199
|
` + `- For Y/n confirmations that align with the original task, respond "y".
|
|
192
200
|
` + `- For design questions or choices that could go either way, escalate.
|
|
193
201
|
` + `- For error recovery prompts, try to respond if the path forward is clear.
|
|
194
|
-
` + `- If the output shows a PR was just created (e.g. "Created pull request #N"),
|
|
195
|
-
` + `- Only use "complete" if the agent confirmed it verified ALL test plan items after creating the PR.
|
|
202
|
+
` + `- If the output shows a PR was just created (e.g. "Created pull request #N"), use "complete" — the task is done.
|
|
196
203
|
` + `- If the agent is asking for information that was NOT provided in the original task ` + `(e.g. which repository to use, project requirements, credentials), ESCALATE. ` + `The coordinator does not have this information — the human must provide it.
|
|
197
204
|
` + `- When in doubt, escalate — it's better to ask the human than to make a wrong choice.
|
|
198
205
|
` + `- If the agent's output reveals a significant decision that sibling agents should know about ` + `(e.g. chose a library, designed an API shape, picked a UI pattern, established a writing style, ` + `narrowed a research scope, made any choice that affects the shared project), ` + `include "keyDecision" with a brief one-line summary. Skip this for routine tool approvals.
|
|
@@ -260,35 +267,24 @@ Output from this turn:
|
|
|
260
267
|
${turnOutput.slice(-3000)}
|
|
261
268
|
---
|
|
262
269
|
|
|
263
|
-
` + `The agent completed a turn. Decide if the
|
|
264
|
-
|
|
265
|
-
` + `IMPORTANT: Coding agents work in multiple turns. A single turn completing does NOT mean ` + `the task is done. You must verify that EVERY objective in the original task has been addressed ` + `in the output before declaring "complete".
|
|
266
|
-
|
|
267
|
-
` + `Your options:
|
|
268
|
-
|
|
269
|
-
` + `1. "respond" — The agent finished a step but the overall task is NOT done yet. ` + `Send a follow-up instruction to continue. Set "response" to the next instruction ` + `(e.g. "Now run the tests", "Create a PR with these changes", "Continue with the next part"). ` + `THIS IS THE DEFAULT — most turns are intermediate steps, not the final result.
|
|
270
|
-
|
|
271
|
-
` + `2. "complete" — The original task objectives have ALL been fully met. For repo-based tasks, ` + `this means code was written, changes were committed, pushed, AND a pull request was created. ` + `Only use this when you can point to specific evidence in the output for EVERY objective ` + `(e.g. "Created pull request #N" in the output).
|
|
272
|
-
|
|
273
|
-
` + `3. "escalate" — Something looks wrong or you're unsure whether the task is complete. ` + `Let the human decide.
|
|
274
|
-
|
|
275
|
-
` + `4. "ignore" — Should not normally be used here.
|
|
270
|
+
` + `The agent completed a turn. Decide if the task is done or needs more work.
|
|
276
271
|
|
|
277
|
-
` + `
|
|
278
|
-
` +
|
|
279
|
-
` +
|
|
280
|
-
` +
|
|
281
|
-
` +
|
|
282
|
-
` +
|
|
283
|
-
` +
|
|
284
|
-
|
|
285
|
-
` +
|
|
286
|
-
` + `-
|
|
287
|
-
` + `-
|
|
288
|
-
` + `-
|
|
289
|
-
` + `-
|
|
290
|
-
` + `-
|
|
291
|
-
|
|
272
|
+
` + `Options:
|
|
273
|
+
` + `1. "complete" — The task objectives have been met.
|
|
274
|
+
` + ` - For repo tasks: ONLY when a PR creation signal appears ("Created pull request #N"). ` + `A generic "done" or "finished" statement is NOT sufficient for repo tasks — a PR must exist.
|
|
275
|
+
` + ` - For scratch/research tasks (no repo): when the agent has produced its deliverable.
|
|
276
|
+
` + `2. "respond" — The agent needs to do more work.
|
|
277
|
+
` + `3. "escalate" — Something is wrong. Let the human decide.
|
|
278
|
+
` + `4. "ignore" — The agent is still working (e.g., spinner text like "Germinating...", "Frosting..."). ` + `Wait for the next turn.
|
|
279
|
+
|
|
280
|
+
` + `CRITICAL RULES:
|
|
281
|
+
` + `- For repo tasks: use "complete" ONLY when "Created pull request #N" appears in output.
|
|
282
|
+
` + `- For scratch/research tasks: use "complete" when the agent delivers its output.
|
|
283
|
+
` + `- Do NOT ask the agent to review, verify, or re-check work it already completed.
|
|
284
|
+
` + `- If output is only spinner text, use "ignore" and wait for the next turn.
|
|
285
|
+
` + `- Use "respond" when the agent hasn't started, or when code was written but not yet committed/pushed/PR'd.
|
|
286
|
+
|
|
287
|
+
` + `If the agent's output reveals a significant decision, include "keyDecision" with a brief summary.
|
|
292
288
|
|
|
293
289
|
` + `Respond with ONLY a JSON object:
|
|
294
290
|
` + `{"action": "respond|complete|escalate|ignore", "response": "...", "useKeys": false, "keys": [], "reasoning": "...", "keyDecision": "..."}`;
|
|
@@ -322,7 +318,7 @@ ${recentOutput.slice(-3000)}
|
|
|
322
318
|
` + `- For tool approvals / Y/n that align with the task, respond "y" or keys:["enter"].
|
|
323
319
|
` + `- If the prompt asks for info NOT in the original task, escalate.
|
|
324
320
|
` + `- Decline access to paths outside ${taskCtx.workdir}.
|
|
325
|
-
` + `- If a PR was just created,
|
|
321
|
+
` + `- If a PR was just created, the task is done — use "complete".
|
|
326
322
|
` + `- When in doubt, escalate.
|
|
327
323
|
|
|
328
324
|
` + `If the agent's output reveals a significant decision that sibling agents should know about, include "keyDecision" with a brief summary.
|
|
@@ -353,16 +349,14 @@ ${turnOutput.slice(-3000)}
|
|
|
353
349
|
|
|
354
350
|
` + `Options:
|
|
355
351
|
` + `- "respond" — send a follow-up instruction (DEFAULT for intermediate steps)
|
|
356
|
-
` + `- "complete" —
|
|
352
|
+
` + `- "complete" — For repo tasks: ONLY when "Created pull request #N" appears. ` + `For scratch/research tasks: when the agent delivers its output.
|
|
357
353
|
` + `- "escalate" — something looks wrong, ask the user
|
|
358
|
-
` + `- "ignore" —
|
|
354
|
+
` + `- "ignore" — spinner/loading output, agent still working
|
|
359
355
|
|
|
360
356
|
` + `Guidelines:
|
|
361
|
-
` + `-
|
|
357
|
+
` + `- For repo tasks, a generic "done" is NOT enough — require a PR creation signal.
|
|
362
358
|
` + `- If code was written but not committed/pushed/PR'd, respond with next step.
|
|
363
|
-
` + `-
|
|
364
|
-
` + `- When asking agents to verify work, prefer CLI tools (gh, curl, cat, etc.) over browser automation.
|
|
365
|
-
` + `- Default to "respond" — only "complete" when certain ALL work is done.
|
|
359
|
+
` + `- Do NOT ask the agent to re-verify work it already completed.
|
|
366
360
|
` + `- If the agent's output reveals a significant creative or architectural decision, include "keyDecision" with a brief summary.
|
|
367
361
|
` + `- Look for explicit "DECISION:" markers in the agent's output — always capture these as keyDecision.
|
|
368
362
|
|
|
@@ -822,6 +816,18 @@ async function executeDecision(ctx, sessionId, decision) {
|
|
|
822
816
|
const taskCtx = ctx.tasks.get(sessionId);
|
|
823
817
|
if (taskCtx) {
|
|
824
818
|
taskCtx.status = "completed";
|
|
819
|
+
ctx.history?.append({
|
|
820
|
+
timestamp: Date.now(),
|
|
821
|
+
type: "task_completed",
|
|
822
|
+
sessionId,
|
|
823
|
+
label: taskCtx.label,
|
|
824
|
+
agentType: taskCtx.agentType,
|
|
825
|
+
repo: taskCtx.repo,
|
|
826
|
+
workdir: taskCtx.workdir,
|
|
827
|
+
completionSummary: decision.reasoning
|
|
828
|
+
}).catch((err) => {
|
|
829
|
+
ctx.log(`Failed to persist task completion for "${taskCtx.label}" (${sessionId}): ${err}`);
|
|
830
|
+
});
|
|
825
831
|
}
|
|
826
832
|
ctx.broadcast({
|
|
827
833
|
type: "task_complete",
|
|
@@ -1022,6 +1028,31 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
|
|
|
1022
1028
|
const raw = await fetchRecentOutput(ctx, sessionId);
|
|
1023
1029
|
turnOutput = cleanForChat(raw);
|
|
1024
1030
|
}
|
|
1031
|
+
const PR_CREATED_RE = /(?:Created|Opened)\s+pull\s+request\s+#?\d+|gh\s+pr\s+create/i;
|
|
1032
|
+
if (PR_CREATED_RE.test(turnOutput)) {
|
|
1033
|
+
const fastDecision = {
|
|
1034
|
+
action: "complete",
|
|
1035
|
+
reasoning: "PR detected in turn output — task complete."
|
|
1036
|
+
};
|
|
1037
|
+
ctx.log(`Turn assessment for "${taskCtx.label}": complete (fast-path: PR detected in output)`);
|
|
1038
|
+
taskCtx.decisions.push({
|
|
1039
|
+
timestamp: Date.now(),
|
|
1040
|
+
event: "turn_complete",
|
|
1041
|
+
promptText: "Agent finished a turn",
|
|
1042
|
+
decision: "complete",
|
|
1043
|
+
response: "",
|
|
1044
|
+
reasoning: fastDecision.reasoning
|
|
1045
|
+
});
|
|
1046
|
+
recordKeyDecision(ctx, taskCtx.label, fastDecision);
|
|
1047
|
+
ctx.broadcast({
|
|
1048
|
+
type: "turn_assessment",
|
|
1049
|
+
sessionId,
|
|
1050
|
+
timestamp: Date.now(),
|
|
1051
|
+
data: { action: "complete", reasoning: fastDecision.reasoning }
|
|
1052
|
+
});
|
|
1053
|
+
await executeDecision(ctx, sessionId, fastDecision);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1025
1056
|
let decision = null;
|
|
1026
1057
|
const decisionFromPipeline = false;
|
|
1027
1058
|
const prompt = buildTurnCompletePrompt(toContextSummary(taskCtx), turnOutput, toDecisionHistory(taskCtx), collectSiblings(ctx, sessionId), ctx.sharedDecisions, ctx.getSwarmContext());
|
|
@@ -2283,15 +2314,15 @@ var sendToAgentAction = {
|
|
|
2283
2314
|
};
|
|
2284
2315
|
|
|
2285
2316
|
// src/actions/spawn-agent.ts
|
|
2286
|
-
import * as
|
|
2287
|
-
import * as
|
|
2317
|
+
import * as os2 from "node:os";
|
|
2318
|
+
import * as path3 from "node:path";
|
|
2288
2319
|
import {
|
|
2289
2320
|
logger as logger4
|
|
2290
2321
|
} from "@elizaos/core";
|
|
2291
2322
|
|
|
2292
2323
|
// src/services/pty-service.ts
|
|
2293
|
-
import { appendFile, mkdir, readFile as
|
|
2294
|
-
import { dirname, join as
|
|
2324
|
+
import { appendFile as appendFile2, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
2325
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
2295
2326
|
import { logger as logger3 } from "@elizaos/core";
|
|
2296
2327
|
import {
|
|
2297
2328
|
checkAdapters,
|
|
@@ -3103,9 +3134,9 @@ Classification states:
|
|
|
3103
3134
|
- For Y/n confirmations that align with the original task, respond "y".
|
|
3104
3135
|
- For TUI menus, use "keys:enter" for default or "keys:down,enter" for non-default.
|
|
3105
3136
|
- If the prompt asks for information NOT in the original task, set suggestedResponse to null (this will escalate to the human).
|
|
3106
|
-
|
|
3137
|
+
` + `- If a PR was just created, the task is likely done — classify as "task_complete".
|
|
3107
3138
|
|
|
3108
|
-
Respond with ONLY a JSON object:
|
|
3139
|
+
` + `Respond with ONLY a JSON object:
|
|
3109
3140
|
{"state": "...", "prompt": "...", "suggestedResponse": "..."}`;
|
|
3110
3141
|
}
|
|
3111
3142
|
async function classifyAndDecideForCoordinator(ctx) {
|
|
@@ -3202,9 +3233,124 @@ init_swarm_decision_loop();
|
|
|
3202
3233
|
|
|
3203
3234
|
// src/services/swarm-coordinator.ts
|
|
3204
3235
|
init_ansi_utils();
|
|
3205
|
-
init_swarm_decision_loop();
|
|
3206
3236
|
import { logger } from "@elizaos/core";
|
|
3207
3237
|
|
|
3238
|
+
// src/services/swarm-history.ts
|
|
3239
|
+
import * as fs from "fs/promises";
|
|
3240
|
+
import * as os from "os";
|
|
3241
|
+
import * as path2 from "path";
|
|
3242
|
+
var MAX_ENTRIES = 150;
|
|
3243
|
+
var TRUNCATE_TO = 100;
|
|
3244
|
+
|
|
3245
|
+
class WriteMutex {
|
|
3246
|
+
queue = [];
|
|
3247
|
+
locked = false;
|
|
3248
|
+
async acquire() {
|
|
3249
|
+
if (!this.locked) {
|
|
3250
|
+
this.locked = true;
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
return new Promise((resolve2) => {
|
|
3254
|
+
this.queue.push(resolve2);
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
release() {
|
|
3258
|
+
const next = this.queue.shift();
|
|
3259
|
+
if (next) {
|
|
3260
|
+
next();
|
|
3261
|
+
} else {
|
|
3262
|
+
this.locked = false;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
class SwarmHistory {
|
|
3268
|
+
filePath;
|
|
3269
|
+
appendCount = 0;
|
|
3270
|
+
mutex = new WriteMutex;
|
|
3271
|
+
constructor(stateDir) {
|
|
3272
|
+
const dir = stateDir || process.env.MILADY_STATE_DIR || process.env.ELIZA_STATE_DIR || path2.join(os.homedir(), ".milady");
|
|
3273
|
+
this.filePath = path2.join(dir, "swarm-history.jsonl");
|
|
3274
|
+
}
|
|
3275
|
+
async append(entry) {
|
|
3276
|
+
await this.mutex.acquire();
|
|
3277
|
+
try {
|
|
3278
|
+
const dir = path2.dirname(this.filePath);
|
|
3279
|
+
await fs.mkdir(dir, { recursive: true });
|
|
3280
|
+
await fs.appendFile(this.filePath, `${JSON.stringify(entry)}
|
|
3281
|
+
`, "utf-8");
|
|
3282
|
+
this.appendCount++;
|
|
3283
|
+
if (this.appendCount >= MAX_ENTRIES - TRUNCATE_TO) {
|
|
3284
|
+
const content = await fs.readFile(this.filePath, "utf-8");
|
|
3285
|
+
const lineCount = content.split(`
|
|
3286
|
+
`).filter((l) => l.trim() !== "").length;
|
|
3287
|
+
if (lineCount > MAX_ENTRIES) {
|
|
3288
|
+
await this.truncateInner(TRUNCATE_TO);
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
} catch (err) {
|
|
3292
|
+
console.error("[swarm-history] append failed:", err);
|
|
3293
|
+
throw err;
|
|
3294
|
+
} finally {
|
|
3295
|
+
this.mutex.release();
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
async readAll() {
|
|
3299
|
+
try {
|
|
3300
|
+
const content = await fs.readFile(this.filePath, "utf-8");
|
|
3301
|
+
const entries = [];
|
|
3302
|
+
const lines = content.split(`
|
|
3303
|
+
`);
|
|
3304
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3305
|
+
if (lines[i].trim() === "")
|
|
3306
|
+
continue;
|
|
3307
|
+
try {
|
|
3308
|
+
entries.push(JSON.parse(lines[i]));
|
|
3309
|
+
} catch {
|
|
3310
|
+
console.warn(`[swarm-history] skipping corrupted line at index ${i} (length=${lines[i].length})`);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
return entries;
|
|
3314
|
+
} catch (err) {
|
|
3315
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
3316
|
+
return [];
|
|
3317
|
+
}
|
|
3318
|
+
console.error("[swarm-history] readAll failed:", err);
|
|
3319
|
+
return [];
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
async getLastUsedRepo() {
|
|
3323
|
+
const entries = await this.readAll();
|
|
3324
|
+
for (let i = entries.length - 1;i >= 0; i--) {
|
|
3325
|
+
if (entries[i].repo) {
|
|
3326
|
+
return entries[i].repo;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
async truncateInner(maxEntries) {
|
|
3332
|
+
const entries = await this.readAll();
|
|
3333
|
+
if (entries.length === 0) {
|
|
3334
|
+
try {
|
|
3335
|
+
await fs.stat(this.filePath);
|
|
3336
|
+
console.error("[swarm-history] truncate aborted: file exists but readAll returned empty");
|
|
3337
|
+
return;
|
|
3338
|
+
} catch {
|
|
3339
|
+
return;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
const kept = entries.slice(-maxEntries);
|
|
3343
|
+
const content = kept.map((e) => JSON.stringify(e)).join(`
|
|
3344
|
+
`) + `
|
|
3345
|
+
`;
|
|
3346
|
+
await fs.writeFile(this.filePath, content, "utf-8");
|
|
3347
|
+
this.appendCount = 0;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
// src/services/swarm-coordinator.ts
|
|
3352
|
+
init_swarm_decision_loop();
|
|
3353
|
+
|
|
3208
3354
|
// src/services/swarm-idle-watchdog.ts
|
|
3209
3355
|
init_ansi_utils();
|
|
3210
3356
|
init_swarm_decision_loop();
|
|
@@ -3397,7 +3543,9 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
|
|
|
3397
3543
|
}
|
|
3398
3544
|
|
|
3399
3545
|
// src/services/swarm-coordinator.ts
|
|
3400
|
-
var
|
|
3546
|
+
var UNREGISTERED_RETRY_DELAYS = [2000, 4000, 8000, 16000];
|
|
3547
|
+
var UNREGISTERED_MAX_TOTAL_MS = 30000;
|
|
3548
|
+
var TURN_COMPLETE_COALESCE_MS = 500;
|
|
3401
3549
|
var IDLE_SCAN_INTERVAL_MS = 60 * 1000;
|
|
3402
3550
|
var PAUSE_TIMEOUT_MS = 30000;
|
|
3403
3551
|
var MAX_PRE_BRIDGE_BUFFER = 100;
|
|
@@ -3431,6 +3579,10 @@ class SwarmCoordinator {
|
|
|
3431
3579
|
pauseBuffer = [];
|
|
3432
3580
|
preBridgeBroadcastBuffer = [];
|
|
3433
3581
|
pauseTimeout = null;
|
|
3582
|
+
startedAt = Date.now();
|
|
3583
|
+
unregisteredRetryTimers = new Map;
|
|
3584
|
+
turnCompleteCoalesceTimers = new Map;
|
|
3585
|
+
history = new SwarmHistory;
|
|
3434
3586
|
constructor(runtime) {
|
|
3435
3587
|
this.runtime = runtime;
|
|
3436
3588
|
}
|
|
@@ -3515,6 +3667,14 @@ class SwarmCoordinator {
|
|
|
3515
3667
|
this.lastBlockedPromptFingerprint.clear();
|
|
3516
3668
|
this.pendingBlocked.clear();
|
|
3517
3669
|
this.unregisteredBuffer.clear();
|
|
3670
|
+
for (const timer of this.unregisteredRetryTimers.values()) {
|
|
3671
|
+
clearTimeout(timer);
|
|
3672
|
+
}
|
|
3673
|
+
this.unregisteredRetryTimers.clear();
|
|
3674
|
+
for (const timer of this.turnCompleteCoalesceTimers.values()) {
|
|
3675
|
+
clearTimeout(timer);
|
|
3676
|
+
}
|
|
3677
|
+
this.turnCompleteCoalesceTimers.clear();
|
|
3518
3678
|
this.lastSeenOutput.clear();
|
|
3519
3679
|
this.lastToolNotification.clear();
|
|
3520
3680
|
this.agentDecisionCb = null;
|
|
@@ -3601,6 +3761,19 @@ class SwarmCoordinator {
|
|
|
3601
3761
|
taskDelivered: false,
|
|
3602
3762
|
lastSeenDecisionIndex: 0
|
|
3603
3763
|
});
|
|
3764
|
+
if (context.repo) {
|
|
3765
|
+
this._lastUsedRepo = context.repo;
|
|
3766
|
+
}
|
|
3767
|
+
this.history.append({
|
|
3768
|
+
timestamp: Date.now(),
|
|
3769
|
+
type: "task_registered",
|
|
3770
|
+
sessionId,
|
|
3771
|
+
label: context.label,
|
|
3772
|
+
agentType: context.agentType,
|
|
3773
|
+
repo: context.repo,
|
|
3774
|
+
workdir: context.workdir,
|
|
3775
|
+
originalTask: context.originalTask
|
|
3776
|
+
}).catch(() => {});
|
|
3604
3777
|
this.broadcast({
|
|
3605
3778
|
type: "task_registered",
|
|
3606
3779
|
sessionId,
|
|
@@ -3611,6 +3784,11 @@ class SwarmCoordinator {
|
|
|
3611
3784
|
originalTask: context.originalTask
|
|
3612
3785
|
}
|
|
3613
3786
|
});
|
|
3787
|
+
const retryTimer = this.unregisteredRetryTimers.get(sessionId);
|
|
3788
|
+
if (retryTimer) {
|
|
3789
|
+
clearTimeout(retryTimer);
|
|
3790
|
+
this.unregisteredRetryTimers.delete(sessionId);
|
|
3791
|
+
}
|
|
3614
3792
|
const buffered = this.unregisteredBuffer.get(sessionId);
|
|
3615
3793
|
if (buffered) {
|
|
3616
3794
|
this.unregisteredBuffer.delete(sessionId);
|
|
@@ -3621,6 +3799,7 @@ class SwarmCoordinator {
|
|
|
3621
3799
|
}
|
|
3622
3800
|
}
|
|
3623
3801
|
}
|
|
3802
|
+
_lastUsedRepo;
|
|
3624
3803
|
getLastUsedRepo() {
|
|
3625
3804
|
let latest;
|
|
3626
3805
|
for (const task of this.tasks.values()) {
|
|
@@ -3628,7 +3807,17 @@ class SwarmCoordinator {
|
|
|
3628
3807
|
latest = task;
|
|
3629
3808
|
}
|
|
3630
3809
|
}
|
|
3631
|
-
return latest?.repo;
|
|
3810
|
+
return latest?.repo ?? this._lastUsedRepo;
|
|
3811
|
+
}
|
|
3812
|
+
async getLastUsedRepoAsync() {
|
|
3813
|
+
const memoryRepo = this.getLastUsedRepo();
|
|
3814
|
+
if (memoryRepo)
|
|
3815
|
+
return memoryRepo;
|
|
3816
|
+
try {
|
|
3817
|
+
return await this.history.getLastUsedRepo();
|
|
3818
|
+
} catch {
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3632
3821
|
}
|
|
3633
3822
|
getTaskContext(sessionId) {
|
|
3634
3823
|
return this.tasks.get(sessionId);
|
|
@@ -3636,6 +3825,33 @@ class SwarmCoordinator {
|
|
|
3636
3825
|
getAllTaskContexts() {
|
|
3637
3826
|
return Array.from(this.tasks.values());
|
|
3638
3827
|
}
|
|
3828
|
+
scheduleUnregisteredRetry(sessionId, attempt) {
|
|
3829
|
+
const delay = UNREGISTERED_RETRY_DELAYS[Math.min(attempt, UNREGISTERED_RETRY_DELAYS.length - 1)];
|
|
3830
|
+
const timer = setTimeout(() => {
|
|
3831
|
+
this.unregisteredRetryTimers.delete(sessionId);
|
|
3832
|
+
const stillBuffered = this.unregisteredBuffer.get(sessionId);
|
|
3833
|
+
if (!stillBuffered || stillBuffered.length === 0)
|
|
3834
|
+
return;
|
|
3835
|
+
const ctx = this.tasks.get(sessionId);
|
|
3836
|
+
if (ctx) {
|
|
3837
|
+
this.unregisteredBuffer.delete(sessionId);
|
|
3838
|
+
for (const entry of stillBuffered) {
|
|
3839
|
+
this.handleSessionEvent(sessionId, entry.event, entry.data).catch(() => {});
|
|
3840
|
+
}
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
const oldest = stillBuffered[0].receivedAt;
|
|
3844
|
+
const totalElapsed = Date.now() - oldest;
|
|
3845
|
+
if (totalElapsed >= UNREGISTERED_MAX_TOTAL_MS) {
|
|
3846
|
+
this.unregisteredBuffer.delete(sessionId);
|
|
3847
|
+
this.log(`Discarding ${stillBuffered.length} buffered events for unregistered session ${sessionId} after ${Math.round(totalElapsed / 1000)}s`);
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
this.log(`Retry ${attempt + 1} for unregistered session ${sessionId} (next in ${delay}ms)`);
|
|
3851
|
+
this.scheduleUnregisteredRetry(sessionId, attempt + 1);
|
|
3852
|
+
}, delay);
|
|
3853
|
+
this.unregisteredRetryTimers.set(sessionId, timer);
|
|
3854
|
+
}
|
|
3639
3855
|
addSseClient(res) {
|
|
3640
3856
|
this.sseClients.add(res);
|
|
3641
3857
|
const snapshot = {
|
|
@@ -3681,6 +3897,13 @@ class SwarmCoordinator {
|
|
|
3681
3897
|
} catch {}
|
|
3682
3898
|
}
|
|
3683
3899
|
async handleSessionEvent(sessionId, event, data) {
|
|
3900
|
+
const tsMatch = sessionId.match(/^pty-(\d+)-/);
|
|
3901
|
+
if (tsMatch) {
|
|
3902
|
+
const sessionCreatedAt = Number(tsMatch[1]);
|
|
3903
|
+
if (sessionCreatedAt < this.startedAt - 60000) {
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3684
3907
|
const taskCtx = this.tasks.get(sessionId);
|
|
3685
3908
|
if (!taskCtx) {
|
|
3686
3909
|
if (event === "blocked" || event === "task_complete" || event === "error") {
|
|
@@ -3690,21 +3913,9 @@ class SwarmCoordinator {
|
|
|
3690
3913
|
this.unregisteredBuffer.set(sessionId, buffer);
|
|
3691
3914
|
}
|
|
3692
3915
|
buffer.push({ event, data, receivedAt: Date.now() });
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
const ctx = this.tasks.get(sessionId);
|
|
3697
|
-
if (ctx) {
|
|
3698
|
-
this.unregisteredBuffer.delete(sessionId);
|
|
3699
|
-
for (const entry of stillBuffered) {
|
|
3700
|
-
this.handleSessionEvent(sessionId, entry.event, entry.data).catch(() => {});
|
|
3701
|
-
}
|
|
3702
|
-
} else {
|
|
3703
|
-
this.unregisteredBuffer.delete(sessionId);
|
|
3704
|
-
this.log(`Discarding ${stillBuffered.length} buffered events for unregistered session ${sessionId}`);
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
}, UNREGISTERED_BUFFER_MS);
|
|
3916
|
+
if (!this.unregisteredRetryTimers.has(sessionId)) {
|
|
3917
|
+
this.scheduleUnregisteredRetry(sessionId, 0);
|
|
3918
|
+
}
|
|
3708
3919
|
}
|
|
3709
3920
|
return;
|
|
3710
3921
|
}
|
|
@@ -3754,7 +3965,20 @@ class SwarmCoordinator {
|
|
|
3754
3965
|
timestamp: Date.now(),
|
|
3755
3966
|
data
|
|
3756
3967
|
});
|
|
3757
|
-
|
|
3968
|
+
const existingCoalesce = this.turnCompleteCoalesceTimers.get(sessionId);
|
|
3969
|
+
if (existingCoalesce)
|
|
3970
|
+
clearTimeout(existingCoalesce);
|
|
3971
|
+
const coalescedData = data;
|
|
3972
|
+
const coalesceTimer = setTimeout(() => {
|
|
3973
|
+
this.turnCompleteCoalesceTimers.delete(sessionId);
|
|
3974
|
+
const currentTask = this.tasks.get(sessionId);
|
|
3975
|
+
if (currentTask && currentTask.status === "active") {
|
|
3976
|
+
handleTurnComplete(this, sessionId, currentTask, coalescedData).catch((err) => {
|
|
3977
|
+
this.log(`Coalesced turn-complete failed: ${err}`);
|
|
3978
|
+
});
|
|
3979
|
+
}
|
|
3980
|
+
}, TURN_COMPLETE_COALESCE_MS);
|
|
3981
|
+
this.turnCompleteCoalesceTimers.set(sessionId, coalesceTimer);
|
|
3758
3982
|
break;
|
|
3759
3983
|
}
|
|
3760
3984
|
case "error": {
|
|
@@ -4169,10 +4393,10 @@ class PTYService {
|
|
|
4169
4393
|
const hookUrl = `http://localhost:${this.runtime.getSetting("SERVER_PORT") ?? "2138"}/api/coding-agents/hooks`;
|
|
4170
4394
|
if (resolvedAgentType === "claude") {
|
|
4171
4395
|
try {
|
|
4172
|
-
const settingsPath =
|
|
4396
|
+
const settingsPath = join3(workdir, ".claude", "settings.json");
|
|
4173
4397
|
let settings = {};
|
|
4174
4398
|
try {
|
|
4175
|
-
settings = JSON.parse(await
|
|
4399
|
+
settings = JSON.parse(await readFile3(settingsPath, "utf-8"));
|
|
4176
4400
|
} catch {}
|
|
4177
4401
|
const permissions = settings.permissions ?? {};
|
|
4178
4402
|
permissions.allowedDirectories = [workdir];
|
|
@@ -4187,8 +4411,8 @@ class PTYService {
|
|
|
4187
4411
|
settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
|
|
4188
4412
|
this.log(`Injecting HTTP hooks for session ${sessionId}`);
|
|
4189
4413
|
}
|
|
4190
|
-
await
|
|
4191
|
-
await
|
|
4414
|
+
await mkdir2(dirname2(settingsPath), { recursive: true });
|
|
4415
|
+
await writeFile3(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
4192
4416
|
this.log(`Wrote allowedDirectories [${workdir}] to ${settingsPath}`);
|
|
4193
4417
|
} catch (err) {
|
|
4194
4418
|
this.log(`Failed to write Claude settings: ${err}`);
|
|
@@ -4196,10 +4420,10 @@ class PTYService {
|
|
|
4196
4420
|
}
|
|
4197
4421
|
if (resolvedAgentType === "gemini") {
|
|
4198
4422
|
try {
|
|
4199
|
-
const settingsPath =
|
|
4423
|
+
const settingsPath = join3(workdir, ".gemini", "settings.json");
|
|
4200
4424
|
let settings = {};
|
|
4201
4425
|
try {
|
|
4202
|
-
settings = JSON.parse(await
|
|
4426
|
+
settings = JSON.parse(await readFile3(settingsPath, "utf-8"));
|
|
4203
4427
|
} catch {}
|
|
4204
4428
|
const adapter = this.getAdapter("gemini");
|
|
4205
4429
|
const hookProtocol = adapter.getHookTelemetryProtocol({
|
|
@@ -4211,8 +4435,8 @@ class PTYService {
|
|
|
4211
4435
|
settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
|
|
4212
4436
|
this.log(`Injecting Gemini CLI hooks for session ${sessionId}`);
|
|
4213
4437
|
}
|
|
4214
|
-
await
|
|
4215
|
-
await
|
|
4438
|
+
await mkdir2(dirname2(settingsPath), { recursive: true });
|
|
4439
|
+
await writeFile3(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
4216
4440
|
} catch (err) {
|
|
4217
4441
|
this.log(`Failed to write Gemini settings: ${err}`);
|
|
4218
4442
|
}
|
|
@@ -4504,7 +4728,7 @@ class PTYService {
|
|
|
4504
4728
|
static GITIGNORE_MARKER = "# orchestrator-injected (do not commit agent config/memory files)";
|
|
4505
4729
|
static gitignoreLocks = new Map;
|
|
4506
4730
|
async ensureOrchestratorGitignore(workdir) {
|
|
4507
|
-
const gitignorePath =
|
|
4731
|
+
const gitignorePath = join3(workdir, ".gitignore");
|
|
4508
4732
|
const existing_lock = PTYService.gitignoreLocks.get(gitignorePath);
|
|
4509
4733
|
if (existing_lock)
|
|
4510
4734
|
await existing_lock;
|
|
@@ -4521,7 +4745,7 @@ class PTYService {
|
|
|
4521
4745
|
async doEnsureGitignore(gitignorePath, workdir) {
|
|
4522
4746
|
let existing = "";
|
|
4523
4747
|
try {
|
|
4524
|
-
existing = await
|
|
4748
|
+
existing = await readFile3(gitignorePath, "utf-8");
|
|
4525
4749
|
} catch {}
|
|
4526
4750
|
if (existing.includes(PTYService.GITIGNORE_MARKER))
|
|
4527
4751
|
return;
|
|
@@ -4536,14 +4760,14 @@ class PTYService {
|
|
|
4536
4760
|
];
|
|
4537
4761
|
try {
|
|
4538
4762
|
if (existing.length === 0) {
|
|
4539
|
-
await
|
|
4763
|
+
await writeFile3(gitignorePath, entries.join(`
|
|
4540
4764
|
`) + `
|
|
4541
4765
|
`, "utf-8");
|
|
4542
4766
|
} else {
|
|
4543
4767
|
const separator = existing.endsWith(`
|
|
4544
4768
|
`) ? "" : `
|
|
4545
4769
|
`;
|
|
4546
|
-
await
|
|
4770
|
+
await appendFile2(gitignorePath, separator + entries.join(`
|
|
4547
4771
|
`) + `
|
|
4548
4772
|
`, "utf-8");
|
|
4549
4773
|
}
|
|
@@ -4687,13 +4911,13 @@ var spawnAgentAction = {
|
|
|
4687
4911
|
}
|
|
4688
4912
|
return { success: false, error: "NO_WORKSPACE" };
|
|
4689
4913
|
}
|
|
4690
|
-
const resolvedWorkdir =
|
|
4691
|
-
const workspaceBaseDir =
|
|
4914
|
+
const resolvedWorkdir = path3.resolve(workdir);
|
|
4915
|
+
const workspaceBaseDir = path3.join(os2.homedir(), ".milady", "workspaces");
|
|
4692
4916
|
const allowedPrefixes = [
|
|
4693
|
-
|
|
4694
|
-
|
|
4917
|
+
path3.resolve(workspaceBaseDir),
|
|
4918
|
+
path3.resolve(process.cwd())
|
|
4695
4919
|
];
|
|
4696
|
-
const isAllowed = allowedPrefixes.some((prefix) => resolvedWorkdir.startsWith(prefix +
|
|
4920
|
+
const isAllowed = allowedPrefixes.some((prefix) => resolvedWorkdir.startsWith(prefix + path3.sep) || resolvedWorkdir === prefix);
|
|
4697
4921
|
if (!isAllowed) {
|
|
4698
4922
|
if (callback) {
|
|
4699
4923
|
await callback({
|
|
@@ -5045,17 +5269,17 @@ function formatAge(timestamp) {
|
|
|
5045
5269
|
|
|
5046
5270
|
// src/actions/coding-task-helpers.ts
|
|
5047
5271
|
import { randomUUID } from "node:crypto";
|
|
5048
|
-
import * as
|
|
5049
|
-
import * as
|
|
5050
|
-
import * as
|
|
5272
|
+
import * as fs2 from "node:fs";
|
|
5273
|
+
import * as os3 from "node:os";
|
|
5274
|
+
import * as path4 from "node:path";
|
|
5051
5275
|
import {
|
|
5052
5276
|
logger as logger5
|
|
5053
5277
|
} from "@elizaos/core";
|
|
5054
5278
|
function createScratchDir() {
|
|
5055
|
-
const baseDir =
|
|
5279
|
+
const baseDir = path4.join(os3.homedir(), ".milady", "workspaces");
|
|
5056
5280
|
const scratchId = randomUUID();
|
|
5057
|
-
const scratchDir =
|
|
5058
|
-
|
|
5281
|
+
const scratchDir = path4.join(baseDir, scratchId);
|
|
5282
|
+
fs2.mkdirSync(scratchDir, { recursive: true });
|
|
5059
5283
|
return scratchDir;
|
|
5060
5284
|
}
|
|
5061
5285
|
function generateLabel(repo, task) {
|
|
@@ -5466,13 +5690,23 @@ var startCodingTaskAction = {
|
|
|
5466
5690
|
repo = urlMatch[0];
|
|
5467
5691
|
}
|
|
5468
5692
|
}
|
|
5469
|
-
|
|
5693
|
+
const reuseRepo = params?.reuseRepo ?? content.reuseRepo ?? /\b(same\s+repo|same\s+project|continue|that\s+repo|the\s+repo|this\s+repo|in\s+the\s+repo)\b/i.test(content.text ?? "");
|
|
5694
|
+
if (!repo && reuseRepo) {
|
|
5470
5695
|
const coordinator = getCoordinator(runtime);
|
|
5471
|
-
const lastRepo = coordinator?.
|
|
5696
|
+
const lastRepo = await coordinator?.getLastUsedRepoAsync();
|
|
5472
5697
|
if (lastRepo) {
|
|
5473
5698
|
repo = lastRepo;
|
|
5474
5699
|
}
|
|
5475
5700
|
}
|
|
5701
|
+
if (!repo && reuseRepo) {
|
|
5702
|
+
const wsService2 = runtime.getService("CODING_WORKSPACE_SERVICE");
|
|
5703
|
+
if (wsService2 && typeof wsService2.listWorkspaces === "function") {
|
|
5704
|
+
const withRepo = wsService2.listWorkspaces().find((ws) => ws.repo);
|
|
5705
|
+
if (withRepo) {
|
|
5706
|
+
repo = withRepo.repo;
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5476
5710
|
const customCredentialKeys = runtime.getSetting("CUSTOM_CREDENTIAL_KEYS");
|
|
5477
5711
|
let customCredentials;
|
|
5478
5712
|
if (customCredentialKeys) {
|
|
@@ -6026,9 +6260,9 @@ var activeWorkspaceContextProvider = {
|
|
|
6026
6260
|
};
|
|
6027
6261
|
|
|
6028
6262
|
// src/services/workspace-service.ts
|
|
6029
|
-
import * as
|
|
6030
|
-
import * as
|
|
6031
|
-
import * as
|
|
6263
|
+
import * as os4 from "node:os";
|
|
6264
|
+
import * as path6 from "node:path";
|
|
6265
|
+
import * as fs4 from "node:fs/promises";
|
|
6032
6266
|
import {
|
|
6033
6267
|
CredentialService,
|
|
6034
6268
|
GitHubPatClient as GitHubPatClient2,
|
|
@@ -6241,17 +6475,17 @@ async function createPR(workspaceService, workspace, workspaceId, options, log)
|
|
|
6241
6475
|
}
|
|
6242
6476
|
|
|
6243
6477
|
// src/services/workspace-lifecycle.ts
|
|
6244
|
-
import * as
|
|
6245
|
-
import * as
|
|
6478
|
+
import * as fs3 from "node:fs";
|
|
6479
|
+
import * as path5 from "node:path";
|
|
6246
6480
|
async function removeScratchDir(dirPath, baseDir, log) {
|
|
6247
|
-
const resolved =
|
|
6248
|
-
const resolvedBase =
|
|
6249
|
-
if (!resolved.startsWith(resolvedBase) && resolved !==
|
|
6481
|
+
const resolved = path5.resolve(dirPath);
|
|
6482
|
+
const resolvedBase = path5.resolve(baseDir) + path5.sep;
|
|
6483
|
+
if (!resolved.startsWith(resolvedBase) && resolved !== path5.resolve(baseDir)) {
|
|
6250
6484
|
console.warn(`[CodingWorkspaceService] Refusing to remove dir outside base: ${resolved}`);
|
|
6251
6485
|
return;
|
|
6252
6486
|
}
|
|
6253
6487
|
try {
|
|
6254
|
-
await
|
|
6488
|
+
await fs3.promises.rm(resolved, { recursive: true, force: true });
|
|
6255
6489
|
log(`Removed scratch dir ${resolved}`);
|
|
6256
6490
|
} catch (err) {
|
|
6257
6491
|
console.warn(`[CodingWorkspaceService] Failed to remove scratch dir ${resolved}:`, err);
|
|
@@ -6264,7 +6498,7 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
|
|
|
6264
6498
|
}
|
|
6265
6499
|
let entries;
|
|
6266
6500
|
try {
|
|
6267
|
-
entries = await
|
|
6501
|
+
entries = await fs3.promises.readdir(baseDir, { withFileTypes: true });
|
|
6268
6502
|
} catch {
|
|
6269
6503
|
return;
|
|
6270
6504
|
}
|
|
@@ -6278,12 +6512,12 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
|
|
|
6278
6512
|
skipped++;
|
|
6279
6513
|
continue;
|
|
6280
6514
|
}
|
|
6281
|
-
const dirPath =
|
|
6515
|
+
const dirPath = path5.join(baseDir, entry.name);
|
|
6282
6516
|
try {
|
|
6283
|
-
const
|
|
6284
|
-
const age = now -
|
|
6517
|
+
const stat2 = await fs3.promises.stat(dirPath);
|
|
6518
|
+
const age = now - stat2.mtimeMs;
|
|
6285
6519
|
if (age > workspaceTtlMs) {
|
|
6286
|
-
await
|
|
6520
|
+
await fs3.promises.rm(dirPath, { recursive: true, force: true });
|
|
6287
6521
|
removed++;
|
|
6288
6522
|
} else {
|
|
6289
6523
|
skipped++;
|
|
@@ -6317,7 +6551,7 @@ class CodingWorkspaceService {
|
|
|
6317
6551
|
constructor(runtime, config = {}) {
|
|
6318
6552
|
this.runtime = runtime;
|
|
6319
6553
|
this.serviceConfig = {
|
|
6320
|
-
baseDir: config.baseDir ??
|
|
6554
|
+
baseDir: config.baseDir ?? path6.join(os4.homedir(), ".milady", "workspaces"),
|
|
6321
6555
|
branchPrefix: config.branchPrefix ?? "milady",
|
|
6322
6556
|
debug: config.debug ?? false,
|
|
6323
6557
|
workspaceTtlMs: config.workspaceTtlMs ?? 24 * 60 * 60 * 1000
|
|
@@ -6636,14 +6870,14 @@ class CodingWorkspaceService {
|
|
|
6636
6870
|
const suggestedName = this.sanitizeWorkspaceName(name || record.label);
|
|
6637
6871
|
const targetPath = await this.allocatePromotedPath(baseDir, suggestedName);
|
|
6638
6872
|
try {
|
|
6639
|
-
await
|
|
6873
|
+
await fs4.rename(record.path, targetPath);
|
|
6640
6874
|
} catch (error) {
|
|
6641
6875
|
const isExdev = typeof error === "object" && error !== null && "code" in error && error.code === "EXDEV";
|
|
6642
6876
|
if (!isExdev)
|
|
6643
6877
|
throw error;
|
|
6644
|
-
await
|
|
6645
|
-
await
|
|
6646
|
-
await
|
|
6878
|
+
await fs4.cp(record.path, targetPath, { recursive: true });
|
|
6879
|
+
await fs4.access(targetPath);
|
|
6880
|
+
await fs4.rm(record.path, { recursive: true, force: true });
|
|
6647
6881
|
}
|
|
6648
6882
|
const next = {
|
|
6649
6883
|
...record,
|
|
@@ -6716,15 +6950,15 @@ class CodingWorkspaceService {
|
|
|
6716
6950
|
return compact || `scratch-${Date.now().toString(36)}`;
|
|
6717
6951
|
}
|
|
6718
6952
|
async allocatePromotedPath(baseDir, baseName) {
|
|
6719
|
-
const baseResolved =
|
|
6953
|
+
const baseResolved = path6.resolve(baseDir);
|
|
6720
6954
|
for (let i = 0;i < 1000; i++) {
|
|
6721
6955
|
const candidateName = i === 0 ? baseName : `${baseName}-${i}`;
|
|
6722
|
-
const candidate =
|
|
6723
|
-
if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${
|
|
6956
|
+
const candidate = path6.resolve(baseResolved, candidateName);
|
|
6957
|
+
if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path6.sep}`)) {
|
|
6724
6958
|
continue;
|
|
6725
6959
|
}
|
|
6726
6960
|
try {
|
|
6727
|
-
await
|
|
6961
|
+
await fs4.access(candidate);
|
|
6728
6962
|
} catch {
|
|
6729
6963
|
return candidate;
|
|
6730
6964
|
}
|
|
@@ -6733,10 +6967,10 @@ class CodingWorkspaceService {
|
|
|
6733
6967
|
}
|
|
6734
6968
|
}
|
|
6735
6969
|
// src/api/agent-routes.ts
|
|
6736
|
-
import { access as access2, readFile as
|
|
6970
|
+
import { access as access2, readFile as readFile4, realpath, rm as rm2 } from "node:fs/promises";
|
|
6737
6971
|
import { createHash } from "node:crypto";
|
|
6738
|
-
import * as
|
|
6739
|
-
import * as
|
|
6972
|
+
import * as os5 from "node:os";
|
|
6973
|
+
import * as path7 from "node:path";
|
|
6740
6974
|
import { execFile } from "node:child_process";
|
|
6741
6975
|
import { promisify } from "node:util";
|
|
6742
6976
|
var execFileAsync = promisify(execFile);
|
|
@@ -6748,23 +6982,23 @@ function shouldAutoPreflight() {
|
|
|
6748
6982
|
return false;
|
|
6749
6983
|
}
|
|
6750
6984
|
function isPathInside(parent, candidate) {
|
|
6751
|
-
return candidate === parent || candidate.startsWith(`${parent}${
|
|
6985
|
+
return candidate === parent || candidate.startsWith(`${parent}${path7.sep}`);
|
|
6752
6986
|
}
|
|
6753
6987
|
async function resolveSafeVenvPath(workdir, venvDirRaw) {
|
|
6754
6988
|
const venvDir = venvDirRaw.trim();
|
|
6755
6989
|
if (!venvDir) {
|
|
6756
6990
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be non-empty");
|
|
6757
6991
|
}
|
|
6758
|
-
if (
|
|
6992
|
+
if (path7.isAbsolute(venvDir)) {
|
|
6759
6993
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be relative to workdir");
|
|
6760
6994
|
}
|
|
6761
|
-
const normalized =
|
|
6762
|
-
if (normalized === "." || normalized === ".." || normalized.startsWith(`..${
|
|
6995
|
+
const normalized = path7.normalize(venvDir);
|
|
6996
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path7.sep}`)) {
|
|
6763
6997
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must stay within workdir");
|
|
6764
6998
|
}
|
|
6765
|
-
const workdirResolved =
|
|
6999
|
+
const workdirResolved = path7.resolve(workdir);
|
|
6766
7000
|
const workdirReal = await realpath(workdirResolved);
|
|
6767
|
-
const resolved =
|
|
7001
|
+
const resolved = path7.resolve(workdirReal, normalized);
|
|
6768
7002
|
if (!isPathInside(workdirReal, resolved)) {
|
|
6769
7003
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV resolves outside workdir");
|
|
6770
7004
|
}
|
|
@@ -6780,7 +7014,7 @@ async function resolveSafeVenvPath(workdir, venvDirRaw) {
|
|
|
6780
7014
|
const maybeErr = err;
|
|
6781
7015
|
if (maybeErr?.code !== "ENOENT")
|
|
6782
7016
|
throw err;
|
|
6783
|
-
const parentReal = await realpath(
|
|
7017
|
+
const parentReal = await realpath(path7.dirname(resolved));
|
|
6784
7018
|
if (!isPathInside(workdirReal, parentReal)) {
|
|
6785
7019
|
throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV parent resolves outside workdir");
|
|
6786
7020
|
}
|
|
@@ -6796,10 +7030,10 @@ async function fileExists(filePath) {
|
|
|
6796
7030
|
}
|
|
6797
7031
|
}
|
|
6798
7032
|
async function resolveRequirementsPath(workdir) {
|
|
6799
|
-
const workdirReal = await realpath(
|
|
7033
|
+
const workdirReal = await realpath(path7.resolve(workdir));
|
|
6800
7034
|
const candidates = [
|
|
6801
|
-
|
|
6802
|
-
|
|
7035
|
+
path7.join(workdir, "apps", "api", "requirements.txt"),
|
|
7036
|
+
path7.join(workdir, "requirements.txt")
|
|
6803
7037
|
];
|
|
6804
7038
|
for (const candidate of candidates) {
|
|
6805
7039
|
if (!await fileExists(candidate))
|
|
@@ -6813,7 +7047,7 @@ async function resolveRequirementsPath(workdir) {
|
|
|
6813
7047
|
return null;
|
|
6814
7048
|
}
|
|
6815
7049
|
async function fingerprintRequirementsFile(requirementsPath) {
|
|
6816
|
-
const file = await
|
|
7050
|
+
const file = await readFile4(requirementsPath);
|
|
6817
7051
|
return createHash("sha256").update(file).digest("hex");
|
|
6818
7052
|
}
|
|
6819
7053
|
async function runBenchmarkPreflight(workdir) {
|
|
@@ -6826,7 +7060,7 @@ async function runBenchmarkPreflight(workdir) {
|
|
|
6826
7060
|
const mode = process.env.PARALLAX_BENCHMARK_PREFLIGHT_MODE?.toLowerCase() === "warm" ? "warm" : "cold";
|
|
6827
7061
|
const venvDir = process.env.PARALLAX_BENCHMARK_PREFLIGHT_VENV || ".benchmark-venv";
|
|
6828
7062
|
const venvPath = await resolveSafeVenvPath(workdir, venvDir);
|
|
6829
|
-
const pythonInVenv =
|
|
7063
|
+
const pythonInVenv = path7.join(venvPath, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
|
|
6830
7064
|
const key = `${workdir}::${mode}::${venvPath}::${requirementsFingerprint}`;
|
|
6831
7065
|
if (PREFLIGHT_DONE.has(key)) {
|
|
6832
7066
|
if (await fileExists(pythonInVenv))
|
|
@@ -7034,21 +7268,21 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
|
|
|
7034
7268
|
customCredentials,
|
|
7035
7269
|
metadata
|
|
7036
7270
|
} = body;
|
|
7037
|
-
const workspaceBaseDir =
|
|
7038
|
-
const workspaceBaseDirResolved =
|
|
7039
|
-
const cwdResolved =
|
|
7271
|
+
const workspaceBaseDir = path7.join(os5.homedir(), ".milady", "workspaces");
|
|
7272
|
+
const workspaceBaseDirResolved = path7.resolve(workspaceBaseDir);
|
|
7273
|
+
const cwdResolved = path7.resolve(process.cwd());
|
|
7040
7274
|
const workspaceBaseDirReal = await realpath(workspaceBaseDirResolved).catch(() => workspaceBaseDirResolved);
|
|
7041
7275
|
const cwdReal = await realpath(cwdResolved).catch(() => cwdResolved);
|
|
7042
7276
|
const allowedPrefixes = [workspaceBaseDirReal, cwdReal];
|
|
7043
7277
|
let workdir = rawWorkdir;
|
|
7044
7278
|
if (workdir) {
|
|
7045
|
-
const resolved =
|
|
7279
|
+
const resolved = path7.resolve(workdir);
|
|
7046
7280
|
const resolvedReal = await realpath(resolved).catch(() => null);
|
|
7047
7281
|
if (!resolvedReal) {
|
|
7048
7282
|
sendError(res, "workdir must exist", 403);
|
|
7049
7283
|
return true;
|
|
7050
7284
|
}
|
|
7051
|
-
const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 +
|
|
7285
|
+
const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 + path7.sep));
|
|
7052
7286
|
if (!isAllowed) {
|
|
7053
7287
|
sendError(res, "workdir must be within workspace base directory or cwd", 403);
|
|
7054
7288
|
return true;
|
|
@@ -7794,5 +8028,5 @@ export {
|
|
|
7794
8028
|
CodingWorkspaceService
|
|
7795
8029
|
};
|
|
7796
8030
|
|
|
7797
|
-
//# debugId=
|
|
8031
|
+
//# debugId=5999C4AE1407A27464756E2164756E21
|
|
7798
8032
|
//# sourceMappingURL=index.js.map
|