@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/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) {
@@ -6026,9 +6260,9 @@ var activeWorkspaceContextProvider = {
6026
6260
  };
6027
6261
 
6028
6262
  // 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";
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 fs2 from "node:fs";
6245
- import * as path4 from "node:path";
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 = path4.resolve(dirPath);
6248
- const resolvedBase = path4.resolve(baseDir) + path4.sep;
6249
- if (!resolved.startsWith(resolvedBase) && resolved !== path4.resolve(baseDir)) {
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 fs2.promises.rm(resolved, { recursive: true, force: true });
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 fs2.promises.readdir(baseDir, { withFileTypes: true });
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 = path4.join(baseDir, entry.name);
6515
+ const dirPath = path5.join(baseDir, entry.name);
6282
6516
  try {
6283
- const stat = await fs2.promises.stat(dirPath);
6284
- const age = now - stat.mtimeMs;
6517
+ const stat2 = await fs3.promises.stat(dirPath);
6518
+ const age = now - stat2.mtimeMs;
6285
6519
  if (age > workspaceTtlMs) {
6286
- await fs2.promises.rm(dirPath, { recursive: true, force: true });
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 ?? path5.join(os3.homedir(), ".milady", "workspaces"),
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 fs3.rename(record.path, targetPath);
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 fs3.cp(record.path, targetPath, { recursive: true });
6645
- await fs3.access(targetPath);
6646
- await fs3.rm(record.path, { recursive: true, force: true });
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 = path5.resolve(baseDir);
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 = path5.resolve(baseResolved, candidateName);
6723
- if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path5.sep}`)) {
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 fs3.access(candidate);
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 readFile3, realpath, rm as rm2 } from "node:fs/promises";
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 os4 from "node:os";
6739
- import * as path6 from "node:path";
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}${path6.sep}`);
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 (path6.isAbsolute(venvDir)) {
6992
+ if (path7.isAbsolute(venvDir)) {
6759
6993
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be relative to workdir");
6760
6994
  }
6761
- const normalized = path6.normalize(venvDir);
6762
- if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path6.sep}`)) {
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 = path6.resolve(workdir);
6999
+ const workdirResolved = path7.resolve(workdir);
6766
7000
  const workdirReal = await realpath(workdirResolved);
6767
- const resolved = path6.resolve(workdirReal, normalized);
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(path6.dirname(resolved));
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(path6.resolve(workdir));
7033
+ const workdirReal = await realpath(path7.resolve(workdir));
6800
7034
  const candidates = [
6801
- path6.join(workdir, "apps", "api", "requirements.txt"),
6802
- path6.join(workdir, "requirements.txt")
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 readFile3(requirementsPath);
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 = path6.join(venvPath, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
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 = path6.join(os4.homedir(), ".milady", "workspaces");
7038
- const workspaceBaseDirResolved = path6.resolve(workspaceBaseDir);
7039
- const cwdResolved = path6.resolve(process.cwd());
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 = path6.resolve(workdir);
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 + path6.sep));
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=62DE82DAFB3D448A64756E2164756E21
8031
+ //# debugId=5999C4AE1407A27464756E2164756E21
7798
8032
  //# sourceMappingURL=index.js.map