@elizaos/plugin-agent-orchestrator 0.3.19 → 0.4.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/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|Forging|Shenaniganing|Inferring|Cooking|Brewing|Loading|Scheming|Pondering|Conjuring|Manifesting|Reflecting|Synthesizing|Vibing|Summoning|Compiling|processing|Elucidating|Cogitat\w+|Bak\w+)(?:…|\.{3})?(?:\s*\(.*\))?\s*$/i;
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"), do NOT use "complete" yet. ` + `Instead respond with "Review your PR, run each test plan item to verify it works, update the PR to check off each item, then confirm all items pass".
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 OVERALL task is done or if more work is needed.
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
- ` + `Guidelines:
278
- ` + `- BEFORE choosing "complete", enumerate each objective from the original task and verify ` + `evidence in the output. If ANY objective lacks evidence, use "respond" with the missing work.
279
- ` + `- A PR being created does NOT mean the task is done check that the PR covers ALL requested changes.
280
- ` + `- If the task mentions multiple features/fixes, verify EACH one is addressed, not just the first.
281
- ` + `- If the agent only analyzed code or read files, it hasn't done the actual work yet — send a follow-up.
282
- ` + `- If the agent wrote code but didn't test it and testing seems appropriate, ask it to run tests.
283
- ` + `- If the output shows errors or failed tests, send a follow-up to fix them.
284
- ` + `- IMPORTANT: If the working directory is a git repository clone (not a scratch dir), the agent ` + `MUST commit its changes, push them, and create a pull request before the task can be "complete". ` + `If the output only shows code edits with no git commit or PR, respond with "Now commit your changes, push, and create a pull request".
285
- ` + `- IMPORTANT: Creating a PR is NOT the final step. If this is the turn where the PR was created ` + `(i.e. "Created pull request" or a PR URL appears for the FIRST time and no previous decision ` + `already sent a review follow-up), respond with "Review your PR, run each test plan item to verify ` + `it works, update the PR to check off each item, then confirm all items pass".
286
- ` + `- If a previous decision ALREADY sent a review/verification follow-up (check the decision history), ` + `and the agent has now responded with its review results, you MAY mark "complete" if the agent ` + `indicates the work is done (e.g. "Done", "verified", "all checks pass", "Here's what I did", ` + `or a clear summary of completed work). Do NOT require exact phrases — use judgment.
287
- ` + `- Keep follow-up instructions concise and specific.
288
- ` + `- When asking agents to verify work, prefer CLI tools (gh, curl, cat, git diff, etc.) over ` + `browser automation. Browser tools may not be available in headless environments and can cause delays.
289
- ` + `- Default to "respond" only use "complete" when you're certain ALL work is done.
290
- ` + `- 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.
291
- ` + `- Look for explicit "DECISION:" markers in the agent's output — these are the agent deliberately ` + `surfacing design choices. Always capture these as keyDecision.
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, respond to review & verify test plan items before completing.
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" — ALL task objectives met (code written, committed, PR created & verified)
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" — should not normally be used here
354
+ ` + `- "ignore" — spinner/loading output, agent still working
359
355
 
360
356
  ` + `Guidelines:
361
- ` + `- Verify evidence for EVERY objective before using "complete".
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
- ` + `- If a PR was just created, respond to review & verify test plan items.
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 os from "node:os";
2287
- import * as path2 from "node:path";
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 readFile2, writeFile as writeFile2 } from "node:fs/promises";
2294
- import { dirname, join as join2 } from "node:path";
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
- - If a PR was just created, respond to review & verify test plan items before completing.
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 UNREGISTERED_BUFFER_MS = 2000;
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
- setTimeout(() => {
3694
- const stillBuffered = this.unregisteredBuffer.get(sessionId);
3695
- if (stillBuffered && stillBuffered.length > 0) {
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
- await handleTurnComplete(this, sessionId, taskCtx, data);
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 = join2(workdir, ".claude", "settings.json");
4396
+ const settingsPath = join3(workdir, ".claude", "settings.json");
4173
4397
  let settings = {};
4174
4398
  try {
4175
- settings = JSON.parse(await readFile2(settingsPath, "utf-8"));
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 mkdir(dirname(settingsPath), { recursive: true });
4191
- await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
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 = join2(workdir, ".gemini", "settings.json");
4423
+ const settingsPath = join3(workdir, ".gemini", "settings.json");
4200
4424
  let settings = {};
4201
4425
  try {
4202
- settings = JSON.parse(await readFile2(settingsPath, "utf-8"));
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 mkdir(dirname(settingsPath), { recursive: true });
4215
- await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
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 = join2(workdir, ".gitignore");
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 readFile2(gitignorePath, "utf-8");
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 writeFile2(gitignorePath, entries.join(`
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 appendFile(gitignorePath, separator + entries.join(`
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 = path2.resolve(workdir);
4691
- const workspaceBaseDir = path2.join(os.homedir(), ".milady", "workspaces");
4914
+ const resolvedWorkdir = path3.resolve(workdir);
4915
+ const workspaceBaseDir = path3.join(os2.homedir(), ".milady", "workspaces");
4692
4916
  const allowedPrefixes = [
4693
- path2.resolve(workspaceBaseDir),
4694
- path2.resolve(process.cwd())
4917
+ path3.resolve(workspaceBaseDir),
4918
+ path3.resolve(process.cwd())
4695
4919
  ];
4696
- const isAllowed = allowedPrefixes.some((prefix) => resolvedWorkdir.startsWith(prefix + path2.sep) || resolvedWorkdir === 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 fs from "node:fs";
5049
- import * as os2 from "node:os";
5050
- import * as path3 from "node:path";
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 = path3.join(os2.homedir(), ".milady", "workspaces");
5279
+ const baseDir = path4.join(os3.homedir(), ".milady", "workspaces");
5056
5280
  const scratchId = randomUUID();
5057
- const scratchDir = path3.join(baseDir, scratchId);
5058
- fs.mkdirSync(scratchDir, { recursive: true });
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
- if (!repo) {
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?.getLastUsedRepo();
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) {
@@ -5512,7 +5746,8 @@ var startCodingTaskAction = {
5512
5746
  return handleMultiAgent(ctx, agentsParam);
5513
5747
  }
5514
5748
  const task = params?.task ?? content.task;
5515
- const singleAgentSpec = task || "";
5749
+ const userText = content.text?.trim() || "";
5750
+ const singleAgentSpec = task || userText;
5516
5751
  return handleMultiAgent(ctx, singleAgentSpec);
5517
5752
  },
5518
5753
  parameters: [
@@ -6026,9 +6261,9 @@ var activeWorkspaceContextProvider = {
6026
6261
  };
6027
6262
 
6028
6263
  // src/services/workspace-service.ts
6029
- import * as os3 from "node:os";
6030
- import * as path5 from "node:path";
6031
- import * as fs3 from "node:fs/promises";
6264
+ import * as os4 from "node:os";
6265
+ import * as path6 from "node:path";
6266
+ import * as fs4 from "node:fs/promises";
6032
6267
  import {
6033
6268
  CredentialService,
6034
6269
  GitHubPatClient as GitHubPatClient2,
@@ -6241,17 +6476,17 @@ async function createPR(workspaceService, workspace, workspaceId, options, log)
6241
6476
  }
6242
6477
 
6243
6478
  // src/services/workspace-lifecycle.ts
6244
- import * as fs2 from "node:fs";
6245
- import * as path4 from "node:path";
6479
+ import * as fs3 from "node:fs";
6480
+ import * as path5 from "node:path";
6246
6481
  async function removeScratchDir(dirPath, baseDir, log) {
6247
- const resolved = path4.resolve(dirPath);
6248
- const resolvedBase = path4.resolve(baseDir) + path4.sep;
6249
- if (!resolved.startsWith(resolvedBase) && resolved !== path4.resolve(baseDir)) {
6482
+ const resolved = path5.resolve(dirPath);
6483
+ const resolvedBase = path5.resolve(baseDir) + path5.sep;
6484
+ if (!resolved.startsWith(resolvedBase) && resolved !== path5.resolve(baseDir)) {
6250
6485
  console.warn(`[CodingWorkspaceService] Refusing to remove dir outside base: ${resolved}`);
6251
6486
  return;
6252
6487
  }
6253
6488
  try {
6254
- await fs2.promises.rm(resolved, { recursive: true, force: true });
6489
+ await fs3.promises.rm(resolved, { recursive: true, force: true });
6255
6490
  log(`Removed scratch dir ${resolved}`);
6256
6491
  } catch (err) {
6257
6492
  console.warn(`[CodingWorkspaceService] Failed to remove scratch dir ${resolved}:`, err);
@@ -6264,7 +6499,7 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
6264
6499
  }
6265
6500
  let entries;
6266
6501
  try {
6267
- entries = await fs2.promises.readdir(baseDir, { withFileTypes: true });
6502
+ entries = await fs3.promises.readdir(baseDir, { withFileTypes: true });
6268
6503
  } catch {
6269
6504
  return;
6270
6505
  }
@@ -6278,12 +6513,12 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
6278
6513
  skipped++;
6279
6514
  continue;
6280
6515
  }
6281
- const dirPath = path4.join(baseDir, entry.name);
6516
+ const dirPath = path5.join(baseDir, entry.name);
6282
6517
  try {
6283
- const stat = await fs2.promises.stat(dirPath);
6284
- const age = now - stat.mtimeMs;
6518
+ const stat2 = await fs3.promises.stat(dirPath);
6519
+ const age = now - stat2.mtimeMs;
6285
6520
  if (age > workspaceTtlMs) {
6286
- await fs2.promises.rm(dirPath, { recursive: true, force: true });
6521
+ await fs3.promises.rm(dirPath, { recursive: true, force: true });
6287
6522
  removed++;
6288
6523
  } else {
6289
6524
  skipped++;
@@ -6317,7 +6552,7 @@ class CodingWorkspaceService {
6317
6552
  constructor(runtime, config = {}) {
6318
6553
  this.runtime = runtime;
6319
6554
  this.serviceConfig = {
6320
- baseDir: config.baseDir ?? path5.join(os3.homedir(), ".milady", "workspaces"),
6555
+ baseDir: config.baseDir ?? path6.join(os4.homedir(), ".milady", "workspaces"),
6321
6556
  branchPrefix: config.branchPrefix ?? "milady",
6322
6557
  debug: config.debug ?? false,
6323
6558
  workspaceTtlMs: config.workspaceTtlMs ?? 24 * 60 * 60 * 1000
@@ -6636,14 +6871,14 @@ class CodingWorkspaceService {
6636
6871
  const suggestedName = this.sanitizeWorkspaceName(name || record.label);
6637
6872
  const targetPath = await this.allocatePromotedPath(baseDir, suggestedName);
6638
6873
  try {
6639
- await fs3.rename(record.path, targetPath);
6874
+ await fs4.rename(record.path, targetPath);
6640
6875
  } catch (error) {
6641
6876
  const isExdev = typeof error === "object" && error !== null && "code" in error && error.code === "EXDEV";
6642
6877
  if (!isExdev)
6643
6878
  throw error;
6644
- await fs3.cp(record.path, targetPath, { recursive: true });
6645
- await fs3.access(targetPath);
6646
- await fs3.rm(record.path, { recursive: true, force: true });
6879
+ await fs4.cp(record.path, targetPath, { recursive: true });
6880
+ await fs4.access(targetPath);
6881
+ await fs4.rm(record.path, { recursive: true, force: true });
6647
6882
  }
6648
6883
  const next = {
6649
6884
  ...record,
@@ -6716,15 +6951,15 @@ class CodingWorkspaceService {
6716
6951
  return compact || `scratch-${Date.now().toString(36)}`;
6717
6952
  }
6718
6953
  async allocatePromotedPath(baseDir, baseName) {
6719
- const baseResolved = path5.resolve(baseDir);
6954
+ const baseResolved = path6.resolve(baseDir);
6720
6955
  for (let i = 0;i < 1000; i++) {
6721
6956
  const candidateName = i === 0 ? baseName : `${baseName}-${i}`;
6722
- const candidate = path5.resolve(baseResolved, candidateName);
6723
- if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path5.sep}`)) {
6957
+ const candidate = path6.resolve(baseResolved, candidateName);
6958
+ if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path6.sep}`)) {
6724
6959
  continue;
6725
6960
  }
6726
6961
  try {
6727
- await fs3.access(candidate);
6962
+ await fs4.access(candidate);
6728
6963
  } catch {
6729
6964
  return candidate;
6730
6965
  }
@@ -6733,10 +6968,10 @@ class CodingWorkspaceService {
6733
6968
  }
6734
6969
  }
6735
6970
  // src/api/agent-routes.ts
6736
- import { access as access2, readFile as readFile3, realpath, rm as rm2 } from "node:fs/promises";
6971
+ import { access as access2, readFile as readFile4, realpath, rm as rm2 } from "node:fs/promises";
6737
6972
  import { createHash } from "node:crypto";
6738
- import * as os4 from "node:os";
6739
- import * as path6 from "node:path";
6973
+ import * as os5 from "node:os";
6974
+ import * as path7 from "node:path";
6740
6975
  import { execFile } from "node:child_process";
6741
6976
  import { promisify } from "node:util";
6742
6977
  var execFileAsync = promisify(execFile);
@@ -6748,23 +6983,23 @@ function shouldAutoPreflight() {
6748
6983
  return false;
6749
6984
  }
6750
6985
  function isPathInside(parent, candidate) {
6751
- return candidate === parent || candidate.startsWith(`${parent}${path6.sep}`);
6986
+ return candidate === parent || candidate.startsWith(`${parent}${path7.sep}`);
6752
6987
  }
6753
6988
  async function resolveSafeVenvPath(workdir, venvDirRaw) {
6754
6989
  const venvDir = venvDirRaw.trim();
6755
6990
  if (!venvDir) {
6756
6991
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be non-empty");
6757
6992
  }
6758
- if (path6.isAbsolute(venvDir)) {
6993
+ if (path7.isAbsolute(venvDir)) {
6759
6994
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be relative to workdir");
6760
6995
  }
6761
- const normalized = path6.normalize(venvDir);
6762
- if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path6.sep}`)) {
6996
+ const normalized = path7.normalize(venvDir);
6997
+ if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path7.sep}`)) {
6763
6998
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must stay within workdir");
6764
6999
  }
6765
- const workdirResolved = path6.resolve(workdir);
7000
+ const workdirResolved = path7.resolve(workdir);
6766
7001
  const workdirReal = await realpath(workdirResolved);
6767
- const resolved = path6.resolve(workdirReal, normalized);
7002
+ const resolved = path7.resolve(workdirReal, normalized);
6768
7003
  if (!isPathInside(workdirReal, resolved)) {
6769
7004
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV resolves outside workdir");
6770
7005
  }
@@ -6780,7 +7015,7 @@ async function resolveSafeVenvPath(workdir, venvDirRaw) {
6780
7015
  const maybeErr = err;
6781
7016
  if (maybeErr?.code !== "ENOENT")
6782
7017
  throw err;
6783
- const parentReal = await realpath(path6.dirname(resolved));
7018
+ const parentReal = await realpath(path7.dirname(resolved));
6784
7019
  if (!isPathInside(workdirReal, parentReal)) {
6785
7020
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV parent resolves outside workdir");
6786
7021
  }
@@ -6796,10 +7031,10 @@ async function fileExists(filePath) {
6796
7031
  }
6797
7032
  }
6798
7033
  async function resolveRequirementsPath(workdir) {
6799
- const workdirReal = await realpath(path6.resolve(workdir));
7034
+ const workdirReal = await realpath(path7.resolve(workdir));
6800
7035
  const candidates = [
6801
- path6.join(workdir, "apps", "api", "requirements.txt"),
6802
- path6.join(workdir, "requirements.txt")
7036
+ path7.join(workdir, "apps", "api", "requirements.txt"),
7037
+ path7.join(workdir, "requirements.txt")
6803
7038
  ];
6804
7039
  for (const candidate of candidates) {
6805
7040
  if (!await fileExists(candidate))
@@ -6813,7 +7048,7 @@ async function resolveRequirementsPath(workdir) {
6813
7048
  return null;
6814
7049
  }
6815
7050
  async function fingerprintRequirementsFile(requirementsPath) {
6816
- const file = await readFile3(requirementsPath);
7051
+ const file = await readFile4(requirementsPath);
6817
7052
  return createHash("sha256").update(file).digest("hex");
6818
7053
  }
6819
7054
  async function runBenchmarkPreflight(workdir) {
@@ -6826,7 +7061,7 @@ async function runBenchmarkPreflight(workdir) {
6826
7061
  const mode = process.env.PARALLAX_BENCHMARK_PREFLIGHT_MODE?.toLowerCase() === "warm" ? "warm" : "cold";
6827
7062
  const venvDir = process.env.PARALLAX_BENCHMARK_PREFLIGHT_VENV || ".benchmark-venv";
6828
7063
  const venvPath = await resolveSafeVenvPath(workdir, venvDir);
6829
- const pythonInVenv = path6.join(venvPath, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
7064
+ const pythonInVenv = path7.join(venvPath, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
6830
7065
  const key = `${workdir}::${mode}::${venvPath}::${requirementsFingerprint}`;
6831
7066
  if (PREFLIGHT_DONE.has(key)) {
6832
7067
  if (await fileExists(pythonInVenv))
@@ -7034,21 +7269,21 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
7034
7269
  customCredentials,
7035
7270
  metadata
7036
7271
  } = body;
7037
- const workspaceBaseDir = path6.join(os4.homedir(), ".milady", "workspaces");
7038
- const workspaceBaseDirResolved = path6.resolve(workspaceBaseDir);
7039
- const cwdResolved = path6.resolve(process.cwd());
7272
+ const workspaceBaseDir = path7.join(os5.homedir(), ".milady", "workspaces");
7273
+ const workspaceBaseDirResolved = path7.resolve(workspaceBaseDir);
7274
+ const cwdResolved = path7.resolve(process.cwd());
7040
7275
  const workspaceBaseDirReal = await realpath(workspaceBaseDirResolved).catch(() => workspaceBaseDirResolved);
7041
7276
  const cwdReal = await realpath(cwdResolved).catch(() => cwdResolved);
7042
7277
  const allowedPrefixes = [workspaceBaseDirReal, cwdReal];
7043
7278
  let workdir = rawWorkdir;
7044
7279
  if (workdir) {
7045
- const resolved = path6.resolve(workdir);
7280
+ const resolved = path7.resolve(workdir);
7046
7281
  const resolvedReal = await realpath(resolved).catch(() => null);
7047
7282
  if (!resolvedReal) {
7048
7283
  sendError(res, "workdir must exist", 403);
7049
7284
  return true;
7050
7285
  }
7051
- const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 + path6.sep));
7286
+ const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 + path7.sep));
7052
7287
  if (!isAllowed) {
7053
7288
  sendError(res, "workdir must be within workspace base directory or cwd", 403);
7054
7289
  return true;
@@ -7794,5 +8029,5 @@ export {
7794
8029
  CodingWorkspaceService
7795
8030
  };
7796
8031
 
7797
- //# debugId=62DE82DAFB3D448A64756E2164756E21
8032
+ //# debugId=C832944FABDC8E0964756E2164756E21
7798
8033
  //# sourceMappingURL=index.js.map