@elizaos/plugin-agent-orchestrator 0.3.18 → 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.
270
+ ` + `The agent completed a turn. Decide if the task is done or needs more work.
274
271
 
275
- ` + `4. "ignore" — Should not normally be used here.
276
-
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) {
@@ -5190,7 +5414,8 @@ ${taskList}
5190
5414
  try {
5191
5415
  const result = await withTrajectoryContext(runtime, { source: "orchestrator", decisionType: "swarm-context-generation" }, () => runtime.useModel(ModelType5.TEXT_SMALL, {
5192
5416
  prompt,
5193
- temperature: 0.3
5417
+ temperature: 0.3,
5418
+ stream: false
5194
5419
  }));
5195
5420
  return result?.trim() || "";
5196
5421
  } catch (err) {
@@ -5217,12 +5442,7 @@ async function handleMultiAgent(ctx, agentsParam) {
5217
5442
  } = ctx;
5218
5443
  const agentSpecs = agentsParam.split("|").map((s) => s.trim()).filter(Boolean);
5219
5444
  if (agentSpecs.length === 0) {
5220
- if (callback) {
5221
- await callback({
5222
- text: "No agent tasks provided in agents parameter."
5223
- });
5224
- }
5225
- return { success: false, error: "EMPTY_AGENTS_PARAM" };
5445
+ agentSpecs.push("");
5226
5446
  }
5227
5447
  if (agentSpecs.length > MAX_CONCURRENT_AGENTS) {
5228
5448
  if (callback) {
@@ -5397,169 +5617,6 @@ ${swarmContext}
5397
5617
  data: { agents: results }
5398
5618
  };
5399
5619
  }
5400
- async function handleSingleAgent(ctx, task) {
5401
- logger6.debug(`[START_CODING_TASK] handleSingleAgent called, agentType=${ctx.defaultAgentType}, task=${task ? "yes" : "none"}, repo=${ctx.repo ?? "none"}`);
5402
- const {
5403
- runtime,
5404
- ptyService,
5405
- wsService,
5406
- credentials,
5407
- customCredentials,
5408
- callback,
5409
- message,
5410
- state,
5411
- repo,
5412
- defaultAgentType: agentType,
5413
- rawAgentType,
5414
- memoryContent,
5415
- approvalPreset,
5416
- explicitLabel
5417
- } = ctx;
5418
- const label = explicitLabel || generateLabel(repo, task);
5419
- let workdir;
5420
- let workspaceId;
5421
- let branch;
5422
- if (repo) {
5423
- if (!wsService) {
5424
- if (callback) {
5425
- await callback({
5426
- text: "Workspace Service is not available. Cannot clone repository."
5427
- });
5428
- }
5429
- return { success: false, error: "WORKSPACE_SERVICE_UNAVAILABLE" };
5430
- }
5431
- try {
5432
- if (callback) {
5433
- await callback({ text: `Cloning ${repo}...` });
5434
- }
5435
- const workspace = await wsService.provisionWorkspace({ repo });
5436
- workdir = workspace.path;
5437
- workspaceId = workspace.id;
5438
- branch = workspace.branch;
5439
- wsService.setLabel(workspace.id, label);
5440
- if (state) {
5441
- state.codingWorkspace = {
5442
- id: workspace.id,
5443
- path: workspace.path,
5444
- branch: workspace.branch,
5445
- isWorktree: workspace.isWorktree,
5446
- label
5447
- };
5448
- }
5449
- } catch (error) {
5450
- const errorMessage = error instanceof Error ? error.message : String(error);
5451
- if (callback) {
5452
- await callback({
5453
- text: `Failed to clone repository: ${errorMessage}`
5454
- });
5455
- }
5456
- return { success: false, error: errorMessage };
5457
- }
5458
- } else {
5459
- workdir = createScratchDir();
5460
- }
5461
- logger6.debug(`[START_CODING_TASK] Spawning ${agentType} agent, task: ${task ? `"${task.slice(0, 80)}..."` : "(none)"}, workdir: ${workdir}`);
5462
- try {
5463
- if (agentType !== "shell" && agentType !== "pi") {
5464
- const [preflight] = await ptyService.checkAvailableAgents([
5465
- agentType
5466
- ]);
5467
- if (preflight && !preflight.installed) {
5468
- logger6.warn(`[START_CODING_TASK] ${preflight.adapter} CLI not installed`);
5469
- if (callback) {
5470
- await callback({
5471
- text: `${preflight.adapter} CLI is not installed.
5472
- Install with: ${preflight.installCommand}
5473
- Docs: ${preflight.docsUrl}`
5474
- });
5475
- }
5476
- return { success: false, error: "AGENT_NOT_INSTALLED" };
5477
- }
5478
- logger6.debug(`[START_CODING_TASK] Preflight OK: ${preflight?.adapter} installed`);
5479
- }
5480
- const piRequested = isPiAgentType(rawAgentType);
5481
- const initialTask = piRequested ? toPiCommand(task) : task;
5482
- const displayType = piRequested ? "pi" : agentType;
5483
- const pastExperience = await queryPastExperience(runtime, {
5484
- taskDescription: task,
5485
- lookbackHours: 48,
5486
- maxEntries: 6,
5487
- repo
5488
- });
5489
- const pastExperienceBlock = formatPastExperience(pastExperience);
5490
- const agentMemory = [memoryContent, pastExperienceBlock].filter(Boolean).join(`
5491
-
5492
- `) || undefined;
5493
- const coordinator = getCoordinator(runtime);
5494
- logger6.debug(`[START_CODING_TASK] Calling spawnSession (${agentType}, coordinator=${!!coordinator})`);
5495
- const session = await ptyService.spawnSession({
5496
- name: `coding-${Date.now()}`,
5497
- agentType,
5498
- workdir,
5499
- initialTask,
5500
- memoryContent: agentMemory,
5501
- credentials,
5502
- approvalPreset: approvalPreset ?? ptyService.defaultApprovalPreset,
5503
- customCredentials,
5504
- ...coordinator ? { skipAdapterAutoResponse: true } : {},
5505
- metadata: {
5506
- requestedType: rawAgentType,
5507
- messageId: message.id,
5508
- userId: message.userId,
5509
- workspaceId,
5510
- label
5511
- }
5512
- });
5513
- logger6.debug(`[START_CODING_TASK] Session spawned: ${session.id} (${session.status})`);
5514
- const isScratchWorkspace = !repo;
5515
- const scratchDir = isScratchWorkspace ? workdir : null;
5516
- registerSessionEvents(ptyService, runtime, session.id, label, scratchDir, callback, !!coordinator);
5517
- if (coordinator && task) {
5518
- coordinator.registerTask(session.id, {
5519
- agentType,
5520
- label,
5521
- originalTask: task,
5522
- workdir,
5523
- repo
5524
- });
5525
- }
5526
- if (state) {
5527
- state.codingSession = {
5528
- id: session.id,
5529
- agentType: session.agentType,
5530
- workdir: session.workdir,
5531
- status: session.status
5532
- };
5533
- }
5534
- const summary = repo ? `Cloned ${repo} and started ${displayType} agent as "${label}"${task ? ` with task: "${task}"` : ""}` : `Started ${displayType} agent as "${label}" in scratch workspace${task ? ` with task: "${task}"` : ""}`;
5535
- if (callback) {
5536
- await callback({ text: `${summary}
5537
- Session ID: ${session.id}` });
5538
- }
5539
- return {
5540
- success: true,
5541
- text: summary,
5542
- data: {
5543
- sessionId: session.id,
5544
- agentType: displayType,
5545
- workdir: session.workdir,
5546
- workspaceId,
5547
- branch,
5548
- label,
5549
- status: session.status
5550
- }
5551
- };
5552
- } catch (error) {
5553
- const errorMessage = error instanceof Error ? error.message : String(error);
5554
- logger6.error("[START_CODING_TASK] Failed to spawn agent:", errorMessage);
5555
- if (callback) {
5556
- await callback({
5557
- text: `Failed to start coding agent: ${errorMessage}`
5558
- });
5559
- }
5560
- return { success: false, error: errorMessage };
5561
- }
5562
- }
5563
5620
 
5564
5621
  // src/actions/start-coding-task.ts
5565
5622
  var startCodingTaskAction = {
@@ -5633,13 +5690,23 @@ var startCodingTaskAction = {
5633
5690
  repo = urlMatch[0];
5634
5691
  }
5635
5692
  }
5636
- 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) {
5637
5695
  const coordinator = getCoordinator(runtime);
5638
- const lastRepo = coordinator?.getLastUsedRepo();
5696
+ const lastRepo = await coordinator?.getLastUsedRepoAsync();
5639
5697
  if (lastRepo) {
5640
5698
  repo = lastRepo;
5641
5699
  }
5642
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
+ }
5643
5710
  const customCredentialKeys = runtime.getSetting("CUSTOM_CREDENTIAL_KEYS");
5644
5711
  let customCredentials;
5645
5712
  if (customCredentialKeys) {
@@ -5679,7 +5746,8 @@ var startCodingTaskAction = {
5679
5746
  return handleMultiAgent(ctx, agentsParam);
5680
5747
  }
5681
5748
  const task = params?.task ?? content.task;
5682
- return handleSingleAgent(ctx, task);
5749
+ const singleAgentSpec = task || "";
5750
+ return handleMultiAgent(ctx, singleAgentSpec);
5683
5751
  },
5684
5752
  parameters: [
5685
5753
  {
@@ -6192,9 +6260,9 @@ var activeWorkspaceContextProvider = {
6192
6260
  };
6193
6261
 
6194
6262
  // src/services/workspace-service.ts
6195
- import * as os3 from "node:os";
6196
- import * as path5 from "node:path";
6197
- 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";
6198
6266
  import {
6199
6267
  CredentialService,
6200
6268
  GitHubPatClient as GitHubPatClient2,
@@ -6407,17 +6475,17 @@ async function createPR(workspaceService, workspace, workspaceId, options, log)
6407
6475
  }
6408
6476
 
6409
6477
  // src/services/workspace-lifecycle.ts
6410
- import * as fs2 from "node:fs";
6411
- import * as path4 from "node:path";
6478
+ import * as fs3 from "node:fs";
6479
+ import * as path5 from "node:path";
6412
6480
  async function removeScratchDir(dirPath, baseDir, log) {
6413
- const resolved = path4.resolve(dirPath);
6414
- const resolvedBase = path4.resolve(baseDir) + path4.sep;
6415
- 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)) {
6416
6484
  console.warn(`[CodingWorkspaceService] Refusing to remove dir outside base: ${resolved}`);
6417
6485
  return;
6418
6486
  }
6419
6487
  try {
6420
- await fs2.promises.rm(resolved, { recursive: true, force: true });
6488
+ await fs3.promises.rm(resolved, { recursive: true, force: true });
6421
6489
  log(`Removed scratch dir ${resolved}`);
6422
6490
  } catch (err) {
6423
6491
  console.warn(`[CodingWorkspaceService] Failed to remove scratch dir ${resolved}:`, err);
@@ -6430,7 +6498,7 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
6430
6498
  }
6431
6499
  let entries;
6432
6500
  try {
6433
- entries = await fs2.promises.readdir(baseDir, { withFileTypes: true });
6501
+ entries = await fs3.promises.readdir(baseDir, { withFileTypes: true });
6434
6502
  } catch {
6435
6503
  return;
6436
6504
  }
@@ -6444,12 +6512,12 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
6444
6512
  skipped++;
6445
6513
  continue;
6446
6514
  }
6447
- const dirPath = path4.join(baseDir, entry.name);
6515
+ const dirPath = path5.join(baseDir, entry.name);
6448
6516
  try {
6449
- const stat = await fs2.promises.stat(dirPath);
6450
- const age = now - stat.mtimeMs;
6517
+ const stat2 = await fs3.promises.stat(dirPath);
6518
+ const age = now - stat2.mtimeMs;
6451
6519
  if (age > workspaceTtlMs) {
6452
- await fs2.promises.rm(dirPath, { recursive: true, force: true });
6520
+ await fs3.promises.rm(dirPath, { recursive: true, force: true });
6453
6521
  removed++;
6454
6522
  } else {
6455
6523
  skipped++;
@@ -6483,7 +6551,7 @@ class CodingWorkspaceService {
6483
6551
  constructor(runtime, config = {}) {
6484
6552
  this.runtime = runtime;
6485
6553
  this.serviceConfig = {
6486
- baseDir: config.baseDir ?? path5.join(os3.homedir(), ".milady", "workspaces"),
6554
+ baseDir: config.baseDir ?? path6.join(os4.homedir(), ".milady", "workspaces"),
6487
6555
  branchPrefix: config.branchPrefix ?? "milady",
6488
6556
  debug: config.debug ?? false,
6489
6557
  workspaceTtlMs: config.workspaceTtlMs ?? 24 * 60 * 60 * 1000
@@ -6802,14 +6870,14 @@ class CodingWorkspaceService {
6802
6870
  const suggestedName = this.sanitizeWorkspaceName(name || record.label);
6803
6871
  const targetPath = await this.allocatePromotedPath(baseDir, suggestedName);
6804
6872
  try {
6805
- await fs3.rename(record.path, targetPath);
6873
+ await fs4.rename(record.path, targetPath);
6806
6874
  } catch (error) {
6807
6875
  const isExdev = typeof error === "object" && error !== null && "code" in error && error.code === "EXDEV";
6808
6876
  if (!isExdev)
6809
6877
  throw error;
6810
- await fs3.cp(record.path, targetPath, { recursive: true });
6811
- await fs3.access(targetPath);
6812
- 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 });
6813
6881
  }
6814
6882
  const next = {
6815
6883
  ...record,
@@ -6882,15 +6950,15 @@ class CodingWorkspaceService {
6882
6950
  return compact || `scratch-${Date.now().toString(36)}`;
6883
6951
  }
6884
6952
  async allocatePromotedPath(baseDir, baseName) {
6885
- const baseResolved = path5.resolve(baseDir);
6953
+ const baseResolved = path6.resolve(baseDir);
6886
6954
  for (let i = 0;i < 1000; i++) {
6887
6955
  const candidateName = i === 0 ? baseName : `${baseName}-${i}`;
6888
- const candidate = path5.resolve(baseResolved, candidateName);
6889
- if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path5.sep}`)) {
6956
+ const candidate = path6.resolve(baseResolved, candidateName);
6957
+ if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path6.sep}`)) {
6890
6958
  continue;
6891
6959
  }
6892
6960
  try {
6893
- await fs3.access(candidate);
6961
+ await fs4.access(candidate);
6894
6962
  } catch {
6895
6963
  return candidate;
6896
6964
  }
@@ -6899,10 +6967,10 @@ class CodingWorkspaceService {
6899
6967
  }
6900
6968
  }
6901
6969
  // src/api/agent-routes.ts
6902
- 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";
6903
6971
  import { createHash } from "node:crypto";
6904
- import * as os4 from "node:os";
6905
- import * as path6 from "node:path";
6972
+ import * as os5 from "node:os";
6973
+ import * as path7 from "node:path";
6906
6974
  import { execFile } from "node:child_process";
6907
6975
  import { promisify } from "node:util";
6908
6976
  var execFileAsync = promisify(execFile);
@@ -6914,23 +6982,23 @@ function shouldAutoPreflight() {
6914
6982
  return false;
6915
6983
  }
6916
6984
  function isPathInside(parent, candidate) {
6917
- return candidate === parent || candidate.startsWith(`${parent}${path6.sep}`);
6985
+ return candidate === parent || candidate.startsWith(`${parent}${path7.sep}`);
6918
6986
  }
6919
6987
  async function resolveSafeVenvPath(workdir, venvDirRaw) {
6920
6988
  const venvDir = venvDirRaw.trim();
6921
6989
  if (!venvDir) {
6922
6990
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be non-empty");
6923
6991
  }
6924
- if (path6.isAbsolute(venvDir)) {
6992
+ if (path7.isAbsolute(venvDir)) {
6925
6993
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be relative to workdir");
6926
6994
  }
6927
- const normalized = path6.normalize(venvDir);
6928
- if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path6.sep}`)) {
6995
+ const normalized = path7.normalize(venvDir);
6996
+ if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path7.sep}`)) {
6929
6997
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must stay within workdir");
6930
6998
  }
6931
- const workdirResolved = path6.resolve(workdir);
6999
+ const workdirResolved = path7.resolve(workdir);
6932
7000
  const workdirReal = await realpath(workdirResolved);
6933
- const resolved = path6.resolve(workdirReal, normalized);
7001
+ const resolved = path7.resolve(workdirReal, normalized);
6934
7002
  if (!isPathInside(workdirReal, resolved)) {
6935
7003
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV resolves outside workdir");
6936
7004
  }
@@ -6946,7 +7014,7 @@ async function resolveSafeVenvPath(workdir, venvDirRaw) {
6946
7014
  const maybeErr = err;
6947
7015
  if (maybeErr?.code !== "ENOENT")
6948
7016
  throw err;
6949
- const parentReal = await realpath(path6.dirname(resolved));
7017
+ const parentReal = await realpath(path7.dirname(resolved));
6950
7018
  if (!isPathInside(workdirReal, parentReal)) {
6951
7019
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV parent resolves outside workdir");
6952
7020
  }
@@ -6962,10 +7030,10 @@ async function fileExists(filePath) {
6962
7030
  }
6963
7031
  }
6964
7032
  async function resolveRequirementsPath(workdir) {
6965
- const workdirReal = await realpath(path6.resolve(workdir));
7033
+ const workdirReal = await realpath(path7.resolve(workdir));
6966
7034
  const candidates = [
6967
- path6.join(workdir, "apps", "api", "requirements.txt"),
6968
- path6.join(workdir, "requirements.txt")
7035
+ path7.join(workdir, "apps", "api", "requirements.txt"),
7036
+ path7.join(workdir, "requirements.txt")
6969
7037
  ];
6970
7038
  for (const candidate of candidates) {
6971
7039
  if (!await fileExists(candidate))
@@ -6979,7 +7047,7 @@ async function resolveRequirementsPath(workdir) {
6979
7047
  return null;
6980
7048
  }
6981
7049
  async function fingerprintRequirementsFile(requirementsPath) {
6982
- const file = await readFile3(requirementsPath);
7050
+ const file = await readFile4(requirementsPath);
6983
7051
  return createHash("sha256").update(file).digest("hex");
6984
7052
  }
6985
7053
  async function runBenchmarkPreflight(workdir) {
@@ -6992,7 +7060,7 @@ async function runBenchmarkPreflight(workdir) {
6992
7060
  const mode = process.env.PARALLAX_BENCHMARK_PREFLIGHT_MODE?.toLowerCase() === "warm" ? "warm" : "cold";
6993
7061
  const venvDir = process.env.PARALLAX_BENCHMARK_PREFLIGHT_VENV || ".benchmark-venv";
6994
7062
  const venvPath = await resolveSafeVenvPath(workdir, venvDir);
6995
- 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");
6996
7064
  const key = `${workdir}::${mode}::${venvPath}::${requirementsFingerprint}`;
6997
7065
  if (PREFLIGHT_DONE.has(key)) {
6998
7066
  if (await fileExists(pythonInVenv))
@@ -7200,21 +7268,21 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
7200
7268
  customCredentials,
7201
7269
  metadata
7202
7270
  } = body;
7203
- const workspaceBaseDir = path6.join(os4.homedir(), ".milady", "workspaces");
7204
- const workspaceBaseDirResolved = path6.resolve(workspaceBaseDir);
7205
- 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());
7206
7274
  const workspaceBaseDirReal = await realpath(workspaceBaseDirResolved).catch(() => workspaceBaseDirResolved);
7207
7275
  const cwdReal = await realpath(cwdResolved).catch(() => cwdResolved);
7208
7276
  const allowedPrefixes = [workspaceBaseDirReal, cwdReal];
7209
7277
  let workdir = rawWorkdir;
7210
7278
  if (workdir) {
7211
- const resolved = path6.resolve(workdir);
7279
+ const resolved = path7.resolve(workdir);
7212
7280
  const resolvedReal = await realpath(resolved).catch(() => null);
7213
7281
  if (!resolvedReal) {
7214
7282
  sendError(res, "workdir must exist", 403);
7215
7283
  return true;
7216
7284
  }
7217
- 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));
7218
7286
  if (!isAllowed) {
7219
7287
  sendError(res, "workdir must be within workspace base directory or cwd", 403);
7220
7288
  return true;
@@ -7960,5 +8028,5 @@ export {
7960
8028
  CodingWorkspaceService
7961
8029
  };
7962
8030
 
7963
- //# debugId=6CDD8C39345EE4DA64756E2164756E21
8031
+ //# debugId=5999C4AE1407A27464756E2164756E21
7964
8032
  //# sourceMappingURL=index.js.map