@elizaos/plugin-agent-orchestrator 0.6.1 → 0.6.2-alpha.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.
Files changed (39) hide show
  1. package/dist/actions/coding-task-handlers.d.ts +1 -0
  2. package/dist/actions/coding-task-handlers.d.ts.map +1 -1
  3. package/dist/actions/finalize-workspace.d.ts.map +1 -1
  4. package/dist/actions/list-agents.d.ts.map +1 -1
  5. package/dist/actions/manage-issues.d.ts.map +1 -1
  6. package/dist/actions/provision-workspace.d.ts.map +1 -1
  7. package/dist/actions/send-to-agent.d.ts.map +1 -1
  8. package/dist/actions/spawn-agent.d.ts.map +1 -1
  9. package/dist/actions/start-coding-task.d.ts.map +1 -1
  10. package/dist/actions/stop-agent.d.ts.map +1 -1
  11. package/dist/api/agent-routes.d.ts.map +1 -1
  12. package/dist/index.js +2076 -480
  13. package/dist/index.js.map +30 -28
  14. package/dist/services/agent-credentials.d.ts +2 -0
  15. package/dist/services/agent-credentials.d.ts.map +1 -1
  16. package/dist/services/agent-metrics.d.ts +3 -1
  17. package/dist/services/agent-metrics.d.ts.map +1 -1
  18. package/dist/services/ansi-utils.d.ts +13 -0
  19. package/dist/services/ansi-utils.d.ts.map +1 -1
  20. package/dist/services/coordinator-event-normalizer.d.ts +47 -0
  21. package/dist/services/coordinator-event-normalizer.d.ts.map +1 -0
  22. package/dist/services/pty-init.d.ts +2 -1
  23. package/dist/services/pty-init.d.ts.map +1 -1
  24. package/dist/services/pty-service.d.ts +19 -6
  25. package/dist/services/pty-service.d.ts.map +1 -1
  26. package/dist/services/pty-spawn.d.ts.map +1 -1
  27. package/dist/services/stall-classifier.d.ts +2 -0
  28. package/dist/services/stall-classifier.d.ts.map +1 -1
  29. package/dist/services/swarm-coordinator.d.ts +5 -0
  30. package/dist/services/swarm-coordinator.d.ts.map +1 -1
  31. package/dist/services/swarm-decision-loop.d.ts.map +1 -1
  32. package/dist/services/task-agent-frameworks.d.ts +31 -1
  33. package/dist/services/task-agent-frameworks.d.ts.map +1 -1
  34. package/dist/services/task-policy.d.ts.map +1 -1
  35. package/dist/services/task-verifier-runner.d.ts +5 -0
  36. package/dist/services/task-verifier-runner.d.ts.map +1 -0
  37. package/dist/services/trajectory-context.d.ts +5 -1
  38. package/dist/services/trajectory-context.d.ts.map +1 -1
  39. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -23,6 +23,9 @@ function applyAnsiStrip(input) {
23
23
  function stripAnsi(raw) {
24
24
  return applyAnsiStrip(raw);
25
25
  }
26
+ function isSessionBootstrapNoiseLine(line) {
27
+ return SESSION_BOOTSTRAP_NOISE_PATTERNS.some((pattern) => pattern.test(line));
28
+ }
26
29
  function cleanForChat(raw) {
27
30
  const stripped = applyAnsiStrip(raw);
28
31
  return stripped.replace(TUI_DECORATIVE, " ").replace(/\xa0/g, " ").split(`
@@ -38,6 +41,8 @@ function cleanForChat(raw) {
38
41
  return false;
39
42
  if (GIT_NOISE_LINE.test(trimmed))
40
43
  return false;
44
+ if (isSessionBootstrapNoiseLine(trimmed))
45
+ return false;
41
46
  if (!/[a-zA-Z0-9]/.test(trimmed))
42
47
  return false;
43
48
  if (trimmed.length <= 3)
@@ -46,6 +51,23 @@ function cleanForChat(raw) {
46
51
  }).map((line) => line.replace(/ {2,}/g, " ").trim()).filter((line) => line.length > 0).join(`
47
52
  `).replace(/\n{3,}/g, `
48
53
 
54
+ `).trim();
55
+ }
56
+ function isWorkdirEchoLine(line, workdir) {
57
+ if (!workdir)
58
+ return false;
59
+ const normalizedWorkdir = workdir.trim();
60
+ if (!normalizedWorkdir)
61
+ return false;
62
+ if (line === normalizedWorkdir || line === `/private${normalizedWorkdir}`) {
63
+ return true;
64
+ }
65
+ const basename = normalizedWorkdir.split("/").filter(Boolean).at(-1);
66
+ return Boolean(basename && line.includes(basename) && (/^\/(?:private\/)?/.test(line) || /^\/…\//.test(line)));
67
+ }
68
+ function cleanForFailoverContext(raw, workdir) {
69
+ return cleanForChat(raw).split(`
70
+ `).map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !FAILOVER_CONTEXT_NOISE_PATTERNS.some((pattern) => pattern.test(line))).filter((line) => !isWorkdirEchoLine(line, workdir)).join(`
49
71
  `).trim();
50
72
  }
51
73
  function extractCompletionSummary(raw) {
@@ -89,7 +111,15 @@ function captureTaskResponse(sessionId, buffers, markers) {
89
111
  return cleanForChat(responseLines.join(`
90
112
  `));
91
113
  }
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;
114
+ function peekTaskResponse(sessionId, buffers, markers) {
115
+ const buffer = buffers.get(sessionId);
116
+ const marker = markers.get(sessionId);
117
+ if (!buffer || marker === undefined)
118
+ return "";
119
+ return cleanForChat(buffer.slice(marker).join(`
120
+ `));
121
+ }
122
+ 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, SESSION_BOOTSTRAP_NOISE_PATTERNS, FAILOVER_CONTEXT_NOISE_PATTERNS;
93
123
  var init_ansi_utils = __esm(() => {
94
124
  CURSOR_MOVEMENT = /\x1b\[\d*[CDABGdEF]/g;
95
125
  CURSOR_POSITION = /\x1b\[\d*(?:;\d+)?[Hf]/g;
@@ -99,11 +129,44 @@ var init_ansi_utils = __esm(() => {
99
129
  CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
100
130
  ORPHAN_SGR = /\[[\d;]*m/g;
101
131
  LONG_SPACES = / {3,}/g;
102
- TUI_DECORATIVE = /[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❮❯▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✽✻✶✳✢⏺←→↑↓⬆⬇◆▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥·⎿✔◼]/g;
132
+ TUI_DECORATIVE = /[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❮❯▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✽✻✶✳✢⏺←→↑↓⬆⬇◆▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥·⎿✔◼█▌▐▖▗▘▝▛▜▟▙◐◑◒◓⏵]/g;
103
133
  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*$/;
104
134
  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
135
  TOOL_MARKER_LINE = /^\s*(?:Bash|Write|Read|Edit|Glob|Grep|Search|TodoWrite|Agent)\s*\(.*\)\s*$/;
106
136
  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;
137
+ SESSION_BOOTSTRAP_NOISE_PATTERNS = [
138
+ /^OpenAI Codex\b/i,
139
+ /^model:\s/i,
140
+ /^directory:\s/i,
141
+ /^Tip:\s+New Try the Codex App\b/i,
142
+ /^until .*Run ['"]codex app['"]/i,
143
+ /Do you trust the contents of this directory/i,
144
+ /higher risk of prompt injection/i,
145
+ /Yes,\s*continue.*No,\s*quit/i,
146
+ /^Press enter to continue$/i,
147
+ /^Quick safety check:/i,
148
+ /^Claude Code can make mistakes\./i,
149
+ /^Claude Code(?:'ll| will)\s+be able to read, edit, and execute files here\.?$/i,
150
+ /^\d+\.\s+Yes,\s*I trust this folder$/i,
151
+ /^\d+\.\s+No,\s*exit$/i,
152
+ /^Enter to confirm(?:\s+Esc to cancel)?$/i,
153
+ /^Welcome back .*Run \/init to create a CLAUDE\.md file with instructions for Claude\./i,
154
+ /^Your bash commands will be sandboxed\. Disable with \/sandbox\./i
155
+ ];
156
+ FAILOVER_CONTEXT_NOISE_PATTERNS = [
157
+ /^Accessing workspace:?$/i,
158
+ /work from your team\)\. If not, take a moment to review what's in this folder first\.$/i,
159
+ /(?:se)?curity guide$/i,
160
+ /^Yes,\s*I trust this folder$/i,
161
+ /^Claude Code v[\d.]+$/i,
162
+ /^Tips for getting started$/i,
163
+ /^Welcome back .*Run \/init to create a CLAUDE\.md file with instructions for Claude\.?$/i,
164
+ /^Recent activity$/i,
165
+ /^No recent activity$/i,
166
+ /^.*\(\d+[MK]? context\)\s+Claude\b.*$/i,
167
+ /^don'?t ask on \(shift\+tab to cycle\)$/i,
168
+ /^\w+\s+\/effort$/i
169
+ ];
107
170
  });
108
171
 
109
172
  // src/services/trajectory-context.ts
@@ -533,7 +596,7 @@ import { execFile } from "node:child_process";
533
596
  import { createHash } from "node:crypto";
534
597
  import { mkdir, readdir, stat, writeFile as writeFile2 } from "node:fs/promises";
535
598
  import { homedir as homedir2 } from "node:os";
536
- import path2 from "node:path";
599
+ import path4 from "node:path";
537
600
  import { promisify } from "node:util";
538
601
  import { ModelType as ModelType3 } from "@elizaos/core";
539
602
  function extractJsonBlock(raw) {
@@ -562,8 +625,8 @@ function parseValidationResponse(raw) {
562
625
  }
563
626
  }
564
627
  function getValidationRootDir() {
565
- const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path2.join(homedir2(), ".milady");
566
- return path2.join(stateDir, "task-validation");
628
+ const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path4.join(homedir2(), ".milady");
629
+ return path4.join(stateDir, "task-validation");
567
630
  }
568
631
  function truncate(text, limit = 1200) {
569
632
  const compact = text.replace(/\s+/g, " ").trim();
@@ -600,9 +663,9 @@ async function captureValidationScreenshot(runtime, task, thread, sessionId) {
600
663
  reason: "Screenshot endpoint returned an empty PNG payload"
601
664
  };
602
665
  }
603
- const dir = path2.join(getValidationRootDir(), task.threadId);
666
+ const dir = path4.join(getValidationRootDir(), task.threadId);
604
667
  await mkdir(dir, { recursive: true });
605
- const screenshotPath = path2.join(dir, `screenshot-${sessionId}-${Date.now()}.png`);
668
+ const screenshotPath = path4.join(dir, `screenshot-${sessionId}-${Date.now()}.png`);
606
669
  await writeFile2(screenshotPath, bytes);
607
670
  const screenshotDescription = await describeScreenshotContent(runtime, task, thread, bytes);
608
671
  return {
@@ -626,7 +689,7 @@ async function captureValidationScreenshot(runtime, task, thread, sessionId) {
626
689
  }
627
690
  }
628
691
  async function listRelevantTrajectories(runtime, task, thread) {
629
- const logger2 = runtime.getService("trajectory_logger");
692
+ const logger2 = runtime.getService("trajectories");
630
693
  if (!logger2?.listTrajectories) {
631
694
  return [];
632
695
  }
@@ -731,7 +794,7 @@ async function collectWorkspaceEvidence(workdir) {
731
794
  evidence.notes.push("no workdir supplied");
732
795
  return evidence;
733
796
  }
734
- const resolved = workdir.startsWith("~") ? path2.join(homedir2(), workdir.slice(1)) : path2.resolve(workdir);
797
+ const resolved = workdir.startsWith("~") ? path4.join(homedir2(), workdir.slice(1)) : path4.resolve(workdir);
735
798
  evidence.workdir = resolved;
736
799
  try {
737
800
  const rootStat = await stat(resolved);
@@ -766,13 +829,13 @@ async function collectWorkspaceEvidence(workdir) {
766
829
  walkCeilingHit = true;
767
830
  break;
768
831
  }
769
- const full = path2.join(dir, entry.name);
832
+ const full = path4.join(dir, entry.name);
770
833
  if (entry.isDirectory()) {
771
834
  await walk(full, depth + 1);
772
835
  } else if (entry.isFile()) {
773
836
  totalCount++;
774
837
  if (collected.length < WORKSPACE_EVIDENCE_FILE_LIMIT) {
775
- collected.push(path2.relative(resolved, full));
838
+ collected.push(path4.relative(resolved, full));
776
839
  }
777
840
  }
778
841
  }
@@ -913,9 +976,9 @@ function buildValidationPrompt(task, thread, completionReasoning, completionSumm
913
976
  `);
914
977
  }
915
978
  async function persistValidationReport(threadId, sessionId, report) {
916
- const dir = path2.join(getValidationRootDir(), threadId);
979
+ const dir = path4.join(getValidationRootDir(), threadId);
917
980
  await mkdir(dir, { recursive: true });
918
- const reportPath = path2.join(dir, `validation-${sessionId}-${Date.now()}.json`);
981
+ const reportPath = path4.join(dir, `validation-${sessionId}-${Date.now()}.json`);
919
982
  await writeFile2(reportPath, JSON.stringify(report, null, 2), "utf8");
920
983
  return reportPath;
921
984
  }
@@ -1047,6 +1110,338 @@ var init_task_validation = __esm(() => {
1047
1110
  ]);
1048
1111
  });
1049
1112
 
1113
+ // src/services/task-verifier-runner.ts
1114
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
1115
+ import { createHash as createHash2 } from "node:crypto";
1116
+ import { homedir as homedir3 } from "node:os";
1117
+ import path5 from "node:path";
1118
+ import { ModelType as ModelType4 } from "@elizaos/core";
1119
+ function extractJsonBlock2(raw) {
1120
+ const trimmed = raw.trim();
1121
+ const fenceMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
1122
+ return (fenceMatch?.[1] ?? trimmed).trim();
1123
+ }
1124
+ function parseAcceptanceEvaluation(raw) {
1125
+ try {
1126
+ const parsed = JSON.parse(extractJsonBlock2(raw));
1127
+ if (parsed.verdict !== "pass" && parsed.verdict !== "fail" || typeof parsed.summary !== "string" || parsed.summary.trim().length === 0) {
1128
+ return null;
1129
+ }
1130
+ const checklist = Array.isArray(parsed.checklist) ? parsed.checklist.filter((entry) => Boolean(entry && typeof entry === "object" && typeof entry.criterion === "string" && typeof entry.status === "string" && typeof entry.evidence === "string")).map((entry) => ({
1131
+ criterion: entry.criterion.trim(),
1132
+ status: entry.status === "pass" || entry.status === "fail" || entry.status === "partial" ? entry.status : "partial",
1133
+ evidence: entry.evidence.trim()
1134
+ })).filter((entry) => entry.criterion.length > 0 && entry.evidence.length > 0) : [];
1135
+ return {
1136
+ verdict: parsed.verdict,
1137
+ summary: parsed.summary.trim(),
1138
+ checklist
1139
+ };
1140
+ } catch {
1141
+ return null;
1142
+ }
1143
+ }
1144
+ function getVerifierRootDir() {
1145
+ const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path5.join(homedir3(), ".milady");
1146
+ return path5.join(stateDir, "task-verifiers");
1147
+ }
1148
+ function truncate2(text, limit = 1200) {
1149
+ const compact = text.replace(/\s+/g, " ").trim();
1150
+ return compact.length <= limit ? compact : `${compact.slice(0, limit)}...`;
1151
+ }
1152
+ function isVerifierReady(thread, job) {
1153
+ if (job.status !== "pending")
1154
+ return false;
1155
+ if (job.verifierType !== "acceptance_criteria")
1156
+ return false;
1157
+ const executionNodes = thread.nodes.filter((node2) => node2.kind !== "goal");
1158
+ if (executionNodes.some((node2) => !terminalNodeStates.has(node2.status))) {
1159
+ return false;
1160
+ }
1161
+ if (!job.nodeId)
1162
+ return true;
1163
+ const node = thread.nodes.find((entry) => entry.id === job.nodeId);
1164
+ return node ? terminalNodeStates.has(node.status) : false;
1165
+ }
1166
+ function collectAcceptancePrerequisiteFailures(thread) {
1167
+ const executionNodes = thread.nodes.filter((node) => node.kind === "execution");
1168
+ const passedCompletionVerifierNodeIds = new Set(thread.verifierJobs.filter((job) => job.verifierType === "task_completion" && job.status === "passed" && typeof job.nodeId === "string").map((job) => job.nodeId));
1169
+ const evidenceBackedNodeIds = new Set(thread.evidence.filter((entry) => typeof entry.nodeId === "string" && (entry.evidenceType === "validation_summary" || entry.evidenceType === "acceptance_report")).map((entry) => entry.nodeId));
1170
+ return {
1171
+ failedExecutionNodes: executionNodes.filter((node) => node.status === "failed" || node.status === "canceled" || node.status === "interrupted"),
1172
+ completedWithoutEvidence: executionNodes.filter((node) => node.status === "completed" && !passedCompletionVerifierNodeIds.has(node.id) && !evidenceBackedNodeIds.has(node.id))
1173
+ };
1174
+ }
1175
+ function buildDeterministicFailureEvaluation(thread, failures) {
1176
+ if (failures.failedExecutionNodes.length > 0) {
1177
+ return {
1178
+ verdict: "fail",
1179
+ summary: `Acceptance cannot pass because execution nodes failed: ${failures.failedExecutionNodes.map((node) => node.title).join(", ")}.`,
1180
+ checklist: thread.acceptanceCriteria.map((criterion) => ({
1181
+ criterion,
1182
+ status: "fail",
1183
+ evidence: `Execution nodes failed before acceptance verification completed: ${failures.failedExecutionNodes.map((node) => `${node.title}=${node.status}`).join(", ")}.`
1184
+ }))
1185
+ };
1186
+ }
1187
+ if (failures.completedWithoutEvidence.length > 0) {
1188
+ return {
1189
+ verdict: "fail",
1190
+ summary: `Acceptance cannot pass because completed execution nodes lack verification evidence: ${failures.completedWithoutEvidence.map((node) => node.title).join(", ")}.`,
1191
+ checklist: thread.acceptanceCriteria.map((criterion) => ({
1192
+ criterion,
1193
+ status: "partial",
1194
+ evidence: `Missing task-completion evidence for: ${failures.completedWithoutEvidence.map((node) => node.title).join(", ")}.`
1195
+ }))
1196
+ };
1197
+ }
1198
+ return null;
1199
+ }
1200
+ function summarizeThreadEvidence(thread) {
1201
+ const sessionSummaries = thread.sessions.map((session) => [
1202
+ `${session.label} [${session.framework}] status=${session.status}`,
1203
+ session.completionSummary ? `summary=${truncate2(session.completionSummary, 400)}` : ""
1204
+ ].filter(Boolean).join(" | ")).filter(Boolean).join(`
1205
+ `);
1206
+ const decisions = thread.decisions.slice(-8).map((decision) => `${decision.decision}: ${truncate2(decision.reasoning, 240)}`).join(`
1207
+ `);
1208
+ const artifacts = thread.artifacts.slice(-12).map((artifact) => `${artifact.artifactType}: ${artifact.title}${artifact.path ? ` (${artifact.path})` : ""}`).join(`
1209
+ `);
1210
+ const evidence = thread.evidence.slice(-16).map((entry) => `${entry.evidenceType}: ${entry.title}${entry.summary ? ` - ${truncate2(entry.summary, 220)}` : ""}`).join(`
1211
+ `);
1212
+ const transcripts = thread.transcripts.slice(-8).map((entry) => `${entry.direction}: ${truncate2(entry.content, 240)}`).join(`
1213
+ `);
1214
+ return [
1215
+ sessionSummaries ? `Sessions
1216
+ ${sessionSummaries}` : "",
1217
+ decisions ? `Decisions
1218
+ ${decisions}` : "",
1219
+ artifacts ? `Artifacts
1220
+ ${artifacts}` : "",
1221
+ evidence ? `Evidence
1222
+ ${evidence}` : "",
1223
+ transcripts ? `Recent transcripts
1224
+ ${transcripts}` : ""
1225
+ ].filter(Boolean).join(`
1226
+
1227
+ `);
1228
+ }
1229
+ async function evaluateAcceptanceCriteria(runtime, thread, job) {
1230
+ const prompt = [
1231
+ "You are a strict task verifier for an agent coordinator.",
1232
+ "Decide whether the thread satisfies its acceptance criteria based only on the provided evidence.",
1233
+ "Return JSON only with keys: verdict, summary, checklist.",
1234
+ 'Set verdict to "pass" only if every criterion is satisfied by concrete evidence.',
1235
+ 'Set verdict to "fail" if any criterion is missing, contradicted, or unsupported.',
1236
+ "Each checklist entry must contain criterion, status (pass|fail|partial), and evidence.",
1237
+ "",
1238
+ `Thread title: ${thread.title}`,
1239
+ `Original request: ${thread.originalRequest}`,
1240
+ `Verifier job: ${job.title}`,
1241
+ `Acceptance criteria:
1242
+ ${thread.acceptanceCriteria.map((criterion, index) => `${index + 1}. ${criterion}`).join(`
1243
+ `)}`,
1244
+ "",
1245
+ `Thread evidence:
1246
+ ${summarizeThreadEvidence(thread) || "No evidence recorded."}`
1247
+ ].join(`
1248
+ `);
1249
+ const raw = await withTrajectoryContext(runtime, {
1250
+ source: "orchestrator",
1251
+ decisionType: "acceptance-verifier",
1252
+ threadId: thread.id,
1253
+ verifierJobId: job.id
1254
+ }, () => runtime.useModel(ModelType4.TEXT_SMALL, {
1255
+ prompt,
1256
+ temperature: 0,
1257
+ stream: false
1258
+ }));
1259
+ const parsed = parseAcceptanceEvaluation(raw);
1260
+ if (parsed) {
1261
+ return parsed;
1262
+ }
1263
+ return {
1264
+ verdict: "fail",
1265
+ summary: "Acceptance verifier returned an invalid response, so the thread could not be proven complete.",
1266
+ checklist: thread.acceptanceCriteria.map((criterion) => ({
1267
+ criterion,
1268
+ status: "partial",
1269
+ evidence: "Verifier response was invalid JSON."
1270
+ }))
1271
+ };
1272
+ }
1273
+ async function writeAcceptanceReport(thread, job, evaluation) {
1274
+ const dir = path5.join(getVerifierRootDir(), thread.id);
1275
+ await mkdir2(dir, { recursive: true });
1276
+ const reportPath = path5.join(dir, `${job.id}.json`);
1277
+ const report = {
1278
+ threadId: thread.id,
1279
+ verifierJobId: job.id,
1280
+ title: job.title,
1281
+ originalRequest: thread.originalRequest,
1282
+ acceptanceCriteria: thread.acceptanceCriteria,
1283
+ evaluation,
1284
+ generatedAt: new Date().toISOString()
1285
+ };
1286
+ const serialized = JSON.stringify(report, null, 2);
1287
+ await writeFile3(reportPath, serialized, "utf8");
1288
+ return {
1289
+ reportPath,
1290
+ sha256: createHash2("sha256").update(serialized).digest("hex")
1291
+ };
1292
+ }
1293
+ async function finalizeAcceptanceJob(taskRegistry, thread, job, evaluation, reportPath, sha256) {
1294
+ await taskRegistry.updateTaskVerifierJob(job.id, {
1295
+ status: evaluation.verdict === "pass" ? "passed" : "failed",
1296
+ completedAt: new Date().toISOString(),
1297
+ metadata: {
1298
+ verdict: evaluation.verdict,
1299
+ summary: evaluation.summary,
1300
+ reportPath,
1301
+ sha256,
1302
+ checklist: evaluation.checklist
1303
+ }
1304
+ });
1305
+ await taskRegistry.recordArtifact({
1306
+ threadId: thread.id,
1307
+ sessionId: thread.latestSessionId ?? null,
1308
+ artifactType: "acceptance_report",
1309
+ title: `${job.title} report`,
1310
+ path: reportPath,
1311
+ mimeType: "application/json",
1312
+ metadata: {
1313
+ verifierJobId: job.id,
1314
+ verdict: evaluation.verdict,
1315
+ sha256
1316
+ }
1317
+ });
1318
+ await taskRegistry.recordTaskEvidence({
1319
+ threadId: thread.id,
1320
+ nodeId: job.nodeId,
1321
+ sessionId: thread.latestSessionId ?? null,
1322
+ verifierJobId: job.id,
1323
+ evidenceType: "acceptance_report",
1324
+ title: job.title,
1325
+ summary: evaluation.summary,
1326
+ path: reportPath,
1327
+ content: {
1328
+ checklist: evaluation.checklist,
1329
+ verdict: evaluation.verdict
1330
+ },
1331
+ metadata: {
1332
+ sha256
1333
+ }
1334
+ });
1335
+ await taskRegistry.appendEvent({
1336
+ threadId: thread.id,
1337
+ sessionId: thread.latestSessionId ?? null,
1338
+ eventType: "verifier_job_completed",
1339
+ summary: `${job.title} ${evaluation.verdict}`,
1340
+ data: {
1341
+ verifierJobId: job.id,
1342
+ verdict: evaluation.verdict,
1343
+ reportPath
1344
+ }
1345
+ });
1346
+ if (job.nodeId) {
1347
+ const node = thread.nodes.find((entry) => entry.id === job.nodeId);
1348
+ const patch = evaluation.verdict === "pass" ? {
1349
+ metadata: {
1350
+ acceptanceVerifierJobId: job.id,
1351
+ acceptanceSummary: evaluation.summary
1352
+ }
1353
+ } : {
1354
+ status: "failed",
1355
+ metadata: {
1356
+ acceptanceVerifierJobId: job.id,
1357
+ acceptanceSummary: evaluation.summary
1358
+ }
1359
+ };
1360
+ if (node) {
1361
+ await taskRegistry.updateTaskNode(node.id, patch);
1362
+ }
1363
+ }
1364
+ }
1365
+ async function runReadyTaskVerifiers(runtime, taskRegistry, threadId) {
1366
+ let thread = await taskRegistry.getThread(threadId);
1367
+ if (!thread) {
1368
+ return;
1369
+ }
1370
+ const readyJobs = thread.verifierJobs.filter((job) => isVerifierReady(thread, job));
1371
+ for (const job of readyJobs) {
1372
+ if (activeVerifierRuns.has(job.id)) {
1373
+ continue;
1374
+ }
1375
+ activeVerifierRuns.add(job.id);
1376
+ try {
1377
+ await taskRegistry.updateTaskVerifierJob(job.id, {
1378
+ status: "running",
1379
+ startedAt: new Date().toISOString(),
1380
+ metadata: {
1381
+ source: "acceptance-runner"
1382
+ }
1383
+ });
1384
+ await taskRegistry.appendEvent({
1385
+ threadId,
1386
+ sessionId: thread.latestSessionId ?? null,
1387
+ eventType: "verifier_job_started",
1388
+ summary: `Running ${job.title}`,
1389
+ data: {
1390
+ verifierJobId: job.id,
1391
+ verifierType: job.verifierType
1392
+ }
1393
+ });
1394
+ thread = await taskRegistry.getThread(threadId) ?? thread;
1395
+ const deterministicFailure = buildDeterministicFailureEvaluation(thread, collectAcceptancePrerequisiteFailures(thread));
1396
+ const evaluation = deterministicFailure ?? await evaluateAcceptanceCriteria(runtime, thread, job);
1397
+ const report = await writeAcceptanceReport(thread, job, evaluation);
1398
+ await finalizeAcceptanceJob(taskRegistry, thread, job, evaluation, report.reportPath, report.sha256);
1399
+ thread = await taskRegistry.getThread(threadId) ?? thread;
1400
+ } catch (error) {
1401
+ const message = error instanceof Error ? error.message : String(error);
1402
+ await taskRegistry.updateTaskVerifierJob(job.id, {
1403
+ status: "failed",
1404
+ completedAt: new Date().toISOString(),
1405
+ metadata: {
1406
+ source: "acceptance-runner",
1407
+ error: message
1408
+ }
1409
+ });
1410
+ await taskRegistry.appendEvent({
1411
+ threadId,
1412
+ sessionId: thread.latestSessionId ?? null,
1413
+ eventType: "verifier_job_failed",
1414
+ summary: `${job.title} failed`,
1415
+ data: {
1416
+ verifierJobId: job.id,
1417
+ error: message
1418
+ }
1419
+ });
1420
+ if (job.nodeId) {
1421
+ await taskRegistry.updateTaskNode(job.nodeId, {
1422
+ status: "failed",
1423
+ metadata: {
1424
+ acceptanceVerifierJobId: job.id,
1425
+ acceptanceSummary: message
1426
+ }
1427
+ });
1428
+ }
1429
+ } finally {
1430
+ activeVerifierRuns.delete(job.id);
1431
+ }
1432
+ }
1433
+ }
1434
+ var terminalNodeStates, activeVerifierRuns;
1435
+ var init_task_verifier_runner = __esm(() => {
1436
+ terminalNodeStates = new Set([
1437
+ "completed",
1438
+ "failed",
1439
+ "canceled",
1440
+ "interrupted"
1441
+ ]);
1442
+ activeVerifierRuns = new Set;
1443
+ });
1444
+
1050
1445
  // src/services/swarm-decision-loop.ts
1051
1446
  var exports_swarm_decision_loop = {};
1052
1447
  __export(exports_swarm_decision_loop, {
@@ -1061,8 +1456,8 @@ __export(exports_swarm_decision_loop, {
1061
1456
  checkAllTasksComplete: () => checkAllTasksComplete,
1062
1457
  POST_SEND_COOLDOWN_MS: () => POST_SEND_COOLDOWN_MS
1063
1458
  });
1064
- import * as path3 from "node:path";
1065
- import { ModelType as ModelType4 } from "@elizaos/core";
1459
+ import * as path6 from "node:path";
1460
+ import { ModelType as ModelType5 } from "@elizaos/core";
1066
1461
  function withTimeout(promise, ms, label) {
1067
1462
  return new Promise((resolve2, reject) => {
1068
1463
  const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
@@ -1195,6 +1590,27 @@ function formatDecisionResponse(decision) {
1195
1590
  return;
1196
1591
  return decision.useKeys ? `keys:${decision.keys?.join(",")}` : decision.response;
1197
1592
  }
1593
+ function truncateForUser(text, max = 140) {
1594
+ const trimmed = text.trim();
1595
+ if (trimmed.length <= max) {
1596
+ return trimmed;
1597
+ }
1598
+ return `${trimmed.slice(0, max)}...`;
1599
+ }
1600
+ function formatSuggestedAction(decision) {
1601
+ if (!decision) {
1602
+ return "Needs human review with no automatic suggestion.";
1603
+ }
1604
+ if (decision.action === "respond") {
1605
+ if (decision.useKeys && decision.keys?.length) {
1606
+ return `Suggested action: send keys ${decision.keys.join(", ")}.`;
1607
+ }
1608
+ if (decision.response?.trim()) {
1609
+ return `Suggested action: reply "${truncateForUser(decision.response, 80)}".`;
1610
+ }
1611
+ }
1612
+ return `Suggested action: ${decision.action}.`;
1613
+ }
1198
1614
  function decisionFromSuggestedResponse(suggestedResponse, reasoning = "Used adapter-provided auto-response for a routine blocking prompt.") {
1199
1615
  if (suggestedResponse.startsWith("keys:")) {
1200
1616
  return {
@@ -1219,8 +1635,8 @@ function inferRoutinePromptResponse(promptText, promptType) {
1219
1635
  }
1220
1636
  if (promptType === "config" && /claude (?:dialog awaiting navigation|menu navigation required)/i.test(promptText)) {
1221
1637
  return {
1222
- suggestedResponse: "keys:esc",
1223
- reasoning: "Dismissed Claude's routine navigation dialog so the replacement session can reach a normal prompt."
1638
+ suggestedResponse: "keys:enter",
1639
+ reasoning: "Accepted Claude's default dialog action so the replacement session can continue without exiting the CLI."
1224
1640
  };
1225
1641
  }
1226
1642
  if (promptType && promptType !== "unknown") {
@@ -1252,10 +1668,10 @@ function isOutOfScopeAccess(promptText, workdir) {
1252
1668
  ];
1253
1669
  if (matches.length === 0)
1254
1670
  return false;
1255
- const resolvedWorkdir = path3.resolve(workdir);
1671
+ const resolvedWorkdir = path6.resolve(workdir);
1256
1672
  return matches.some((p) => {
1257
- const resolved = path3.resolve(p);
1258
- return !resolved.startsWith(resolvedWorkdir + path3.sep) && resolved !== resolvedWorkdir;
1673
+ const resolved = path6.resolve(p);
1674
+ return !resolved.startsWith(resolvedWorkdir + path6.sep) && resolved !== resolvedWorkdir;
1259
1675
  });
1260
1676
  }
1261
1677
  function checkAllTasksComplete(ctx) {
@@ -1273,20 +1689,18 @@ async function checkAllTasksCompleteAsync(ctx) {
1273
1689
  return;
1274
1690
  }
1275
1691
  const threadIds = [...new Set(tasks.map((task) => task.threadId))];
1692
+ const failingThreads = [];
1276
1693
  for (const threadId of threadIds) {
1694
+ await runReadyTaskVerifiers(ctx.runtime, ctx.taskRegistry, threadId);
1277
1695
  const thread = await ctx.taskRegistry.getThread(threadId);
1278
1696
  if (!thread || thread.nodes.length === 0) {
1279
1697
  continue;
1280
1698
  }
1281
- const terminalNodeStates = new Set([
1282
- "completed",
1283
- "failed",
1284
- "canceled",
1285
- "interrupted"
1286
- ]);
1287
1699
  const goalNodes = thread.nodes.filter((node) => node.kind === "goal");
1288
- if (goalNodes.some((node) => !terminalNodeStates.has(node.status))) {
1289
- const pendingGoals = goalNodes.filter((node) => !terminalNodeStates.has(node.status)).map((node) => `${node.title}=${node.status}`).join(", ");
1700
+ const failedGoals = goalNodes.filter((node) => node.status === "failed" || node.status === "canceled" || node.status === "interrupted").map((node) => `${node.title}=${node.status}`);
1701
+ const incompleteGoals = goalNodes.filter((node) => node.status !== "completed" && node.status !== "failed" && node.status !== "canceled" && node.status !== "interrupted").map((node) => `${node.title}=${node.status}`);
1702
+ if (incompleteGoals.length > 0) {
1703
+ const pendingGoals = goalNodes.filter((node) => node.status !== "completed").map((node) => `${node.title}=${node.status}`).join(", ");
1290
1704
  ctx.log(`checkAllTasksComplete: thread ${threadId} still has non-terminal goal nodes — ${pendingGoals}`);
1291
1705
  return;
1292
1706
  }
@@ -1295,6 +1709,43 @@ async function checkAllTasksCompleteAsync(ctx) {
1295
1709
  ctx.log(`checkAllTasksComplete: thread ${threadId} still has running verifier jobs`);
1296
1710
  return;
1297
1711
  }
1712
+ const pendingVerifiers = thread.verifierJobs.filter((job) => job.status === "pending");
1713
+ if (pendingVerifiers.length > 0) {
1714
+ ctx.log(`checkAllTasksComplete: thread ${threadId} still has pending verifier jobs`);
1715
+ return;
1716
+ }
1717
+ const failedVerifiers = thread.verifierJobs.filter((job) => job.status === "failed").map((job) => `${job.title}=failed`);
1718
+ if (failedGoals.length > 0 || failedVerifiers.length > 0) {
1719
+ failingThreads.push({
1720
+ threadId,
1721
+ failedGoals,
1722
+ failedVerifiers
1723
+ });
1724
+ }
1725
+ }
1726
+ if (failingThreads.length > 0) {
1727
+ if (ctx.swarmCompleteNotified) {
1728
+ ctx.log("checkAllTasksComplete: failure notification already sent — skipping");
1729
+ return;
1730
+ }
1731
+ ctx.swarmCompleteNotified = true;
1732
+ const summary = failingThreads.map((thread) => [
1733
+ `thread ${thread.threadId}`,
1734
+ thread.failedGoals.length > 0 ? `failed goals: ${thread.failedGoals.join(", ")}` : "",
1735
+ thread.failedVerifiers.length > 0 ? `failed verifiers: ${thread.failedVerifiers.join(", ")}` : ""
1736
+ ].filter(Boolean).join(" | ")).join("; ");
1737
+ ctx.log(`checkAllTasksComplete: sessions are terminal but acceptance failed — ${summary}`);
1738
+ ctx.broadcast({
1739
+ type: "swarm_attention_required",
1740
+ sessionId: "",
1741
+ timestamp: Date.now(),
1742
+ data: {
1743
+ summary,
1744
+ threads: failingThreads
1745
+ }
1746
+ });
1747
+ ctx.sendChatMessage(`Task agents finished running, but the coordinator could not prove completion. ${summary}`, "task-agent");
1748
+ return;
1298
1749
  }
1299
1750
  if (ctx.swarmCompleteNotified) {
1300
1751
  ctx.log("checkAllTasksComplete: already notified — skipping");
@@ -1384,7 +1835,7 @@ async function makeCoordinationDecision(ctx, taskCtx, promptText, recentOutput)
1384
1835
  repo: taskCtx.repo,
1385
1836
  workdir: taskCtx.workdir,
1386
1837
  originalTask: taskCtx.originalTask
1387
- }, () => ctx.runtime.useModel(ModelType4.TEXT_SMALL, { prompt }));
1838
+ }, () => ctx.runtime.useModel(ModelType5.TEXT_SMALL, { prompt }));
1388
1839
  return parseCoordinationResponse(result);
1389
1840
  } catch (err) {
1390
1841
  ctx.log(`LLM coordination call failed: ${err}`);
@@ -1803,6 +2254,7 @@ async function handleBlocked(ctx, sessionId, taskCtx, data) {
1803
2254
  reason: "max_auto_responses_exceeded"
1804
2255
  }
1805
2256
  });
2257
+ ctx.sendChatMessage(`[${taskCtx.label}] Paused for your attention after ${MAX_AUTO_RESPONSES} consecutive automatic approvals. Prompt: ${truncateForUser(promptText, 180)}`, "coding-agent");
1806
2258
  return;
1807
2259
  }
1808
2260
  switch (ctx.getSupervisionLevel()) {
@@ -1820,6 +2272,7 @@ async function handleBlocked(ctx, sessionId, taskCtx, data) {
1820
2272
  decision: "escalate",
1821
2273
  reasoning: "Supervision level is notify — broadcasting only"
1822
2274
  });
2275
+ ctx.sendChatMessage(`[${taskCtx.label}] Waiting on a blocked prompt: ${truncateForUser(promptText, 180)}`, "coding-agent");
1823
2276
  break;
1824
2277
  }
1825
2278
  }
@@ -1909,7 +2362,7 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
1909
2362
  repo: taskCtx.repo,
1910
2363
  workdir: taskCtx.workdir,
1911
2364
  originalTask: taskCtx.originalTask
1912
- }, () => ctx.runtime.useModel(ModelType4.TEXT_SMALL, { prompt }));
2365
+ }, () => ctx.runtime.useModel(ModelType5.TEXT_SMALL, { prompt }));
1913
2366
  decision = parseCoordinationResponse(result);
1914
2367
  } catch (err) {
1915
2368
  ctx.log(`Turn-complete LLM call failed: ${err}`);
@@ -1950,6 +2403,7 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
1950
2403
  const instruction = decision.response ?? "";
1951
2404
  const preview = instruction.length > 120 ? `${instruction.slice(0, 120)}...` : instruction;
1952
2405
  ctx.log(`[${taskCtx.label}] Turn done, continuing: ${preview}`);
2406
+ ctx.sendChatMessage(`[${taskCtx.label}] Continuing work: ${preview || "sent follow-up instructions."}`, "coding-agent");
1953
2407
  } else if (decision.action === "escalate") {
1954
2408
  ctx.sendChatMessage(`[${taskCtx.label}] Turn finished — needs your attention: ${decision.reasoning}`, "coding-agent");
1955
2409
  }
@@ -2017,6 +2471,7 @@ async function handleAutonomousDecision(ctx, sessionId, taskCtx, promptText, rec
2017
2471
  reason: "invalid_llm_response"
2018
2472
  }
2019
2473
  });
2474
+ ctx.sendChatMessage(`[${taskCtx.label}] Needs your attention: the coordinator could not decide how to handle "${truncateForUser(promptText, 160)}".`, "coding-agent");
2020
2475
  return;
2021
2476
  }
2022
2477
  if (decision.action === "respond" && isOutOfScopeAccess(promptText, taskCtx.workdir)) {
@@ -2170,6 +2625,11 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
2170
2625
  fromPipeline: decisionFromPipeline
2171
2626
  }
2172
2627
  });
2628
+ ctx.sendChatMessage([
2629
+ `[${taskCtx.label}] Waiting for your approval: ${truncateForUser(promptText, 180)}`,
2630
+ formatSuggestedAction(decision),
2631
+ decision?.reasoning ? `Reason: ${truncateForUser(decision.reasoning, 180)}` : ""
2632
+ ].filter(Boolean).join(" "), "coding-agent");
2173
2633
  } finally {
2174
2634
  ctx.inFlightDecisions.delete(sessionId);
2175
2635
  await drainPendingTurnComplete(ctx, sessionId);
@@ -2181,6 +2641,7 @@ var init_swarm_decision_loop = __esm(() => {
2181
2641
  init_ansi_utils();
2182
2642
  init_swarm_event_triage();
2183
2643
  init_task_validation();
2644
+ init_task_verifier_runner();
2184
2645
  deferredTurnCompleteTimers = new Map;
2185
2646
  });
2186
2647
 
@@ -3145,7 +3606,7 @@ var init_query_promise = __esm(() => {
3145
3606
  // node_modules/drizzle-orm/utils.js
3146
3607
  function mapResultRow(columns, row, joinsNotNullableMap) {
3147
3608
  const nullifyMap = {};
3148
- const result = columns.reduce((result2, { path: path6, field }, columnIndex) => {
3609
+ const result = columns.reduce((result2, { path: path9, field }, columnIndex) => {
3149
3610
  let decoder;
3150
3611
  if (is(field, Column)) {
3151
3612
  decoder = field;
@@ -3157,8 +3618,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
3157
3618
  decoder = field.sql.decoder;
3158
3619
  }
3159
3620
  let node = result2;
3160
- for (const [pathChunkIndex, pathChunk] of path6.entries()) {
3161
- if (pathChunkIndex < path6.length - 1) {
3621
+ for (const [pathChunkIndex, pathChunk] of path9.entries()) {
3622
+ if (pathChunkIndex < path9.length - 1) {
3162
3623
  if (!(pathChunk in node)) {
3163
3624
  node[pathChunk] = {};
3164
3625
  }
@@ -3166,8 +3627,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
3166
3627
  } else {
3167
3628
  const rawValue = row[columnIndex];
3168
3629
  const value = node[pathChunk] = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);
3169
- if (joinsNotNullableMap && is(field, Column) && path6.length === 2) {
3170
- const objectName = path6[0];
3630
+ if (joinsNotNullableMap && is(field, Column) && path9.length === 2) {
3631
+ const objectName = path9[0];
3171
3632
  if (!(objectName in nullifyMap)) {
3172
3633
  nullifyMap[objectName] = value === null ? getTableName(field.table) : false;
3173
3634
  } else if (typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== getTableName(field.table)) {
@@ -3971,6 +4432,194 @@ var init_drizzle_orm = __esm(() => {
3971
4432
  init_view_common();
3972
4433
  });
3973
4434
 
4435
+ // src/services/task-policy.ts
4436
+ import fs from "node:fs";
4437
+ import path from "node:path";
4438
+ import { pathToFileURL } from "node:url";
4439
+ var ROLE_RANK = {
4440
+ GUEST: 0,
4441
+ USER: 1,
4442
+ ADMIN: 2,
4443
+ OWNER: 3
4444
+ };
4445
+ var DEFAULT_POLICY = {
4446
+ default: "GUEST",
4447
+ connectors: {
4448
+ discord: {
4449
+ create: "ADMIN",
4450
+ interact: "ADMIN"
4451
+ }
4452
+ }
4453
+ };
4454
+ var LOCAL_ROLES_MODULE_CANDIDATES = [
4455
+ path.resolve(process.cwd(), "packages/plugin-roles/src/index.ts"),
4456
+ path.resolve(process.cwd(), "packages/plugin-roles/dist/index.js")
4457
+ ];
4458
+ function normalizeRole(value) {
4459
+ const upper = typeof value === "string" ? value.trim().toUpperCase() : "";
4460
+ switch (upper) {
4461
+ case "OWNER":
4462
+ case "ADMIN":
4463
+ case "USER":
4464
+ return upper;
4465
+ default:
4466
+ return "GUEST";
4467
+ }
4468
+ }
4469
+ function normalizeConnectorPolicy(value) {
4470
+ if (!value)
4471
+ return {};
4472
+ if (typeof value === "string") {
4473
+ const role = normalizeRole(value);
4474
+ return {
4475
+ create: role,
4476
+ interact: role
4477
+ };
4478
+ }
4479
+ return {
4480
+ ...value.create ? { create: normalizeRole(value.create) } : {},
4481
+ ...value.interact ? { interact: normalizeRole(value.interact) } : {}
4482
+ };
4483
+ }
4484
+ function parseTaskAgentPolicy(runtime) {
4485
+ if (typeof runtime.getSetting !== "function") {
4486
+ return DEFAULT_POLICY;
4487
+ }
4488
+ const configured = runtime.getSetting("TASK_AGENT_ROLE_POLICY") ?? runtime.getSetting("TASK_AGENT_CONNECTOR_ROLE_POLICY");
4489
+ if (!configured) {
4490
+ return DEFAULT_POLICY;
4491
+ }
4492
+ let parsed = configured;
4493
+ if (typeof configured === "string") {
4494
+ try {
4495
+ parsed = JSON.parse(configured);
4496
+ } catch {
4497
+ return DEFAULT_POLICY;
4498
+ }
4499
+ }
4500
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4501
+ return DEFAULT_POLICY;
4502
+ }
4503
+ const record = parsed;
4504
+ const connectors = record.connectors && typeof record.connectors === "object" && !Array.isArray(record.connectors) ? Object.fromEntries(Object.entries(record.connectors).map(([connector, value]) => [
4505
+ connector,
4506
+ normalizeConnectorPolicy(value)
4507
+ ])) : DEFAULT_POLICY.connectors;
4508
+ return {
4509
+ default: normalizeConnectorPolicy(record.default ?? DEFAULT_POLICY.default),
4510
+ connectors
4511
+ };
4512
+ }
4513
+ function getConnectorFromBridgeMetadata(message) {
4514
+ const metadata = message.content?.metadata;
4515
+ if (!metadata || typeof metadata !== "object")
4516
+ return null;
4517
+ const bridgeSender = metadata.bridgeSender;
4518
+ if (!bridgeSender || typeof bridgeSender !== "object")
4519
+ return null;
4520
+ const liveMetadata = bridgeSender.metadata;
4521
+ if (!liveMetadata || typeof liveMetadata !== "object")
4522
+ return null;
4523
+ for (const [connector, value] of Object.entries(liveMetadata)) {
4524
+ if (value && typeof value === "object") {
4525
+ return connector;
4526
+ }
4527
+ }
4528
+ return null;
4529
+ }
4530
+ async function resolveConnectorSource(runtime, message) {
4531
+ const content = message.content;
4532
+ const directSource = typeof content?.source === "string" && content.source !== "client_chat" ? content.source : null;
4533
+ if (directSource)
4534
+ return directSource;
4535
+ const bridgeSource = getConnectorFromBridgeMetadata(message);
4536
+ if (bridgeSource)
4537
+ return bridgeSource;
4538
+ try {
4539
+ const room = await runtime.getRoom(message.roomId);
4540
+ if (typeof room?.source === "string" && room.source.trim().length > 0) {
4541
+ return room.source;
4542
+ }
4543
+ } catch {}
4544
+ return null;
4545
+ }
4546
+ async function resolveSenderRole(runtime, message) {
4547
+ if (process.env.MILADY_SKIP_LOCAL_PLUGIN_ROLES !== "1") {
4548
+ for (const candidate of LOCAL_ROLES_MODULE_CANDIDATES) {
4549
+ if (!fs.existsSync(candidate)) {
4550
+ continue;
4551
+ }
4552
+ try {
4553
+ const localRolesModule = await import(pathToFileURL(candidate).href);
4554
+ if (typeof localRolesModule.checkSenderRole === "function") {
4555
+ return await localRolesModule.checkSenderRole(runtime, message);
4556
+ }
4557
+ } catch {}
4558
+ }
4559
+ }
4560
+ try {
4561
+ const rolesModuleSpecifier = "@miladyai/plugin-roles";
4562
+ const rolesModule = await import(rolesModuleSpecifier);
4563
+ if (typeof rolesModule.checkSenderRole !== "function") {
4564
+ return null;
4565
+ }
4566
+ return await rolesModule.checkSenderRole(runtime, message);
4567
+ } catch {
4568
+ return null;
4569
+ }
4570
+ }
4571
+ async function requireTaskAgentAccess(runtime, message, ability) {
4572
+ const messageEntityId = typeof message.entityId === "string" && message.entityId.length > 0 ? message.entityId : null;
4573
+ const runtimeAgentId = typeof runtime.agentId === "string" && runtime.agentId.length > 0 ? runtime.agentId : null;
4574
+ if (messageEntityId && runtimeAgentId && messageEntityId === runtimeAgentId) {
4575
+ return {
4576
+ allowed: true,
4577
+ connector: null,
4578
+ requiredRole: "GUEST",
4579
+ actualRole: "OWNER"
4580
+ };
4581
+ }
4582
+ const connector = await resolveConnectorSource(runtime, message);
4583
+ const policy = parseTaskAgentPolicy(runtime);
4584
+ const connectorPolicy = connector ? normalizeConnectorPolicy(policy.connectors?.[connector]) : {};
4585
+ const defaultPolicy = normalizeConnectorPolicy(policy.default);
4586
+ const requiredRole = connectorPolicy[ability] ?? defaultPolicy[ability] ?? "GUEST";
4587
+ if (requiredRole === "GUEST") {
4588
+ return {
4589
+ allowed: true,
4590
+ connector,
4591
+ requiredRole,
4592
+ actualRole: "GUEST"
4593
+ };
4594
+ }
4595
+ const roleCheck = await resolveSenderRole(runtime, message);
4596
+ if (!roleCheck) {
4597
+ return {
4598
+ allowed: false,
4599
+ connector,
4600
+ requiredRole,
4601
+ actualRole: "GUEST",
4602
+ reason: connector === "discord" ? "Task-agent access in Discord requires a verified OWNER or ADMIN role." : "Task-agent access requires a verified role, but role context is unavailable."
4603
+ };
4604
+ }
4605
+ const actualRole = normalizeRole(roleCheck.role);
4606
+ if (ROLE_RANK[actualRole] < ROLE_RANK[requiredRole]) {
4607
+ return {
4608
+ allowed: false,
4609
+ connector,
4610
+ requiredRole,
4611
+ actualRole,
4612
+ reason: connector === "discord" ? `Task-agent access in Discord requires ${requiredRole} or higher. Current role: ${actualRole}.` : `Task-agent access requires ${requiredRole} or higher. Current role: ${actualRole}.`
4613
+ };
4614
+ }
4615
+ return {
4616
+ allowed: true,
4617
+ connector,
4618
+ requiredRole,
4619
+ actualRole
4620
+ };
4621
+ }
4622
+
3974
4623
  // src/actions/finalize-workspace.ts
3975
4624
  var finalizeWorkspaceAction = {
3976
4625
  name: "FINALIZE_WORKSPACE",
@@ -4009,6 +4658,15 @@ var finalizeWorkspaceAction = {
4009
4658
  return workspaceService != null;
4010
4659
  },
4011
4660
  handler: async (runtime, message, state, _options, callback) => {
4661
+ const access = await requireTaskAgentAccess(runtime, message, "interact");
4662
+ if (!access.allowed) {
4663
+ if (callback) {
4664
+ await callback({
4665
+ text: access.reason
4666
+ });
4667
+ }
4668
+ return { success: false, error: "FORBIDDEN", text: access.reason };
4669
+ }
4012
4670
  const workspaceService = runtime.getService("CODING_WORKSPACE_SERVICE");
4013
4671
  if (!workspaceService) {
4014
4672
  if (callback) {
@@ -4165,7 +4823,7 @@ Automated changes generated by Milady task agent.
4165
4823
  };
4166
4824
 
4167
4825
  // src/services/pty-service.ts
4168
- import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
4826
+ import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile3, writeFile as writeFile5 } from "node:fs/promises";
4169
4827
  import { dirname as dirname2, join as join4 } from "node:path";
4170
4828
  import { logger as logger4 } from "@elizaos/core";
4171
4829
  import {
@@ -4186,6 +4844,7 @@ class AgentMetricsTracker {
4186
4844
  completed: 0,
4187
4845
  completedViaFastPath: 0,
4188
4846
  completedViaClassifier: 0,
4847
+ completedViaOutputReconcile: 0,
4189
4848
  stallCount: 0,
4190
4849
  avgCompletionMs: 0,
4191
4850
  totalCompletionMs: 0
@@ -4199,8 +4858,10 @@ class AgentMetricsTracker {
4199
4858
  m.completed++;
4200
4859
  if (method === "fast-path")
4201
4860
  m.completedViaFastPath++;
4202
- else
4861
+ else if (method === "classifier")
4203
4862
  m.completedViaClassifier++;
4863
+ else
4864
+ m.completedViaOutputReconcile++;
4204
4865
  m.totalCompletionMs += durationMs;
4205
4866
  m.avgCompletionMs = Math.round(m.totalCompletionMs / m.completed);
4206
4867
  }
@@ -4220,10 +4881,10 @@ class AgentMetricsTracker {
4220
4881
  // src/services/config-env.ts
4221
4882
  import { readFileSync } from "node:fs";
4222
4883
  import * as os from "node:os";
4223
- import * as path from "node:path";
4884
+ import * as path2 from "node:path";
4224
4885
  function readConfig() {
4225
4886
  try {
4226
- const configPath = path.join(process.env.MILADY_STATE_DIR ?? process.env.ELIZA_STATE_DIR ?? path.join(os.homedir(), ".milady"), process.env.ELIZA_NAMESPACE === "milady" || !process.env.ELIZA_NAMESPACE ? "milady.json" : `${process.env.ELIZA_NAMESPACE}.json`);
4887
+ const configPath = path2.join(process.env.MILADY_STATE_DIR ?? process.env.ELIZA_STATE_DIR ?? path2.join(os.homedir(), ".milady"), process.env.ELIZA_NAMESPACE === "milady" || !process.env.ELIZA_NAMESPACE ? "milady.json" : `${process.env.ELIZA_NAMESPACE}.json`);
4227
4888
  const raw = readFileSync(configPath, "utf-8");
4228
4889
  return JSON.parse(raw);
4229
4890
  } catch {
@@ -4402,7 +5063,10 @@ async function handleGeminiAuth(ctx, sessionId, sendKeysToSession) {
4402
5063
 
4403
5064
  // src/services/pty-init.ts
4404
5065
  init_ansi_utils();
5066
+ import { existsSync, readdirSync } from "node:fs";
4405
5067
  import { createRequire as createRequire2 } from "node:module";
5068
+ import os2 from "node:os";
5069
+ import path3 from "node:path";
4406
5070
  import { createAllAdapters } from "coding-agent-adapters";
4407
5071
  import {
4408
5072
  BunCompatiblePTYManager,
@@ -4415,13 +5079,44 @@ var resolvedAdapterModule = "coding-agent-adapters";
4415
5079
  try {
4416
5080
  resolvedAdapterModule = _require.resolve("coding-agent-adapters");
4417
5081
  } catch {}
5082
+ function resolveNodeWorkerPath() {
5083
+ const explicitCandidates = [
5084
+ process.env.NODE,
5085
+ process.env.NODE_BINARY,
5086
+ "/opt/homebrew/bin/node",
5087
+ "/usr/local/bin/node"
5088
+ ].filter((value) => Boolean(value?.trim()));
5089
+ for (const candidate of explicitCandidates) {
5090
+ if (existsSync(candidate)) {
5091
+ return candidate;
5092
+ }
5093
+ }
5094
+ const nvmVersionsDir = path3.join(os2.homedir(), ".nvm", "versions", "node");
5095
+ if (existsSync(nvmVersionsDir)) {
5096
+ const versions = readdirSync(nvmVersionsDir).filter((entry) => entry.startsWith("v")).sort((a, b) => b.localeCompare(a, undefined, {
5097
+ numeric: true,
5098
+ sensitivity: "base"
5099
+ }));
5100
+ for (const version of versions) {
5101
+ const candidate = path3.join(nvmVersionsDir, version, "bin", "node");
5102
+ if (existsSync(candidate)) {
5103
+ return candidate;
5104
+ }
5105
+ }
5106
+ }
5107
+ return "node";
5108
+ }
4418
5109
  function forwardReadyAsTaskComplete(ctx, session) {
4419
5110
  if (!ctx.hasActiveTask?.(session.id) || !ctx.hasTaskActivity?.(session.id)) {
4420
5111
  return;
4421
5112
  }
4422
5113
  const response = ctx.taskResponseMarkers.has(session.id) ? captureTaskResponse(session.id, ctx.sessionOutputBuffers, ctx.taskResponseMarkers) : "";
4423
5114
  ctx.log(`session_ready for active task ${session.id} — forwarding as task_complete (stall classifier path, response: ${response.length} chars)`);
4424
- ctx.emitEvent(session.id, "task_complete", { session, response });
5115
+ ctx.emitEvent(session.id, "task_complete", {
5116
+ session,
5117
+ response,
5118
+ source: "session_ready_forward"
5119
+ });
4425
5120
  }
4426
5121
  async function initializePTYManager(ctx) {
4427
5122
  const usingBunWorker = isBun();
@@ -4430,6 +5125,7 @@ async function initializePTYManager(ctx) {
4430
5125
  ctx.log(`Resolved adapter module: ${resolvedAdapterModule}`);
4431
5126
  const bunManager = new BunCompatiblePTYManager({
4432
5127
  adapterModules: [resolvedAdapterModule],
5128
+ nodePath: resolveNodeWorkerPath(),
4433
5129
  stallDetectionEnabled: true,
4434
5130
  stallTimeoutMs: 4000,
4435
5131
  onStallClassify: async (sessionId, recentOutput, _stallDurationMs) => {
@@ -4438,40 +5134,62 @@ async function initializePTYManager(ctx) {
4438
5134
  });
4439
5135
  bunManager.on("session_ready", (session) => {
4440
5136
  ctx.log(`session_ready event received for ${session.id} (type: ${session.type}, status: ${session.status})`);
4441
- ctx.emitEvent(session.id, "ready", { session });
5137
+ ctx.emitEvent(session.id, "ready", { session, source: "pty_manager" });
4442
5138
  forwardReadyAsTaskComplete(ctx, session);
4443
5139
  ctx.markTaskDelivered?.(session.id);
4444
5140
  });
4445
5141
  bunManager.on("session_exit", (id, code) => {
4446
- ctx.emitEvent(id, "stopped", { reason: `exit code ${code}` });
5142
+ ctx.emitEvent(id, "stopped", {
5143
+ reason: `exit code ${code}`,
5144
+ source: "pty_manager"
5145
+ });
4447
5146
  });
4448
5147
  bunManager.on("session_error", (id, error) => {
4449
- ctx.emitEvent(id, "error", { message: error });
5148
+ ctx.emitEvent(id, "error", { message: error, source: "pty_manager" });
4450
5149
  });
4451
5150
  bunManager.on("blocking_prompt", (session, promptInfo, autoResponded) => {
4452
5151
  const info = promptInfo;
4453
5152
  ctx.log(`blocking_prompt for ${session.id}: type=${info?.type}, autoResponded=${autoResponded}, prompt="${(info?.prompt ?? "").slice(0, 80)}"`);
4454
- ctx.emitEvent(session.id, "blocked", { promptInfo, autoResponded });
5153
+ ctx.emitEvent(session.id, "blocked", {
5154
+ promptInfo,
5155
+ autoResponded,
5156
+ source: "pty_manager"
5157
+ });
4455
5158
  });
4456
5159
  bunManager.on("login_required", (session, instructions, url) => {
4457
5160
  if (session.type === "gemini") {
4458
5161
  ctx.handleGeminiAuth(session.id);
4459
5162
  }
4460
- ctx.emitEvent(session.id, "login_required", { instructions, url });
5163
+ ctx.emitEvent(session.id, "login_required", {
5164
+ instructions,
5165
+ url,
5166
+ source: "pty_manager"
5167
+ });
4461
5168
  });
4462
5169
  bunManager.on("task_complete", (session) => {
4463
5170
  const response = captureTaskResponse(session.id, ctx.sessionOutputBuffers, ctx.taskResponseMarkers);
4464
5171
  const durationMs = session.startedAt ? Date.now() - new Date(session.startedAt).getTime() : 0;
4465
5172
  ctx.metricsTracker.recordCompletion(session.type, "fast-path", durationMs);
4466
5173
  ctx.log(`Task complete for ${session.id} (adapter fast-path), response: ${response.length} chars`);
4467
- ctx.emitEvent(session.id, "task_complete", { session, response });
5174
+ ctx.emitEvent(session.id, "task_complete", {
5175
+ session,
5176
+ response,
5177
+ source: "adapter_fast_path"
5178
+ });
4468
5179
  });
4469
5180
  bunManager.on("tool_running", (session, info) => {
4470
5181
  ctx.log(`tool_running for ${session.id}: ${info.toolName}${info.description ? ` — ${info.description}` : ""}`);
4471
- ctx.emitEvent(session.id, "tool_running", { session, ...info });
5182
+ ctx.emitEvent(session.id, "tool_running", {
5183
+ session,
5184
+ ...info,
5185
+ source: "pty_manager"
5186
+ });
4472
5187
  });
4473
5188
  bunManager.on("message", (message) => {
4474
- ctx.emitEvent(message.sessionId, "message", message);
5189
+ ctx.emitEvent(message.sessionId, "message", {
5190
+ ...message,
5191
+ source: "pty_manager"
5192
+ });
4475
5193
  });
4476
5194
  bunManager.on("worker_error", (err) => {
4477
5195
  const raw = typeof err === "string" ? err : String(err);
@@ -4519,38 +5237,60 @@ async function initializePTYManager(ctx) {
4519
5237
  }
4520
5238
  }
4521
5239
  nodeManager.on("session_ready", (session) => {
4522
- ctx.emitEvent(session.id, "ready", { session });
5240
+ ctx.emitEvent(session.id, "ready", { session, source: "pty_manager" });
4523
5241
  forwardReadyAsTaskComplete(ctx, session);
4524
5242
  ctx.markTaskDelivered?.(session.id);
4525
5243
  });
4526
5244
  nodeManager.on("blocking_prompt", (session, promptInfo, autoResponded) => {
4527
- ctx.emitEvent(session.id, "blocked", { promptInfo, autoResponded });
5245
+ ctx.emitEvent(session.id, "blocked", {
5246
+ promptInfo,
5247
+ autoResponded,
5248
+ source: "pty_manager"
5249
+ });
4528
5250
  });
4529
5251
  nodeManager.on("login_required", (session, instructions, url) => {
4530
5252
  if (session.type === "gemini") {
4531
5253
  ctx.handleGeminiAuth(session.id);
4532
5254
  }
4533
- ctx.emitEvent(session.id, "login_required", { instructions, url });
5255
+ ctx.emitEvent(session.id, "login_required", {
5256
+ instructions,
5257
+ url,
5258
+ source: "pty_manager"
5259
+ });
4534
5260
  });
4535
5261
  nodeManager.on("task_complete", (session) => {
4536
5262
  const response = captureTaskResponse(session.id, ctx.sessionOutputBuffers, ctx.taskResponseMarkers);
4537
5263
  const durationMs = session.startedAt ? Date.now() - new Date(session.startedAt).getTime() : 0;
4538
5264
  ctx.metricsTracker.recordCompletion(session.type, "fast-path", durationMs);
4539
5265
  ctx.log(`Task complete for ${session.id} (adapter fast-path), response: ${response.length} chars`);
4540
- ctx.emitEvent(session.id, "task_complete", { session, response });
5266
+ ctx.emitEvent(session.id, "task_complete", {
5267
+ session,
5268
+ response,
5269
+ source: "adapter_fast_path"
5270
+ });
4541
5271
  });
4542
5272
  nodeManager.on("tool_running", (session, info) => {
4543
5273
  ctx.log(`tool_running for ${session.id}: ${info.toolName}${info.description ? ` — ${info.description}` : ""}`);
4544
- ctx.emitEvent(session.id, "tool_running", { session, ...info });
5274
+ ctx.emitEvent(session.id, "tool_running", {
5275
+ session,
5276
+ ...info,
5277
+ source: "pty_manager"
5278
+ });
4545
5279
  });
4546
5280
  nodeManager.on("session_stopped", (session, reason) => {
4547
- ctx.emitEvent(session.id, "stopped", { reason });
5281
+ ctx.emitEvent(session.id, "stopped", { reason, source: "pty_manager" });
4548
5282
  });
4549
5283
  nodeManager.on("session_error", (session, error) => {
4550
- ctx.emitEvent(session.id, "error", { message: error });
5284
+ ctx.emitEvent(session.id, "error", {
5285
+ message: error,
5286
+ source: "pty_manager"
5287
+ });
4551
5288
  });
4552
5289
  nodeManager.on("message", (message) => {
4553
- ctx.emitEvent(message.sessionId, "message", message);
5290
+ ctx.emitEvent(message.sessionId, "message", {
5291
+ ...message,
5292
+ source: "pty_manager"
5293
+ });
4554
5294
  });
4555
5295
  return { manager: nodeManager, usingBunWorker: false };
4556
5296
  }
@@ -4681,7 +5421,11 @@ async function getSessionOutput(ctx, sessionId, lines) {
4681
5421
  `);
4682
5422
  }
4683
5423
 
5424
+ // src/services/pty-service.ts
5425
+ init_ansi_utils();
5426
+
4684
5427
  // src/services/pty-spawn.ts
5428
+ init_ansi_utils();
4685
5429
  var ENV_ALLOWLIST = [
4686
5430
  "PATH",
4687
5431
  "HOME",
@@ -4696,7 +5440,9 @@ var ENV_ALLOWLIST = [
4696
5440
  "TMPDIR",
4697
5441
  "XDG_RUNTIME_DIR",
4698
5442
  "NODE_OPTIONS",
4699
- "BUN_INSTALL"
5443
+ "BUN_INSTALL",
5444
+ "ANTHROPIC_MODEL",
5445
+ "ANTHROPIC_SMALL_FAST_MODEL"
4700
5446
  ];
4701
5447
  function buildSanitizedBaseEnv() {
4702
5448
  const env = {};
@@ -4758,6 +5504,9 @@ function setupDeferredTaskDelivery(ctx, session, task, agentType) {
4758
5504
  const VERIFY_DELAY_MS = 5000;
4759
5505
  const MAX_RETRIES = 2;
4760
5506
  const minNewLines = MIN_NEW_LINES_BY_AGENT[agentType] ?? 15;
5507
+ const READY_PROBE_INTERVAL_MS = 500;
5508
+ const isAdapterBackedAgent = agentType === "claude" || agentType === "gemini" || agentType === "codex" || agentType === "aider";
5509
+ const adapter = isAdapterBackedAgent ? ctx.getAdapter(agentType) : null;
4761
5510
  const sendTaskWithRetry = (attempt) => {
4762
5511
  const buffer = ctx.sessionOutputBuffers.get(sid);
4763
5512
  const baselineLength = buffer?.length ?? 0;
@@ -4767,7 +5516,10 @@ function setupDeferredTaskDelivery(ctx, session, task, agentType) {
4767
5516
  setTimeout(() => {
4768
5517
  const currentLength = buffer?.length ?? 0;
4769
5518
  const newLines = currentLength - baselineLength;
4770
- if (newLines < minNewLines) {
5519
+ const newOutput = buffer?.slice(baselineLength).join(`
5520
+ `) ?? "";
5521
+ const accepted = newLines > 0 || newLines >= minNewLines || (adapter?.detectLoading?.(newOutput) ?? false) || cleanForChat(newOutput).length >= 32;
5522
+ if (!accepted) {
4771
5523
  ctx.log(`Session ${sid} — task may not have been accepted (only ${newLines} new lines after ${VERIFY_DELAY_MS}ms). Retrying (attempt ${attempt + 2}/${MAX_RETRIES + 1})`);
4772
5524
  sendTaskWithRetry(attempt + 1);
4773
5525
  } else {
@@ -4778,15 +5530,31 @@ function setupDeferredTaskDelivery(ctx, session, task, agentType) {
4778
5530
  };
4779
5531
  const READY_TIMEOUT_MS = 30000;
4780
5532
  let taskSent = false;
5533
+ let taskDeliveredMarked = false;
4781
5534
  let readyTimeout;
5535
+ let readyProbe;
5536
+ const clearPendingReadyWait = () => {
5537
+ if (readyTimeout) {
5538
+ clearTimeout(readyTimeout);
5539
+ readyTimeout = undefined;
5540
+ }
5541
+ if (readyProbe) {
5542
+ clearInterval(readyProbe);
5543
+ readyProbe = undefined;
5544
+ }
5545
+ };
4782
5546
  const sendTask = () => {
4783
5547
  if (taskSent)
4784
5548
  return;
4785
5549
  taskSent = true;
4786
- if (readyTimeout)
4787
- clearTimeout(readyTimeout);
4788
- ctx.markTaskDelivered(sid);
4789
- setTimeout(() => sendTaskWithRetry(0), settleMs);
5550
+ clearPendingReadyWait();
5551
+ setTimeout(() => {
5552
+ if (!taskDeliveredMarked) {
5553
+ ctx.markTaskDelivered(sid);
5554
+ taskDeliveredMarked = true;
5555
+ }
5556
+ sendTaskWithRetry(0);
5557
+ }, settleMs);
4790
5558
  if (ctx.usingBunWorker) {
4791
5559
  ctx.manager.removeListener("session_ready", onReady);
4792
5560
  } else {
@@ -4812,6 +5580,29 @@ function setupDeferredTaskDelivery(ctx, session, task, agentType) {
4812
5580
  sendTask();
4813
5581
  }
4814
5582
  }, READY_TIMEOUT_MS);
5583
+ if (ctx.usingBunWorker && isAdapterBackedAgent && adapter) {
5584
+ readyProbe = setInterval(() => {
5585
+ if (taskSent)
5586
+ return;
5587
+ const buffer = ctx.sessionOutputBuffers.get(sid);
5588
+ if (!buffer || buffer.length === 0)
5589
+ return;
5590
+ const output = buffer.join(`
5591
+ `);
5592
+ const cleanedOutput = cleanForChat(output);
5593
+ if (adapter.detectLoading?.(output))
5594
+ return;
5595
+ if (adapter.detectLogin(output).required)
5596
+ return;
5597
+ if (adapter.detectBlockingPrompt(output).detected)
5598
+ return;
5599
+ const promptVisible = adapter.detectReady(output) || agentType === "codex" && /›\s+(?:Ask Codex to do anything|\S.*)/.test(cleanedOutput);
5600
+ if (!promptVisible)
5601
+ return;
5602
+ ctx.log(`Session ${sid} — detected ready prompt from buffered output, delivering task before timeout`);
5603
+ sendTask();
5604
+ }, READY_PROBE_INTERVAL_MS);
5605
+ }
4815
5606
  }
4816
5607
  }
4817
5608
  function buildSpawnConfig(sessionId, options, workdir) {
@@ -4900,6 +5691,71 @@ import {
4900
5691
  buildTaskCompletionTimeline,
4901
5692
  extractTaskCompletionTraceRecords
4902
5693
  } from "pty-manager";
5694
+ var STATUS_NOISE_LINE = /messages to be submitted after next tool call|working \(\d+s .*esc to interrupt\)|\b\d+% left\b|context left|use \/skills to list available skills/i;
5695
+ var STATUS_PATH_LINE = /(\/private\/|\/var\/folders\/|\/Users\/|\/tmp\/)/;
5696
+ var SPINNER_FRAGMENT_TOKEN = /^(?:w|wo|wor|work|worki|workin|working|orking|rking|king|ing|ng|g|\d+|[•·])$/i;
5697
+ function normalizeForComparison(value) {
5698
+ return stripAnsi(value).replace(/\s+/g, " ").trim().toLowerCase();
5699
+ }
5700
+ function looksLikeSpinnerFragments(line) {
5701
+ const tokens = line.replace(/[^\w/%@.:\-/ ]+/g, " ").split(/\s+/).filter(Boolean);
5702
+ if (tokens.length === 0)
5703
+ return false;
5704
+ const fragmentTokens = tokens.filter((token) => SPINNER_FRAGMENT_TOKEN.test(token));
5705
+ return fragmentTokens.length >= 4 && fragmentTokens.length >= Math.ceil(tokens.length * 0.6);
5706
+ }
5707
+ function isStatusNoiseLine(line) {
5708
+ const compact = line.replace(/\s+/g, " ").trim();
5709
+ if (!compact)
5710
+ return true;
5711
+ if (compact.startsWith("› "))
5712
+ return true;
5713
+ if (STATUS_NOISE_LINE.test(compact))
5714
+ return true;
5715
+ if (looksLikeSpinnerFragments(compact))
5716
+ return true;
5717
+ if (STATUS_PATH_LINE.test(compact) && /\b\d+% left\b/i.test(compact))
5718
+ return true;
5719
+ if (STATUS_PATH_LINE.test(compact) && looksLikeSpinnerFragments(compact))
5720
+ return true;
5721
+ return false;
5722
+ }
5723
+ function sanitizeOutputForClassification(output, lastSentInput) {
5724
+ const normalizedInput = lastSentInput ? normalizeForComparison(lastSentInput) : "";
5725
+ let removedEchoLines = 0;
5726
+ let removedStatusLines = 0;
5727
+ const sanitized = stripAnsi(output).split(`
5728
+ `).map((line) => line.replace(/\s+/g, " ").trim()).filter((line) => {
5729
+ if (!line)
5730
+ return false;
5731
+ const normalizedLine = line.toLowerCase();
5732
+ if (normalizedInput && normalizedLine.length >= 12 && normalizedInput.includes(normalizedLine)) {
5733
+ removedEchoLines += 1;
5734
+ return false;
5735
+ }
5736
+ if (isStatusNoiseLine(line)) {
5737
+ removedStatusLines += 1;
5738
+ return false;
5739
+ }
5740
+ return true;
5741
+ }).join(`
5742
+ `).trim();
5743
+ return { sanitized, removedEchoLines, removedStatusLines };
5744
+ }
5745
+ function promptLooksLikeFalseBlockedNoise(prompt, lastSentInput) {
5746
+ if (!prompt)
5747
+ return false;
5748
+ const normalizedPrompt = normalizeForComparison(prompt);
5749
+ if (!normalizedPrompt)
5750
+ return false;
5751
+ if (lastSentInput) {
5752
+ const normalizedInput = normalizeForComparison(lastSentInput);
5753
+ if (normalizedPrompt.length >= 12 && normalizedInput.includes(normalizedPrompt)) {
5754
+ return true;
5755
+ }
5756
+ }
5757
+ return isStatusNoiseLine(prompt) || looksLikeSpinnerFragments(prompt);
5758
+ }
4903
5759
  function buildStallClassificationPrompt(agentType, sessionId, output) {
4904
5760
  return `You are Milady, an AI orchestrator managing task-agent sessions. ` + `A ${agentType} task agent (session: ${sessionId}) appears to have stalled — ` + `it has stopped producing output while in a busy state.
4905
5761
 
@@ -4920,7 +5776,7 @@ ${output.slice(-1500)}
4920
5776
 
4921
5777
  ` + `5. "tool_running" — The agent is using an external tool (browser automation, ` + `MCP tool, etc.). Indicators: "Claude in Chrome", "javascript_tool", ` + `"computer_tool", "screenshot", "navigate", tool execution output. ` + `The agent is actively working but the terminal may be quiet.
4922
5778
 
4923
- ` + `IMPORTANT: If you see BOTH completed work output AND an idle prompt (❯), choose "task_complete". ` + `Only choose "waiting_for_input" if the agent is clearly asking a question mid-task.
5779
+ ` + `IMPORTANT: If you see BOTH completed work output AND an idle prompt (❯), choose "task_complete". ` + `Only choose "waiting_for_input" if the agent is clearly asking a question mid-task. ` + `Ignore echoed user input, copied prior transcripts, spinner fragments, and status rows like ` + `"Working (12s • esc to interrupt)" or "97% left" — those mean the agent is still working, not blocked.
4924
5780
 
4925
5781
  ` + `If "waiting_for_input", also provide:
4926
5782
  ` + `- "prompt": the text of what it's asking
@@ -4931,11 +5787,11 @@ ${output.slice(-1500)}
4931
5787
  }
4932
5788
  async function writeStallSnapshot(sessionId, agentType, recentOutput, effectiveOutput, buffers, traceEntries, log) {
4933
5789
  try {
4934
- const fs = await import("node:fs");
4935
- const os2 = await import("node:os");
4936
- const path2 = await import("node:path");
4937
- const snapshotDir = path2.join(os2.homedir(), ".milady", "debug");
4938
- fs.mkdirSync(snapshotDir, { recursive: true });
5790
+ const fs2 = await import("node:fs");
5791
+ const os3 = await import("node:os");
5792
+ const path4 = await import("node:path");
5793
+ const snapshotDir = path4.join(os3.homedir(), ".milady", "debug");
5794
+ fs2.mkdirSync(snapshotDir, { recursive: true });
4939
5795
  const ourBuffer = buffers.get(sessionId);
4940
5796
  const ourTail = ourBuffer ? ourBuffer.slice(-100).join(`
4941
5797
  `) : "(no buffer)";
@@ -4966,8 +5822,8 @@ async function writeStallSnapshot(sessionId, agentType, recentOutput, effectiveO
4966
5822
  ``
4967
5823
  ].join(`
4968
5824
  `);
4969
- const snapshotPath = path2.join(snapshotDir, `stall-snapshot-${sessionId}.txt`);
4970
- fs.writeFileSync(snapshotPath, snapshot);
5825
+ const snapshotPath = path4.join(snapshotDir, `stall-snapshot-${sessionId}.txt`);
5826
+ fs2.writeFileSync(snapshotPath, snapshot);
4971
5827
  log(`Stall snapshot → ${snapshotPath}`);
4972
5828
  } catch (_) {}
4973
5829
  }
@@ -4997,7 +5853,19 @@ async function classifyStallOutput(ctx) {
4997
5853
  }
4998
5854
  }
4999
5855
  }
5000
- const systemPrompt = buildStallClassificationPrompt(agentType, sessionId, effectiveOutput);
5856
+ const {
5857
+ sanitized: sanitizedOutput,
5858
+ removedEchoLines,
5859
+ removedStatusLines
5860
+ } = sanitizeOutputForClassification(effectiveOutput, ctx.lastSentInput);
5861
+ if (removedEchoLines > 0 || removedStatusLines > 0) {
5862
+ log(`Sanitized stall output for ${sessionId}: removed ${removedEchoLines} echoed lines and ${removedStatusLines} status lines`);
5863
+ }
5864
+ if (!sanitizedOutput && removedEchoLines + removedStatusLines > 0) {
5865
+ log(`Stall classification short-circuit for ${sessionId}: only echoed input / status noise remained`);
5866
+ return { state: "still_working" };
5867
+ }
5868
+ const systemPrompt = buildStallClassificationPrompt(agentType, sessionId, sanitizedOutput || effectiveOutput);
5001
5869
  if (ctx.debugSnapshots) {
5002
5870
  await writeStallSnapshot(sessionId, agentType, recentOutput, effectiveOutput, buffers, traceEntries, log);
5003
5871
  }
@@ -5031,6 +5899,10 @@ async function classifyStallOutput(ctx) {
5031
5899
  prompt: parsed.prompt,
5032
5900
  suggestedResponse: parsed.suggestedResponse
5033
5901
  };
5902
+ if (classification.state === "waiting_for_input" && promptLooksLikeFalseBlockedNoise(classification.prompt, ctx.lastSentInput)) {
5903
+ log(`Stall classification override for ${sessionId}: prompt looked like echoed input / status noise`);
5904
+ return { state: "still_working" };
5905
+ }
5034
5906
  log(`Stall classification for ${sessionId}: ${classification.state}${classification.suggestedResponse ? ` → "${classification.suggestedResponse}"` : ""}`);
5035
5907
  if (classification.state === "task_complete") {
5036
5908
  const session = manager?.get(sessionId);
@@ -5074,6 +5946,8 @@ Classification states:
5074
5946
 
5075
5947
  ` + `5. "tool_running" — The agent is using an external tool (browser automation, MCP tool, etc.).
5076
5948
 
5949
+ ` + `Ignore echoed user input, copied prior transcripts, spinner fragments, and status rows like ` + `"Working (12s • esc to interrupt)" or "97% left" — those indicate active work, not a live prompt.
5950
+
5077
5951
  ` + `If "waiting_for_input", you must also decide how to respond. Guidelines:
5078
5952
  - IMPORTANT: If the prompt asks to approve access to files or directories OUTSIDE the working directory (${taskContext.workdir}), DECLINE the request. Respond with "n" and tell the agent: "That path is outside your workspace. Use ${taskContext.workdir} instead."
5079
5953
  - For tool approval prompts (file writes, shell commands), respond "y" or use "keys:enter".
@@ -5113,7 +5987,19 @@ async function classifyAndDecideForCoordinator(ctx) {
5113
5987
  }
5114
5988
  }
5115
5989
  }
5116
- const systemPrompt = buildCombinedClassifyDecidePrompt(agentType, sessionId, effectiveOutput, taskContext, decisionHistory);
5990
+ const {
5991
+ sanitized: sanitizedOutput,
5992
+ removedEchoLines,
5993
+ removedStatusLines
5994
+ } = sanitizeOutputForClassification(effectiveOutput, ctx.lastSentInput);
5995
+ if (removedEchoLines > 0 || removedStatusLines > 0) {
5996
+ log(`Sanitized combined stall output for ${sessionId}: removed ${removedEchoLines} echoed lines and ${removedStatusLines} status lines`);
5997
+ }
5998
+ if (!sanitizedOutput && removedEchoLines + removedStatusLines > 0) {
5999
+ log(`Combined classify+decide short-circuit for ${sessionId}: only echoed input / status noise remained`);
6000
+ return { state: "still_working" };
6001
+ }
6002
+ const systemPrompt = buildCombinedClassifyDecidePrompt(agentType, sessionId, sanitizedOutput || effectiveOutput, taskContext, decisionHistory);
5117
6003
  if (ctx.debugSnapshots) {
5118
6004
  await writeStallSnapshot(sessionId, agentType, recentOutput, effectiveOutput, buffers, traceEntries, log);
5119
6005
  }
@@ -5161,6 +6047,10 @@ async function classifyAndDecideForCoordinator(ctx) {
5161
6047
  prompt: parsed.prompt,
5162
6048
  suggestedResponse: parsed.suggestedResponse
5163
6049
  };
6050
+ if (classification.state === "waiting_for_input" && promptLooksLikeFalseBlockedNoise(classification.prompt, ctx.lastSentInput)) {
6051
+ log(`Combined classify+decide override for ${sessionId}: prompt looked like echoed input / status noise`);
6052
+ return { state: "still_working" };
6053
+ }
5164
6054
  log(`Combined classify+decide for ${sessionId}: ${classification.state}${classification.suggestedResponse ? ` → "${classification.suggestedResponse}"` : ""}`);
5165
6055
  if (classification.state === "task_complete") {
5166
6056
  const session = manager?.get(sessionId);
@@ -5191,6 +6081,20 @@ function buildCodexCloudProviderToml(baseUrl) {
5191
6081
  ` + `supports_websockets = false
5192
6082
  `;
5193
6083
  }
6084
+ function compactCredentials(credentials) {
6085
+ return Object.fromEntries(Object.entries(credentials).filter(([, value]) => value !== undefined));
6086
+ }
6087
+ function isAnthropicOAuthToken(value) {
6088
+ return typeof value === "string" && value.startsWith("sk-ant-oat");
6089
+ }
6090
+ function sanitizeCustomCredentials(customCredentials, blockedValues = []) {
6091
+ if (!customCredentials) {
6092
+ return;
6093
+ }
6094
+ const blocked = new Set(blockedValues.filter(Boolean));
6095
+ const filtered = Object.entries(customCredentials).filter(([, value]) => !blocked.has(value));
6096
+ return filtered.length > 0 ? Object.fromEntries(filtered) : undefined;
6097
+ }
5194
6098
  function buildAgentCredentials(runtime) {
5195
6099
  const llmProvider = readConfigEnvKey("PARALLAX_LLM_PROVIDER") || "subscription";
5196
6100
  if (llmProvider === "cloud") {
@@ -5198,7 +6102,7 @@ function buildAgentCredentials(runtime) {
5198
6102
  if (!cloudKey) {
5199
6103
  throw new Error("Eliza Cloud is selected as the LLM provider but no cloud.apiKey is paired. Pair your account in the Cloud settings section first.");
5200
6104
  }
5201
- const cloudCredentials = {
6105
+ const cloudCredentials = compactCredentials({
5202
6106
  anthropicKey: cloudKey,
5203
6107
  openaiKey: cloudKey,
5204
6108
  googleKey: undefined,
@@ -5206,28 +6110,163 @@ function buildAgentCredentials(runtime) {
5206
6110
  openaiBaseUrl: ELIZA_CLOUD_OPENAI_BASE,
5207
6111
  githubToken: runtime.getSetting("GITHUB_TOKEN"),
5208
6112
  extraConfigToml: buildCodexCloudProviderToml(ELIZA_CLOUD_OPENAI_BASE)
5209
- };
6113
+ });
5210
6114
  return cloudCredentials;
5211
6115
  }
5212
- const directCredentials = {
5213
- anthropicKey: runtime.getSetting("ANTHROPIC_API_KEY"),
6116
+ const subscriptionMode = llmProvider === "subscription";
6117
+ const rawAnthropicKey = runtime.getSetting("ANTHROPIC_API_KEY");
6118
+ const anthropicKey = isAnthropicOAuthToken(rawAnthropicKey) ? undefined : rawAnthropicKey;
6119
+ const directCredentials = compactCredentials({
6120
+ anthropicKey: subscriptionMode ? undefined : anthropicKey,
5214
6121
  openaiKey: runtime.getSetting("OPENAI_API_KEY"),
5215
6122
  googleKey: runtime.getSetting("GOOGLE_GENERATIVE_AI_API_KEY"),
5216
6123
  githubToken: runtime.getSetting("GITHUB_TOKEN"),
5217
- anthropicBaseUrl: runtime.getSetting("ANTHROPIC_BASE_URL"),
6124
+ anthropicBaseUrl: subscriptionMode ? undefined : anthropicKey ? runtime.getSetting("ANTHROPIC_BASE_URL") : undefined,
5218
6125
  openaiBaseUrl: runtime.getSetting("OPENAI_BASE_URL")
5219
- };
6126
+ });
5220
6127
  return directCredentials;
5221
6128
  }
5222
6129
 
5223
6130
  // src/services/swarm-coordinator.ts
5224
6131
  init_ansi_utils();
6132
+
6133
+ // src/services/coordinator-event-normalizer.ts
6134
+ function normalizeSource(data) {
6135
+ const source = typeof data?.source === "string" ? data.source : "";
6136
+ switch (source) {
6137
+ case "pty_manager":
6138
+ case "adapter_fast_path":
6139
+ case "session_ready_forward":
6140
+ case "hook":
6141
+ return source;
6142
+ default:
6143
+ return "unknown";
6144
+ }
6145
+ }
6146
+ function normalizeSessionSnapshot(data) {
6147
+ const session = data?.session;
6148
+ if (!session || typeof session !== "object" || Array.isArray(session)) {
6149
+ return;
6150
+ }
6151
+ const record = session;
6152
+ const id = typeof record.id === "string" ? record.id : undefined;
6153
+ if (!id)
6154
+ return;
6155
+ return {
6156
+ id,
6157
+ ...typeof record.type === "string" ? { type: record.type } : {},
6158
+ ...typeof record.status === "string" ? { status: record.status } : {}
6159
+ };
6160
+ }
6161
+ function normalizeCoordinatorEvent(sessionId, event, data) {
6162
+ const timestamp = typeof data?.timestamp === "number" ? data.timestamp : Date.now();
6163
+ const source = normalizeSource(data);
6164
+ const session = normalizeSessionSnapshot(data);
6165
+ switch (event) {
6166
+ case "ready":
6167
+ return {
6168
+ sessionId,
6169
+ name: "ready",
6170
+ source,
6171
+ timestamp,
6172
+ rawData: data,
6173
+ ...session ? { session } : {}
6174
+ };
6175
+ case "blocked": {
6176
+ const promptInfo = data?.promptInfo;
6177
+ const promptRecord = promptInfo && typeof promptInfo === "object" && !Array.isArray(promptInfo) ? promptInfo : undefined;
6178
+ const promptText = typeof promptRecord?.prompt === "string" && promptRecord.prompt || typeof promptRecord?.instructions === "string" && promptRecord.instructions || "";
6179
+ return {
6180
+ sessionId,
6181
+ name: "blocked",
6182
+ source,
6183
+ timestamp,
6184
+ rawData: data,
6185
+ ...session ? { session } : {},
6186
+ promptText,
6187
+ ...typeof promptRecord?.type === "string" ? { promptType: promptRecord.type } : {},
6188
+ ...promptRecord ? { promptInfo: promptRecord } : {},
6189
+ autoResponded: data?.autoResponded === true
6190
+ };
6191
+ }
6192
+ case "login_required":
6193
+ return {
6194
+ sessionId,
6195
+ name: "login_required",
6196
+ source,
6197
+ timestamp,
6198
+ rawData: data,
6199
+ ...session ? { session } : {},
6200
+ ...typeof data?.instructions === "string" ? {
6201
+ instructions: data.instructions
6202
+ } : {},
6203
+ ...typeof data?.url === "string" ? {
6204
+ url: data.url
6205
+ } : {}
6206
+ };
6207
+ case "task_complete":
6208
+ return {
6209
+ sessionId,
6210
+ name: "task_complete",
6211
+ source,
6212
+ timestamp,
6213
+ rawData: data,
6214
+ ...session ? { session } : {},
6215
+ response: typeof data?.response === "string" ? data.response : ""
6216
+ };
6217
+ case "tool_running":
6218
+ return {
6219
+ sessionId,
6220
+ name: "tool_running",
6221
+ source,
6222
+ timestamp,
6223
+ rawData: data,
6224
+ ...session ? { session } : {},
6225
+ ...typeof data?.toolName === "string" ? { toolName: data.toolName } : {},
6226
+ ...typeof data?.description === "string" ? { description: data.description } : {}
6227
+ };
6228
+ case "stopped":
6229
+ return {
6230
+ sessionId,
6231
+ name: "stopped",
6232
+ source,
6233
+ timestamp,
6234
+ rawData: data,
6235
+ ...session ? { session } : {},
6236
+ ...typeof data?.reason === "string" ? { reason: data.reason } : {}
6237
+ };
6238
+ case "error":
6239
+ return {
6240
+ sessionId,
6241
+ name: "error",
6242
+ source,
6243
+ timestamp,
6244
+ rawData: data,
6245
+ ...session ? { session } : {},
6246
+ message: typeof data?.message === "string" ? data.message : "unknown error"
6247
+ };
6248
+ case "message":
6249
+ return {
6250
+ sessionId,
6251
+ name: "message",
6252
+ source,
6253
+ timestamp,
6254
+ rawData: data,
6255
+ ...session ? { session } : {},
6256
+ ...typeof data?.content === "string" ? { content: data.content } : {}
6257
+ };
6258
+ default:
6259
+ return null;
6260
+ }
6261
+ }
6262
+
6263
+ // src/services/swarm-coordinator.ts
5225
6264
  init_swarm_decision_loop();
5226
6265
 
5227
6266
  // src/services/swarm-history.ts
5228
- import * as fs from "fs/promises";
5229
- import * as os2 from "os";
5230
- import * as path4 from "path";
6267
+ import * as fs2 from "fs/promises";
6268
+ import * as os3 from "os";
6269
+ import * as path7 from "path";
5231
6270
  var MAX_ENTRIES = 150;
5232
6271
  var TRUNCATE_TO = 100;
5233
6272
  var MAX_FILE_SIZE_BYTES = 1048576;
@@ -5259,26 +6298,26 @@ class SwarmHistory {
5259
6298
  appendCount = 0;
5260
6299
  mutex = new WriteMutex;
5261
6300
  constructor(stateDir) {
5262
- const dir = stateDir || process.env.MILADY_STATE_DIR || process.env.ELIZA_STATE_DIR || path4.join(os2.homedir(), ".milady");
5263
- this.filePath = path4.join(dir, "swarm-history.jsonl");
6301
+ const dir = stateDir || process.env.MILADY_STATE_DIR || process.env.ELIZA_STATE_DIR || path7.join(os3.homedir(), ".milady");
6302
+ this.filePath = path7.join(dir, "swarm-history.jsonl");
5264
6303
  }
5265
6304
  async append(entry) {
5266
6305
  await this.mutex.acquire();
5267
6306
  try {
5268
- const dir = path4.dirname(this.filePath);
5269
- await fs.mkdir(dir, { recursive: true });
5270
- await fs.appendFile(this.filePath, `${JSON.stringify(entry)}
6307
+ const dir = path7.dirname(this.filePath);
6308
+ await fs2.mkdir(dir, { recursive: true });
6309
+ await fs2.appendFile(this.filePath, `${JSON.stringify(entry)}
5271
6310
  `, "utf-8");
5272
6311
  this.appendCount++;
5273
6312
  try {
5274
- const stat3 = await fs.stat(this.filePath);
6313
+ const stat3 = await fs2.stat(this.filePath);
5275
6314
  if (stat3.size > MAX_FILE_SIZE_BYTES) {
5276
6315
  await this.truncateInner(TRUNCATE_TO);
5277
6316
  return;
5278
6317
  }
5279
6318
  } catch {}
5280
6319
  if (this.appendCount >= MAX_ENTRIES - TRUNCATE_TO) {
5281
- const content = await fs.readFile(this.filePath, "utf-8");
6320
+ const content = await fs2.readFile(this.filePath, "utf-8");
5282
6321
  const lineCount = content.split(`
5283
6322
  `).filter((l) => l.trim() !== "").length;
5284
6323
  if (lineCount > MAX_ENTRIES) {
@@ -5294,7 +6333,7 @@ class SwarmHistory {
5294
6333
  }
5295
6334
  async readAll() {
5296
6335
  try {
5297
- const content = await fs.readFile(this.filePath, "utf-8");
6336
+ const content = await fs2.readFile(this.filePath, "utf-8");
5298
6337
  const entries = [];
5299
6338
  const lines = content.split(`
5300
6339
  `);
@@ -5329,7 +6368,7 @@ class SwarmHistory {
5329
6368
  const entries = await this.readAll();
5330
6369
  if (entries.length === 0) {
5331
6370
  try {
5332
- await fs.stat(this.filePath);
6371
+ await fs2.stat(this.filePath);
5333
6372
  console.error("[swarm-history] truncate aborted: file exists but readAll returned empty");
5334
6373
  return;
5335
6374
  } catch {
@@ -5346,7 +6385,7 @@ class SwarmHistory {
5346
6385
  `) + `
5347
6386
  `;
5348
6387
  }
5349
- await fs.writeFile(this.filePath, content, "utf-8");
6388
+ await fs2.writeFile(this.filePath, content, "utf-8");
5350
6389
  this.appendCount = 0;
5351
6390
  }
5352
6391
  }
@@ -5354,7 +6393,7 @@ class SwarmHistory {
5354
6393
  // src/services/swarm-idle-watchdog.ts
5355
6394
  init_ansi_utils();
5356
6395
  init_swarm_decision_loop();
5357
- import { ModelType as ModelType5 } from "@elizaos/core";
6396
+ import { ModelType as ModelType6 } from "@elizaos/core";
5358
6397
  var IDLE_THRESHOLD_MS = 5 * 60 * 1000;
5359
6398
  var MAX_IDLE_CHECKS = 4;
5360
6399
  async function scanIdleSessions(ctx) {
@@ -5510,7 +6549,7 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
5510
6549
  repo: taskCtx.repo,
5511
6550
  workdir: taskCtx.workdir,
5512
6551
  originalTask: taskCtx.originalTask
5513
- }, () => ctx.runtime.useModel(ModelType5.TEXT_SMALL, { prompt }));
6552
+ }, () => ctx.runtime.useModel(ModelType6.TEXT_SMALL, { prompt }));
5514
6553
  decision = parseCoordinationResponse(result);
5515
6554
  } catch (err) {
5516
6555
  ctx.log(`Idle check LLM call failed: ${err}`);
@@ -5554,7 +6593,7 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
5554
6593
  }
5555
6594
 
5556
6595
  // src/services/task-acceptance.ts
5557
- import { ModelType as ModelType6 } from "@elizaos/core";
6596
+ import { ModelType as ModelType7 } from "@elizaos/core";
5558
6597
  var MAX_CRITERIA = 7;
5559
6598
  function trimCriterion(value) {
5560
6599
  return value.replace(/^[\s*-]+/, "").replace(/\s+/g, " ").trim();
@@ -5641,7 +6680,7 @@ async function deriveTaskAcceptanceCriteria(runtime, input) {
5641
6680
  };
5642
6681
  }
5643
6682
  try {
5644
- const raw = await runtime.useModel(ModelType6.TEXT_SMALL, {
6683
+ const raw = await runtime.useModel(ModelType7.TEXT_SMALL, {
5645
6684
  prompt: buildAcceptancePrompt(input),
5646
6685
  temperature: 0.1,
5647
6686
  stream: false
@@ -5664,9 +6703,69 @@ async function deriveTaskAcceptanceCriteria(runtime, input) {
5664
6703
 
5665
6704
  // src/services/task-agent-frameworks.ts
5666
6705
  import { execFileSync } from "node:child_process";
5667
- import fs2 from "node:fs";
5668
- import os3 from "node:os";
5669
- import path5 from "node:path";
6706
+ import fs3 from "node:fs";
6707
+ import os4 from "node:os";
6708
+ import path8 from "node:path";
6709
+ var RESEARCH_SIGNAL_RE = /\b(research|investigate|analy[sz]e|analysis|compare|evaluate|review|study|summari[sz]e|deep research|look into|explore)\b/i;
6710
+ var PLANNING_SIGNAL_RE = /\b(plan|planning|roadmap|strategy|spec|architecture|design|scope|milestone|sequence|timeline)\b/i;
6711
+ var OPS_SIGNAL_RE = /\b(deploy|release|ship|rollback|monitor|incident|infra|infrastructure|configure|setup|docker|kubernetes|ci|cd|runbook)\b/i;
6712
+ var IMPLEMENTATION_SIGNAL_RE = /\b(code|coding|implement|fix|debug|refactor|write|build|patch|feature|server|api|component|function|typescrip?t|javascript|react)\b/i;
6713
+ var VERIFICATION_SIGNAL_RE = /\b(test|tests|verify|validation|prove|acceptance|check|regression|benchmark|lint|typecheck|qa)\b/i;
6714
+ var COORDINATION_SIGNAL_RE = /\b(parallel|delegate|subagent|sub-agent|swarm|coordinate|coordination|handoff|mailbox|scheduler|orchestrate)\b/i;
6715
+ var REPO_SIGNAL_RE = /\b(repo|repository|branch|commit|pull request|pr|diff|workspace|file|directory|codebase)\b/i;
6716
+ var FAST_ITERATION_SIGNAL_RE = /\b(fix|debug|patch|flaky|quick|fast|iterate|loop|unblock|repair)\b/i;
6717
+ var FRAMEWORK_CAPABILITY_PROFILES = {
6718
+ claude: {
6719
+ implementation: 0.95,
6720
+ research: 0.95,
6721
+ planning: 1,
6722
+ ops: 0.8,
6723
+ verification: 0.85,
6724
+ coordination: 1,
6725
+ repoWork: 0.9,
6726
+ fastIteration: 0.75
6727
+ },
6728
+ codex: {
6729
+ implementation: 1,
6730
+ research: 0.8,
6731
+ planning: 0.75,
6732
+ ops: 0.85,
6733
+ verification: 1,
6734
+ coordination: 0.9,
6735
+ repoWork: 1,
6736
+ fastIteration: 0.95
6737
+ },
6738
+ gemini: {
6739
+ implementation: 0.7,
6740
+ research: 1,
6741
+ planning: 0.95,
6742
+ ops: 0.7,
6743
+ verification: 0.6,
6744
+ coordination: 0.7,
6745
+ repoWork: 0.65,
6746
+ fastIteration: 0.7
6747
+ },
6748
+ aider: {
6749
+ implementation: 0.9,
6750
+ research: 0.45,
6751
+ planning: 0.45,
6752
+ ops: 0.75,
6753
+ verification: 0.85,
6754
+ coordination: 0.35,
6755
+ repoWork: 0.95,
6756
+ fastIteration: 1
6757
+ },
6758
+ pi: {
6759
+ implementation: 0.55,
6760
+ research: 0.5,
6761
+ planning: 0.55,
6762
+ ops: 0.5,
6763
+ verification: 0.5,
6764
+ coordination: 0.35,
6765
+ repoWork: 0.5,
6766
+ fastIteration: 0.5
6767
+ }
6768
+ };
5670
6769
  var FRAMEWORK_LABELS = {
5671
6770
  claude: "Claude Code",
5672
6771
  codex: "Codex",
@@ -5718,11 +6817,11 @@ function safeGetSetting(runtime, key) {
5718
6817
  }
5719
6818
  }
5720
6819
  function getUserHomeDir() {
5721
- return process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || os3.homedir();
6820
+ return process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || os4.homedir();
5722
6821
  }
5723
6822
  function readJsonFile(filePath) {
5724
6823
  try {
5725
- return JSON.parse(fs2.readFileSync(filePath, "utf8"));
6824
+ return JSON.parse(fs3.readFileSync(filePath, "utf8"));
5726
6825
  } catch {
5727
6826
  return null;
5728
6827
  }
@@ -5746,10 +6845,10 @@ function resolveMiladyConfigPath() {
5746
6845
  const explicit = process.env.MILADY_CONFIG_PATH?.trim() || process.env.ELIZA_CONFIG_PATH?.trim();
5747
6846
  if (explicit)
5748
6847
  return explicit;
5749
- const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path5.join(getUserHomeDir(), ".milady");
6848
+ const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path8.join(getUserHomeDir(), ".milady");
5750
6849
  const namespace = process.env.ELIZA_NAMESPACE?.trim();
5751
6850
  const filename = !namespace || namespace === "milady" ? "milady.json" : `${namespace}.json`;
5752
- return path5.join(stateDir, filename);
6851
+ return path8.join(stateDir, filename);
5753
6852
  }
5754
6853
  function readConfiguredSubscriptionProvider() {
5755
6854
  const config = readJsonFile(resolveMiladyConfigPath());
@@ -5765,7 +6864,7 @@ function readConfiguredSubscriptionProvider() {
5765
6864
  return typeof provider === "string" && provider.trim() ? provider.trim() : undefined;
5766
6865
  }
5767
6866
  function hasClaudeSubscriptionAuth() {
5768
- const credentialsPath = path5.join(getUserHomeDir(), ".claude", ".credentials.json");
6867
+ const credentialsPath = path8.join(getUserHomeDir(), ".claude", ".credentials.json");
5769
6868
  const fileToken = extractOauthAccessToken(readJsonFile(credentialsPath));
5770
6869
  if (fileToken)
5771
6870
  return true;
@@ -5784,7 +6883,7 @@ function hasClaudeApiKey(runtime) {
5784
6883
  return Boolean(process.env.ANTHROPIC_API_KEY?.trim() || safeGetSetting(runtime, "ANTHROPIC_API_KEY"));
5785
6884
  }
5786
6885
  function hasCodexSubscriptionAuth() {
5787
- const authPath = path5.join(getUserHomeDir(), ".codex", "auth.json");
6886
+ const authPath = path8.join(getUserHomeDir(), ".codex", "auth.json");
5788
6887
  const auth = readJsonFile(authPath);
5789
6888
  if (!auth || typeof auth !== "object" || Array.isArray(auth))
5790
6889
  return false;
@@ -5801,8 +6900,11 @@ function hasElizaCloudApiKey() {
5801
6900
  return Boolean(readConfigCloudKey("apiKey"));
5802
6901
  }
5803
6902
  function hasPiBinary() {
6903
+ return hasBinaryOnPath("pi");
6904
+ }
6905
+ function hasBinaryOnPath(binaryName) {
5804
6906
  const command = process.platform === "win32" ? "where" : "which";
5805
- const args = process.platform === "win32" ? ["pi.exe"] : ["pi"];
6907
+ const args = [binaryName];
5806
6908
  try {
5807
6909
  execFileSync(command, args, {
5808
6910
  encoding: "utf8",
@@ -5814,6 +6916,18 @@ function hasPiBinary() {
5814
6916
  return false;
5815
6917
  }
5816
6918
  }
6919
+ function hasFrameworkBinary(id) {
6920
+ switch (id) {
6921
+ case "claude":
6922
+ return hasBinaryOnPath("claude");
6923
+ case "codex":
6924
+ return hasBinaryOnPath("codex");
6925
+ case "gemini":
6926
+ return hasBinaryOnPath("gemini");
6927
+ case "aider":
6928
+ return hasBinaryOnPath("aider");
6929
+ }
6930
+ }
5817
6931
  function getFrameworkCooldown(id) {
5818
6932
  const cooldown = frameworkCooldowns.get(id);
5819
6933
  if (!cooldown)
@@ -5824,7 +6938,7 @@ function getFrameworkCooldown(id) {
5824
6938
  }
5825
6939
  return cooldown;
5826
6940
  }
5827
- async function computeTaskAgentFrameworkState(runtime, probe) {
6941
+ async function computeTaskAgentFrameworkState(runtime, probe, profileInput) {
5828
6942
  const configuredSubscriptionProvider = readConfiguredSubscriptionProvider();
5829
6943
  const preflightByAdapter = new Map;
5830
6944
  if (probe?.checkAvailableAgents) {
@@ -5848,10 +6962,10 @@ async function computeTaskAgentFrameworkState(runtime, probe) {
5848
6962
  const piReady = hasPiBinary();
5849
6963
  const providerPrefersClaude = configuredSubscriptionProvider === "anthropic-subscription";
5850
6964
  const providerPrefersCodex = configuredSubscriptionProvider === "openai-codex" || configuredSubscriptionProvider === "openai-subscription";
5851
- const frameworks = STANDARD_FRAMEWORKS.map((id) => {
6965
+ const inventory = STANDARD_FRAMEWORKS.map((id) => {
5852
6966
  const preflight = preflightByAdapter.get(id);
5853
6967
  const cooldown = getFrameworkCooldown(id);
5854
- const installed = preflight?.installed === true;
6968
+ const installed = preflight?.installed === true || hasFrameworkBinary(id);
5855
6969
  const subscriptionReady = id === "claude" ? claudeSubscriptionReady : id === "codex" ? codexSubscriptionReady : false;
5856
6970
  const authReady = id === "claude" ? claudeAuthReady : id === "codex" ? codexAuthReady : id === "gemini" ? geminiAuthReady : claudeAuthReady || codexAuthReady || geminiAuthReady;
5857
6971
  const reason = id === "claude" && subscriptionReady ? "ready to use the user's Claude subscription" : id === "codex" && subscriptionReady ? "ready to use the user's OpenAI subscription" : installed ? authReady ? "installed with credentials available" : "installed but credentials were not detected" : "CLI not detected";
@@ -5870,7 +6984,7 @@ async function computeTaskAgentFrameworkState(runtime, probe) {
5870
6984
  docsUrl: preflight?.docsUrl
5871
6985
  };
5872
6986
  });
5873
- frameworks.push({
6987
+ inventory.push({
5874
6988
  id: "pi",
5875
6989
  label: FRAMEWORK_LABELS.pi,
5876
6990
  installed: piReady,
@@ -5880,59 +6994,53 @@ async function computeTaskAgentFrameworkState(runtime, probe) {
5880
6994
  recommended: false,
5881
6995
  reason: piReady ? "CLI detected" : "CLI not detected"
5882
6996
  });
5883
- const byId = new Map(frameworks.map((framework) => [framework.id, framework]));
5884
- const isSelectable = (id) => !byId.get(id)?.temporarilyDisabled;
6997
+ const frameworks = inventory.map((framework) => ({
6998
+ ...framework,
6999
+ recommended: false
7000
+ }));
7001
+ const metrics = probe?.getAgentMetrics?.() ?? {};
7002
+ const profile = buildTaskAgentTaskProfile(profileInput);
5885
7003
  const explicitDefault = safeGetSetting(runtime, "PARALLAX_DEFAULT_AGENT_TYPE")?.toLowerCase().trim();
5886
- let preferred;
5887
- if (explicitDefault && (explicitDefault === "claude" || explicitDefault === "codex" || explicitDefault === "gemini" || explicitDefault === "aider" || explicitDefault === "pi") && byId.get(explicitDefault)?.installed && isSelectable(explicitDefault)) {
5888
- preferred = {
5889
- id: explicitDefault,
5890
- reason: "explicit PARALLAX_DEFAULT_AGENT_TYPE override"
5891
- };
5892
- } else if (providerPrefersClaude && byId.get("claude")?.installed && claudeSubscriptionReady && isSelectable("claude")) {
5893
- preferred = {
5894
- id: "claude",
5895
- reason: "configured Claude subscription should drive Claude Code first"
5896
- };
5897
- } else if (providerPrefersCodex && byId.get("codex")?.installed && codexSubscriptionReady && isSelectable("codex")) {
5898
- preferred = {
5899
- id: "codex",
5900
- reason: "configured OpenAI subscription should drive Codex first"
5901
- };
5902
- } else if (byId.get("claude")?.installed && claudeSubscriptionReady && isSelectable("claude")) {
5903
- preferred = {
5904
- id: "claude",
5905
- reason: "Claude Code is installed and the user is logged in"
5906
- };
5907
- } else if (byId.get("codex")?.installed && codexSubscriptionReady && isSelectable("codex")) {
5908
- preferred = {
5909
- id: "codex",
5910
- reason: "Codex is installed and the user is logged in"
5911
- };
5912
- } else if (byId.get("claude")?.installed && claudeAuthReady && isSelectable("claude")) {
5913
- preferred = {
5914
- id: "claude",
5915
- reason: "Claude Code is installed and credentials are available"
7004
+ const selectable = frameworks.filter((framework) => framework.installed && !framework.temporarilyDisabled);
7005
+ const candidates = selectable.length > 0 ? selectable : frameworks.filter((framework) => framework.installed);
7006
+ const scoredCandidates = candidates.map((framework) => {
7007
+ const explicitOverride = explicitDefault === framework.id ? framework.installed && !framework.temporarilyDisabled ? 40 : 0 : 0;
7008
+ const providerPreference = providerPrefersClaude && framework.id === "claude" ? framework.subscriptionReady ? 18 : 6 : providerPrefersCodex && framework.id === "codex" ? framework.subscriptionReady ? 18 : 6 : 0;
7009
+ const availabilityScore = (framework.installed ? 40 : -100) + (framework.authReady ? 18 : -25) + (framework.subscriptionReady ? 8 : 0) + (framework.temporarilyDisabled ? -80 : 0);
7010
+ const profileScore = computeProfileFitScore(framework.id, profile);
7011
+ const metricsScore = computeMetricsScore(metrics[framework.id], profile.signals.fastIteration);
7012
+ const selectionSignals = {
7013
+ availability: availabilityScore,
7014
+ profile: profileScore,
7015
+ provider: providerPreference,
7016
+ metrics: metricsScore,
7017
+ explicitOverride
5916
7018
  };
5917
- } else if (byId.get("codex")?.installed && codexAuthReady && isSelectable("codex")) {
5918
- preferred = {
5919
- id: "codex",
5920
- reason: "Codex is installed and credentials are available"
5921
- };
5922
- } else if (byId.get("gemini")?.installed && geminiAuthReady && isSelectable("gemini")) {
5923
- preferred = {
5924
- id: "gemini",
5925
- reason: "Gemini CLI is installed and credentials are available"
5926
- };
5927
- } else {
5928
- const fallback = frameworks.find((framework) => framework.installed && !framework.temporarilyDisabled) ?? frameworks.find((framework) => framework.installed) ?? frameworks[0];
5929
- preferred = {
5930
- id: fallback.id,
5931
- reason: fallback.installed ? "best available installed task-agent framework" : "default fallback while no task-agent CLI is installed"
7019
+ return {
7020
+ framework,
7021
+ score: Object.values(selectionSignals).reduce((sum, value) => sum + value, 0),
7022
+ selectionSignals
5932
7023
  };
5933
- }
7024
+ });
7025
+ const fallback = candidates[0] ?? frameworks.find((framework) => framework.installed) ?? frameworks[0];
7026
+ const preferredCandidate = scoredCandidates.sort((left, right) => {
7027
+ if (right.score !== left.score) {
7028
+ return right.score - left.score;
7029
+ }
7030
+ return left.framework.id.localeCompare(right.framework.id);
7031
+ })[0]?.framework ?? fallback;
7032
+ const preferredSignals = scoredCandidates.find((entry) => entry.framework.id === preferredCandidate.id)?.selectionSignals ?? {};
7033
+ const preferred = {
7034
+ id: preferredCandidate.id,
7035
+ reason: buildPreferredReason(preferredCandidate, profile, preferredSignals, explicitDefault, configuredSubscriptionProvider)
7036
+ };
5934
7037
  for (const framework of frameworks) {
5935
7038
  framework.recommended = framework.id === preferred.id;
7039
+ const scored = scoredCandidates.find((entry) => entry.framework.id === framework.id);
7040
+ if (scored) {
7041
+ framework.selectionScore = scored.score;
7042
+ framework.selectionSignals = scored.selectionSignals;
7043
+ }
5936
7044
  }
5937
7045
  return {
5938
7046
  configuredSubscriptionProvider,
@@ -5940,16 +7048,169 @@ async function computeTaskAgentFrameworkState(runtime, probe) {
5940
7048
  preferred
5941
7049
  };
5942
7050
  }
5943
- async function getTaskAgentFrameworkState(runtime, probe) {
7051
+ async function getTaskAgentFrameworkState(runtime, probe, profileInput) {
5944
7052
  if (frameworkStateCache && frameworkStateCache.expiresAt > Date.now()) {
5945
- return frameworkStateCache.value;
7053
+ return computeTaskAgentFrameworkStateFromInventory(runtime, frameworkStateCache.value, probe, profileInput);
7054
+ }
7055
+ const value = await computeTaskAgentFrameworkState(runtime, probe, profileInput);
7056
+ if (!profileInput) {
7057
+ frameworkStateCache = {
7058
+ expiresAt: Date.now() + 15000,
7059
+ value: {
7060
+ configuredSubscriptionProvider: value.configuredSubscriptionProvider,
7061
+ frameworks: value.frameworks.map((framework) => ({
7062
+ ...framework,
7063
+ recommended: false,
7064
+ selectionScore: undefined,
7065
+ selectionSignals: undefined
7066
+ }))
7067
+ }
7068
+ };
5946
7069
  }
5947
- const value = await computeTaskAgentFrameworkState(runtime, probe);
7070
+ return value;
7071
+ }
7072
+ function computeTaskAgentFrameworkStateFromInventory(runtime, inventory, probe, profileInput) {
7073
+ const clonedProbe = {
7074
+ ...probe,
7075
+ checkAvailableAgents: undefined
7076
+ };
5948
7077
  frameworkStateCache = {
5949
7078
  expiresAt: Date.now() + 15000,
5950
- value
7079
+ value: inventory
5951
7080
  };
5952
- return value;
7081
+ return {
7082
+ ...computeTaskAgentFrameworkStateFromCachedInventory(runtime, inventory, clonedProbe, profileInput)
7083
+ };
7084
+ }
7085
+ function computeTaskAgentFrameworkStateFromCachedInventory(runtime, inventory, probe, profileInput) {
7086
+ const metrics = probe?.getAgentMetrics?.() ?? {};
7087
+ const frameworks = inventory.frameworks.map((framework) => ({
7088
+ ...framework,
7089
+ recommended: false
7090
+ }));
7091
+ const profile = buildTaskAgentTaskProfile(profileInput);
7092
+ const configuredSubscriptionProvider = inventory.configuredSubscriptionProvider;
7093
+ const providerPrefersClaude = configuredSubscriptionProvider === "anthropic-subscription";
7094
+ const providerPrefersCodex = configuredSubscriptionProvider === "openai-codex" || configuredSubscriptionProvider === "openai-subscription";
7095
+ const explicitDefault = safeGetSetting(runtime, "PARALLAX_DEFAULT_AGENT_TYPE")?.toLowerCase().trim();
7096
+ const candidates = frameworks.filter((framework) => framework.installed && !framework.temporarilyDisabled).length > 0 ? frameworks.filter((framework) => framework.installed && !framework.temporarilyDisabled) : frameworks.filter((framework) => framework.installed);
7097
+ const scoredCandidates = candidates.map((framework) => {
7098
+ const explicitOverride = explicitDefault === framework.id ? framework.installed && !framework.temporarilyDisabled ? 40 : 0 : 0;
7099
+ const providerPreference = providerPrefersClaude && framework.id === "claude" ? framework.subscriptionReady ? 18 : 6 : providerPrefersCodex && framework.id === "codex" ? framework.subscriptionReady ? 18 : 6 : 0;
7100
+ const availabilityScore = (framework.installed ? 40 : -100) + (framework.authReady ? 18 : -25) + (framework.subscriptionReady ? 8 : 0) + (framework.temporarilyDisabled ? -80 : 0);
7101
+ const profileScore = computeProfileFitScore(framework.id, profile);
7102
+ const metricsScore = computeMetricsScore(metrics[framework.id], profile.signals.fastIteration);
7103
+ const selectionSignals = {
7104
+ availability: availabilityScore,
7105
+ profile: profileScore,
7106
+ provider: providerPreference,
7107
+ metrics: metricsScore,
7108
+ explicitOverride
7109
+ };
7110
+ return {
7111
+ framework,
7112
+ score: Object.values(selectionSignals).reduce((sum, value) => sum + value, 0),
7113
+ selectionSignals
7114
+ };
7115
+ });
7116
+ const fallback = candidates[0] ?? frameworks.find((framework) => framework.installed) ?? frameworks[0];
7117
+ const preferredCandidate = scoredCandidates.sort((left, right) => {
7118
+ if (right.score !== left.score) {
7119
+ return right.score - left.score;
7120
+ }
7121
+ return left.framework.id.localeCompare(right.framework.id);
7122
+ })[0]?.framework ?? fallback;
7123
+ const preferredSignals = scoredCandidates.find((entry) => entry.framework.id === preferredCandidate.id)?.selectionSignals ?? {};
7124
+ const preferred = {
7125
+ id: preferredCandidate.id,
7126
+ reason: buildPreferredReason(preferredCandidate, profile, preferredSignals, explicitDefault, configuredSubscriptionProvider)
7127
+ };
7128
+ for (const framework of frameworks) {
7129
+ framework.recommended = framework.id === preferred.id;
7130
+ const scored = scoredCandidates.find((entry) => entry.framework.id === framework.id);
7131
+ if (scored) {
7132
+ framework.selectionScore = scored.score;
7133
+ framework.selectionSignals = scored.selectionSignals;
7134
+ }
7135
+ }
7136
+ frameworkStateCache = {
7137
+ expiresAt: Date.now() + 15000,
7138
+ value: inventory
7139
+ };
7140
+ return {
7141
+ configuredSubscriptionProvider,
7142
+ frameworks,
7143
+ preferred
7144
+ };
7145
+ }
7146
+ function clampSignal(value) {
7147
+ return Math.max(0, Math.min(1, value));
7148
+ }
7149
+ function kindBoost(kind, target) {
7150
+ if (kind === "mixed")
7151
+ return 0.25;
7152
+ return kind === target ? 0.4 : 0;
7153
+ }
7154
+ function buildTaskAgentTaskProfile(input) {
7155
+ const text = [
7156
+ input?.task?.trim(),
7157
+ input?.repo?.trim(),
7158
+ ...(input?.acceptanceCriteria ?? []).map((value) => value.trim())
7159
+ ].filter((value) => Boolean(value)).join(`
7160
+ `);
7161
+ const inferredKind = input?.threadKind ?? (OPS_SIGNAL_RE.test(text) ? "ops" : PLANNING_SIGNAL_RE.test(text) ? "planning" : RESEARCH_SIGNAL_RE.test(text) && !IMPLEMENTATION_SIGNAL_RE.test(text) ? "research" : IMPLEMENTATION_SIGNAL_RE.test(text) ? "coding" : RESEARCH_SIGNAL_RE.test(text) ? "mixed" : "coding");
7162
+ const repoPresent = Boolean(input?.repo?.trim() || input?.workdir?.trim());
7163
+ const subtaskCount = Math.max(1, input?.subtaskCount ?? 1);
7164
+ const signals = {
7165
+ implementation: clampSignal((IMPLEMENTATION_SIGNAL_RE.test(text) ? 0.7 : 0.2) + (repoPresent ? 0.15 : 0) + kindBoost(inferredKind, "coding")),
7166
+ research: clampSignal((RESEARCH_SIGNAL_RE.test(text) ? 0.7 : 0.1) + kindBoost(inferredKind, "research")),
7167
+ planning: clampSignal((PLANNING_SIGNAL_RE.test(text) ? 0.75 : 0.1) + kindBoost(inferredKind, "planning")),
7168
+ ops: clampSignal((OPS_SIGNAL_RE.test(text) ? 0.75 : 0.05) + kindBoost(inferredKind, "ops")),
7169
+ verification: clampSignal((VERIFICATION_SIGNAL_RE.test(text) ? 0.8 : 0.15) + ((input?.acceptanceCriteria?.length ?? 0) > 0 ? 0.15 : 0)),
7170
+ coordination: clampSignal((COORDINATION_SIGNAL_RE.test(text) ? 0.7 : 0.05) + (subtaskCount > 1 ? 0.25 : 0)),
7171
+ repoWork: clampSignal((REPO_SIGNAL_RE.test(text) ? 0.7 : 0.1) + (repoPresent ? 0.25 : 0)),
7172
+ fastIteration: clampSignal((FAST_ITERATION_SIGNAL_RE.test(text) ? 0.75 : 0.15) + (inferredKind === "coding" ? 0.1 : 0))
7173
+ };
7174
+ return {
7175
+ text,
7176
+ kind: inferredKind,
7177
+ subtaskCount,
7178
+ repoPresent,
7179
+ signals
7180
+ };
7181
+ }
7182
+ function computeProfileFitScore(frameworkId, profile) {
7183
+ const capability = FRAMEWORK_CAPABILITY_PROFILES[frameworkId];
7184
+ const weightedSum = profile.signals.implementation * capability.implementation * 18 + profile.signals.research * capability.research * 16 + profile.signals.planning * capability.planning * 14 + profile.signals.ops * capability.ops * 12 + profile.signals.verification * capability.verification * 14 + profile.signals.coordination * capability.coordination * 14 + profile.signals.repoWork * capability.repoWork * 10 + profile.signals.fastIteration * capability.fastIteration * 10;
7185
+ return Math.round(weightedSum);
7186
+ }
7187
+ function computeMetricsScore(metrics, fastIterationSignal) {
7188
+ if (!metrics || metrics.spawned === 0) {
7189
+ return 0;
7190
+ }
7191
+ const successRate = metrics.spawned > 0 ? metrics.completed / metrics.spawned : 0;
7192
+ const stallRate = metrics.spawned > 0 ? metrics.stallCount / metrics.spawned : 0;
7193
+ const durationBonus = metrics.completed > 0 ? Math.max(-8, Math.min(8, (120000 - metrics.avgCompletionMs) / 120000 * (4 + fastIterationSignal * 4))) : 0;
7194
+ return Math.round(successRate * 14 - stallRate * 12 + durationBonus);
7195
+ }
7196
+ function buildPreferredReason(framework, profile, selectionSignals, explicitDefault, configuredSubscriptionProvider) {
7197
+ const dominantSignals = Object.entries(profile.signals).sort((left, right) => right[1] - left[1]).slice(0, 2).map(([key]) => key);
7198
+ if (explicitDefault === framework.id && selectionSignals.explicitOverride > 0) {
7199
+ return `explicit PARALLAX_DEFAULT_AGENT_TYPE override, with ${FRAMEWORK_LABELS[framework.id]} still scoring well for ${dominantSignals.join(" + ")} work`;
7200
+ }
7201
+ if (configuredSubscriptionProvider === "anthropic-subscription" && framework.id === "claude" && framework.subscriptionReady) {
7202
+ return `best fit for ${dominantSignals.join(" + ")} work while honoring the configured Claude subscription`;
7203
+ }
7204
+ if ((configuredSubscriptionProvider === "openai-codex" || configuredSubscriptionProvider === "openai-subscription") && framework.id === "codex" && framework.subscriptionReady) {
7205
+ return `best fit for ${dominantSignals.join(" + ")} work while honoring the configured OpenAI subscription`;
7206
+ }
7207
+ if (framework.subscriptionReady) {
7208
+ return `best overall score for ${dominantSignals.join(" + ")} work with subscription-backed auth already available`;
7209
+ }
7210
+ if (framework.authReady) {
7211
+ return `best overall score for ${dominantSignals.join(" + ")} work with credentials already available`;
7212
+ }
7213
+ return `selected as the highest-scoring installed framework for ${dominantSignals.join(" + ")} work`;
5953
7214
  }
5954
7215
  function clearTaskAgentFrameworkStateCache() {
5955
7216
  frameworkStateCache = undefined;
@@ -8147,6 +9408,8 @@ var PAUSE_TIMEOUT_MS = 30000;
8147
9408
  var MAX_PRE_BRIDGE_BUFFER = 100;
8148
9409
  var STOPPED_RECOVERY_WINDOW_MS = 90000;
8149
9410
  var FAILOVER_OUTPUT_MAX_CHARS = 4000;
9411
+ var MAX_AUTOMATIC_ERROR_RECOVERIES = 2;
9412
+ var ALTERNATE_FRAMEWORK_ERROR_RE = /\b(auth|login|credential|401|403|unauthorized|forbidden|token|api key|not found|enoent|missing executable|command not found)\b/i;
8150
9413
  function inferProviderSource(framework) {
8151
9414
  if (framework.subscriptionReady) {
8152
9415
  return "subscription";
@@ -8264,8 +9527,8 @@ class SwarmCoordinator {
8264
9527
  await this.taskRegistry.recoverInterruptedTasks();
8265
9528
  await this.rehydratePendingDecisions();
8266
9529
  this.ptyService = ptyService;
8267
- this.unsubscribeEvents = ptyService.onSessionEvent((sessionId, event, data) => {
8268
- this.handleSessionEvent(sessionId, event, data).catch((err) => {
9530
+ this.unsubscribeEvents = ptyService.onNormalizedSessionEvent((normalized) => {
9531
+ this.handleNormalizedSessionEvent(normalized).catch((err) => {
8269
9532
  this.log(`Error handling event: ${err}`);
8270
9533
  });
8271
9534
  });
@@ -8450,7 +9713,7 @@ class SwarmCoordinator {
8450
9713
  const buffered = [...this.pauseBuffer];
8451
9714
  this.pauseBuffer = [];
8452
9715
  for (const entry of buffered) {
8453
- this.handleSessionEvent(entry.sessionId, entry.event, entry.data).catch((err) => {
9716
+ this.handleNormalizedSessionEvent(entry).catch((err) => {
8454
9717
  this.log(`Error replaying buffered event: ${err}`);
8455
9718
  });
8456
9719
  }
@@ -8498,7 +9761,9 @@ class SwarmCoordinator {
8498
9761
  repo: context.repo,
8499
9762
  workdir: context.workdir,
8500
9763
  originalTask: context.originalTask
8501
- }).catch(() => {});
9764
+ }).catch((err) => {
9765
+ this.log(`Failed to append task registration history for ${sessionId}: ${err}`);
9766
+ });
8502
9767
  const taskCtx = this.tasks.get(sessionId);
8503
9768
  const persistPromise = taskCtx ? (async () => {
8504
9769
  const existingThread = await this.taskRegistry.getThreadRecord(threadId);
@@ -8619,7 +9884,7 @@ class SwarmCoordinator {
8619
9884
  if (buffered) {
8620
9885
  this.unregisteredBuffer.delete(sessionId);
8621
9886
  for (const entry of buffered) {
8622
- this.handleSessionEvent(sessionId, entry.event, entry.data).catch((err) => {
9887
+ this.handleNormalizedSessionEvent(entry.normalized).catch((err) => {
8623
9888
  this.log(`Error replaying buffered event: ${err}`);
8624
9889
  });
8625
9890
  }
@@ -9206,8 +10471,40 @@ ${transcriptExcerpt}` : "",
9206
10471
  const remainder = frameworks.filter((framework) => framework.id !== preferredFrameworkId);
9207
10472
  return [preferred, ...remainder].filter((framework) => Boolean(framework && framework.id !== failedFramework && framework.installed && framework.authReady && !framework.temporarilyDisabled));
9208
10473
  }
10474
+ getRecoveryCandidates(frameworks, currentFramework, preferredFrameworkId, preferAlternative) {
10475
+ const healthy = frameworks.filter((framework) => framework.installed && framework.authReady && !framework.temporarilyDisabled);
10476
+ const byId = new Map(healthy.map((framework) => [framework.id, framework]));
10477
+ const orderedIds = [];
10478
+ if (!preferAlternative) {
10479
+ orderedIds.push(currentFramework);
10480
+ }
10481
+ orderedIds.push(preferredFrameworkId);
10482
+ for (const framework of healthy) {
10483
+ orderedIds.push(framework.id);
10484
+ }
10485
+ const seen = new Set;
10486
+ const candidates = [];
10487
+ for (const id of orderedIds) {
10488
+ if (seen.has(id)) {
10489
+ continue;
10490
+ }
10491
+ seen.add(id);
10492
+ if (preferAlternative && id === currentFramework) {
10493
+ continue;
10494
+ }
10495
+ const framework = byId.get(id);
10496
+ if (framework) {
10497
+ candidates.push(framework);
10498
+ }
10499
+ }
10500
+ return candidates;
10501
+ }
10502
+ shouldPreferAlternativeFrameworkForError(reason) {
10503
+ return ALTERNATE_FRAMEWORK_ERROR_RE.test(reason);
10504
+ }
9209
10505
  formatFailoverPrompt(taskCtx, failedFramework, reason, recentOutput) {
9210
- const trimmedOutput = recentOutput.trim();
10506
+ const cleanedOutput = cleanForFailoverContext(recentOutput, taskCtx.workdir);
10507
+ const trimmedOutput = cleanedOutput.trim();
9211
10508
  const clippedOutput = trimmedOutput.length > FAILOVER_OUTPUT_MAX_CHARS ? trimmedOutput.slice(-FAILOVER_OUTPUT_MAX_CHARS) : trimmedOutput;
9212
10509
  const recentDecisions = taskCtx.decisions.slice(-5).map((decision, index) => `${index + 1}. ${decision.event}: ${decision.reasoning}${decision.response ? ` (response: ${decision.response})` : ""}`).join(`
9213
10510
  `);
@@ -9228,6 +10525,32 @@ ${clippedOutput}
9228
10525
  ` : "",
9229
10526
  "Use the existing workspace state instead of starting from scratch. Inspect the files, continue the task, run the needed validation, and then report what changed and how you verified it."
9230
10527
  ].filter(Boolean).join(`
10528
+ `);
10529
+ }
10530
+ formatErrorRecoveryPrompt(taskCtx, recoveryFramework, reason, recentOutput) {
10531
+ const cleanedOutput = cleanForFailoverContext(recentOutput, taskCtx.workdir);
10532
+ const trimmedOutput = cleanedOutput.trim();
10533
+ const clippedOutput = trimmedOutput.length > FAILOVER_OUTPUT_MAX_CHARS ? trimmedOutput.slice(-FAILOVER_OUTPUT_MAX_CHARS) : trimmedOutput;
10534
+ const recentDecisions = taskCtx.decisions.slice(-5).map((decision, index) => `${index + 1}. ${decision.event}: ${decision.reasoning}${decision.response ? ` (response: ${decision.response})` : ""}`).join(`
10535
+ `);
10536
+ const recoveryMode = recoveryFramework === taskCtx.agentType ? `a fresh ${recoveryFramework} session` : `a ${recoveryFramework} recovery session`;
10537
+ return [
10538
+ `Continue an in-progress task after the previous session terminated unexpectedly. Milady started ${recoveryMode} for recovery.`,
10539
+ "",
10540
+ "Original task:",
10541
+ taskCtx.originalTask,
10542
+ "",
10543
+ `Failure reason: ${reason}`,
10544
+ `Workspace: ${taskCtx.workdir}`,
10545
+ "",
10546
+ recentDecisions ? `Recent coordinator decisions:
10547
+ ${recentDecisions}
10548
+ ` : "",
10549
+ clippedOutput ? `Recent terminal output from the failed session:
10550
+ ${clippedOutput}
10551
+ ` : "",
10552
+ "Use the existing workspace state instead of starting over. Inspect the current files, recover from the failure, continue the task, run the needed validation, and then report exactly what changed and how you verified it."
10553
+ ].filter(Boolean).join(`
9231
10554
  `);
9232
10555
  }
9233
10556
  async handleFrameworkDepletion(taskCtx, sessionId, reason) {
@@ -9325,6 +10648,95 @@ ${clippedOutput}
9325
10648
  replacementLabel
9326
10649
  };
9327
10650
  }
10651
+ async attemptTaskRecovery(taskCtx, errorMsg) {
10652
+ if (!this.ptyService) {
10653
+ return null;
10654
+ }
10655
+ const failedSession = this.ptyService.getSession(taskCtx.sessionId);
10656
+ const priorMetadata = failedSession?.metadata && typeof failedSession.metadata === "object" && !Array.isArray(failedSession.metadata) ? failedSession.metadata : {};
10657
+ const recoveryOrdinal = typeof priorMetadata.recoveryOrdinal === "number" ? priorMetadata.recoveryOrdinal + 1 : 1;
10658
+ if (recoveryOrdinal > MAX_AUTOMATIC_ERROR_RECOVERIES) {
10659
+ return null;
10660
+ }
10661
+ let recoveryFramework = taskCtx.agentType;
10662
+ let recoveryAvailability = null;
10663
+ if (this.isAutomaticFailoverFramework(taskCtx.agentType)) {
10664
+ const frameworkState = await this.ptyService.getFrameworkState();
10665
+ const candidates = this.getRecoveryCandidates(frameworkState.frameworks, taskCtx.agentType, frameworkState.preferred.id, this.shouldPreferAlternativeFrameworkForError(errorMsg));
10666
+ const selected = candidates[0];
10667
+ if (!selected) {
10668
+ return null;
10669
+ }
10670
+ recoveryFramework = selected.id;
10671
+ recoveryAvailability = selected;
10672
+ }
10673
+ const priorOutput = await Promise.race([
10674
+ this.ptyService.getSessionOutput(taskCtx.sessionId, 200),
10675
+ new Promise((resolve2) => setTimeout(() => resolve2(""), 5000))
10676
+ ]);
10677
+ const replacementLabel = `${taskCtx.label} (${recoveryFramework} recovery ${recoveryOrdinal})`;
10678
+ const replacementSession = await this.ptyService.spawnSession({
10679
+ name: failedSession?.name ?? `task-recovery-${Date.now()}-${recoveryFramework}`,
10680
+ agentType: recoveryFramework,
10681
+ workdir: taskCtx.workdir,
10682
+ initialTask: this.formatErrorRecoveryPrompt(taskCtx, recoveryFramework, errorMsg, priorOutput),
10683
+ credentials: buildAgentCredentials(this.runtime),
10684
+ approvalPreset: this.ptyService.defaultApprovalPreset,
10685
+ skipAdapterAutoResponse: true,
10686
+ metadata: {
10687
+ ...priorMetadata,
10688
+ threadId: taskCtx.threadId,
10689
+ requestedType: recoveryFramework,
10690
+ label: replacementLabel,
10691
+ recoveryOrdinal,
10692
+ recoveredFromFramework: taskCtx.agentType,
10693
+ recoveredFromSessionId: taskCtx.sessionId,
10694
+ recoveryReason: errorMsg,
10695
+ recoveryAt: Date.now()
10696
+ }
10697
+ });
10698
+ await this.registerTask(replacementSession.id, {
10699
+ threadId: taskCtx.threadId,
10700
+ taskNodeId: taskCtx.taskNodeId,
10701
+ agentType: recoveryFramework,
10702
+ label: replacementLabel,
10703
+ originalTask: taskCtx.originalTask,
10704
+ workdir: taskCtx.workdir,
10705
+ repo: taskCtx.repo,
10706
+ providerSource: recoveryAvailability ? inferProviderSource(recoveryAvailability) : null,
10707
+ metadata: replacementSession.metadata && typeof replacementSession.metadata === "object" && !Array.isArray(replacementSession.metadata) ? replacementSession.metadata : undefined
10708
+ });
10709
+ await this.taskRegistry.appendEvent({
10710
+ threadId: taskCtx.threadId,
10711
+ sessionId: replacementSession.id,
10712
+ eventType: "task_error_recovery_started",
10713
+ summary: `Continuing "${taskCtx.label}" after an agent error`,
10714
+ data: {
10715
+ fromFramework: taskCtx.agentType,
10716
+ fromSessionId: taskCtx.sessionId,
10717
+ toFramework: recoveryFramework,
10718
+ toSessionId: replacementSession.id,
10719
+ reason: errorMsg,
10720
+ recoveryOrdinal
10721
+ }
10722
+ });
10723
+ this.broadcast({
10724
+ type: "task_recovery_started",
10725
+ sessionId: replacementSession.id,
10726
+ timestamp: Date.now(),
10727
+ data: {
10728
+ fromSessionId: taskCtx.sessionId,
10729
+ fromFramework: taskCtx.agentType,
10730
+ toFramework: recoveryFramework,
10731
+ reason: errorMsg
10732
+ }
10733
+ });
10734
+ return {
10735
+ replacementSessionId: replacementSession.id,
10736
+ replacementFramework: recoveryFramework,
10737
+ replacementLabel
10738
+ };
10739
+ }
9328
10740
  async recordDecision(taskCtx, decision) {
9329
10741
  taskCtx.decisions.push(decision);
9330
10742
  await this.taskRegistry.recordDecision({
@@ -9357,7 +10769,9 @@ ${clippedOutput}
9357
10769
  if (ctx) {
9358
10770
  this.unregisteredBuffer.delete(sessionId);
9359
10771
  for (const entry of stillBuffered) {
9360
- this.handleSessionEvent(sessionId, entry.event, entry.data).catch(() => {});
10772
+ this.handleNormalizedSessionEvent(entry.normalized).catch((err) => {
10773
+ this.log(`Failed to replay buffered event for ${sessionId}: ${err}`);
10774
+ });
9361
10775
  }
9362
10776
  return;
9363
10777
  }
@@ -9418,6 +10832,22 @@ ${clippedOutput}
9418
10832
  } catch {}
9419
10833
  }
9420
10834
  async handleSessionEvent(sessionId, event, data) {
10835
+ const normalized = normalizeCoordinatorEvent(sessionId, event, data);
10836
+ if (!normalized) {
10837
+ this.broadcast({
10838
+ type: event,
10839
+ sessionId,
10840
+ timestamp: Date.now(),
10841
+ data
10842
+ });
10843
+ return;
10844
+ }
10845
+ await this.handleNormalizedSessionEvent(normalized);
10846
+ }
10847
+ async handleNormalizedSessionEvent(normalized) {
10848
+ const sessionId = normalized.sessionId;
10849
+ const event = normalized.name;
10850
+ const data = normalized.rawData;
9421
10851
  if (!this.scratchDecisionWired) {
9422
10852
  this.wireScratchDecisionCallback();
9423
10853
  }
@@ -9436,7 +10866,7 @@ ${clippedOutput}
9436
10866
  buffer = [];
9437
10867
  this.unregisteredBuffer.set(sessionId, buffer);
9438
10868
  }
9439
- buffer.push({ event, data, receivedAt: Date.now() });
10869
+ buffer.push({ normalized, receivedAt: Date.now() });
9440
10870
  if (!this.unregisteredRetryTimers.has(sessionId)) {
9441
10871
  this.scheduleUnregisteredRetry(sessionId, 0);
9442
10872
  }
@@ -9466,22 +10896,23 @@ ${clippedOutput}
9466
10896
  taskCtx.lastActivityAt = Date.now();
9467
10897
  taskCtx.idleCheckCount = 0;
9468
10898
  if (this._paused && (event === "blocked" || event === "task_complete")) {
9469
- const eventData = data;
9470
- if (!(event === "blocked" && eventData.autoResponded)) {
10899
+ const blockedAutoResponded = event === "blocked" && normalized.autoResponded === true;
10900
+ if (!blockedAutoResponded) {
9471
10901
  this.broadcast({
9472
10902
  type: event === "blocked" ? "blocked_buffered" : "turn_complete_buffered",
9473
10903
  sessionId,
9474
10904
  timestamp: Date.now(),
9475
10905
  data
9476
10906
  });
9477
- this.pauseBuffer.push({ sessionId, event, data });
10907
+ this.pauseBuffer.push(normalized);
9478
10908
  this.log(`Buffered "${event}" for ${taskCtx.label} (coordinator paused)`);
9479
10909
  return;
9480
10910
  }
9481
10911
  }
9482
10912
  switch (event) {
9483
10913
  case "blocked": {
9484
- const blockedPrompt = data.promptInfo?.prompt ?? data.promptInfo?.instructions ?? "";
10914
+ const blockedEvent = normalized;
10915
+ const blockedPrompt = blockedEvent.promptText;
9485
10916
  if (this.isAutomaticFailoverFramework(taskCtx.agentType) && isUsageExhaustedTaskAgentError(blockedPrompt)) {
9486
10917
  const failoverResult = await this.handleFrameworkDepletion(taskCtx, sessionId, blockedPrompt);
9487
10918
  taskCtx.status = "error";
@@ -9549,8 +10980,18 @@ ${clippedOutput}
9549
10980
  });
9550
10981
  const errorMsg = data.message ?? "unknown error";
9551
10982
  const failoverResult = await this.handleFrameworkDepletion(taskCtx, sessionId, errorMsg);
10983
+ let recoveryResult = null;
9552
10984
  if (!failoverResult) {
9553
- this.sendChatMessage(`"${taskCtx.label}" hit an error: ${errorMsg}`, "coding-agent");
10985
+ try {
10986
+ recoveryResult = await this.attemptTaskRecovery(taskCtx, errorMsg);
10987
+ } catch (recoveryError) {
10988
+ this.log(`Automatic error recovery failed for "${taskCtx.label}": ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
10989
+ }
10990
+ }
10991
+ if (recoveryResult) {
10992
+ this.sendChatMessage(`"${taskCtx.label}" hit an error: ${errorMsg}. Milady is continuing the same task on ${recoveryResult.replacementFramework}.`, "coding-agent");
10993
+ } else if (!failoverResult) {
10994
+ this.sendChatMessage(`"${taskCtx.label}" hit an error and needs your attention: ${errorMsg}`, "coding-agent");
9554
10995
  }
9555
10996
  taskCtx.status = "error";
9556
10997
  await this.taskRegistry.appendEvent({
@@ -9560,10 +11001,13 @@ ${clippedOutput}
9560
11001
  summary: `Task "${taskCtx.label}" errored`,
9561
11002
  data: { status: "error", message: errorMsg }
9562
11003
  });
9563
- checkAllTasksComplete(this);
11004
+ if (!failoverResult && !recoveryResult) {
11005
+ checkAllTasksComplete(this);
11006
+ }
9564
11007
  break;
9565
11008
  }
9566
- case "stopped":
11009
+ case "stopped": {
11010
+ const alreadyTerminal = taskCtx.status === "completed" || taskCtx.status === "error";
9567
11011
  if (taskCtx.status !== "completed" && taskCtx.status !== "error") {
9568
11012
  taskCtx.status = "stopped";
9569
11013
  taskCtx.stoppedAt = Date.now();
@@ -9582,8 +11026,12 @@ ${clippedOutput}
9582
11026
  summary: `Task "${taskCtx.label}" stopped`,
9583
11027
  data: { status: taskCtx.status }
9584
11028
  });
11029
+ if (!alreadyTerminal) {
11030
+ this.sendChatMessage(`"${taskCtx.label}" stopped before completion.`, "coding-agent");
11031
+ }
9585
11032
  checkAllTasksComplete(this);
9586
11033
  break;
11034
+ }
9587
11035
  case "ready":
9588
11036
  taskCtx.status = "active";
9589
11037
  if (taskCtx.agentType === "claude" || taskCtx.agentType === "codex" || taskCtx.agentType === "gemini" || taskCtx.agentType === "aider") {
@@ -9603,6 +11051,35 @@ ${clippedOutput}
9603
11051
  data: { status: "ready" }
9604
11052
  });
9605
11053
  break;
11054
+ case "login_required": {
11055
+ const loginEvent = normalized;
11056
+ taskCtx.status = "blocked";
11057
+ this.broadcast({
11058
+ type: "login_required",
11059
+ sessionId,
11060
+ timestamp: Date.now(),
11061
+ data
11062
+ });
11063
+ await this.taskRegistry.appendEvent({
11064
+ threadId: taskCtx.threadId,
11065
+ sessionId,
11066
+ eventType: "task_status_changed",
11067
+ summary: `Task "${taskCtx.label}" is waiting for login`,
11068
+ data: {
11069
+ status: "blocked",
11070
+ reason: "login_required",
11071
+ instructions: loginEvent.instructions ?? null,
11072
+ url: loginEvent.url ?? null
11073
+ }
11074
+ });
11075
+ const loginParts = [
11076
+ `"${taskCtx.label}" needs a provider login before it can continue.`,
11077
+ loginEvent.instructions?.trim() || "",
11078
+ loginEvent.url ? `Login link: ${loginEvent.url}` : ""
11079
+ ].filter(Boolean);
11080
+ this.sendChatMessage(loginParts.join(" "), "coding-agent");
11081
+ break;
11082
+ }
9606
11083
  case "tool_running": {
9607
11084
  taskCtx.status = "tool_running";
9608
11085
  taskCtx.lastActivityAt = Date.now();
@@ -9636,7 +11113,9 @@ ${clippedOutput}
9636
11113
  }
9637
11114
  } catch {}
9638
11115
  }
9639
- this.log(`[${taskCtx.label}] Running ${toolDesc}.${urlSuffix} The agent is working outside the terminal.`);
11116
+ const message = `[${taskCtx.label}] Running ${toolDesc}.${urlSuffix} The agent is working outside the terminal.`;
11117
+ this.log(message);
11118
+ this.sendChatMessage(message, "coding-agent");
9640
11119
  }
9641
11120
  break;
9642
11121
  }
@@ -9701,6 +11180,7 @@ ${clippedOutput}
9701
11180
  response: decision.action === "respond" ? decision.useKeys ? `keys:${decision.keys?.join(",")}` : decision.response : undefined,
9702
11181
  reasoning: `Human-approved: ${decision.reasoning}`
9703
11182
  });
11183
+ await this.syncTaskContext(taskCtx);
9704
11184
  }
9705
11185
  await this.executeDecision(sessionId, decision);
9706
11186
  this.pendingDecisions.delete(sessionId);
@@ -9728,6 +11208,9 @@ ${clippedOutput}
9728
11208
  keys: decision.keys
9729
11209
  }
9730
11210
  });
11211
+ if (taskCtx) {
11212
+ this.sendChatMessage(`"${taskCtx.label}" was approved. Milady is continuing the task now.`, "coding-agent");
11213
+ }
9731
11214
  } else {
9732
11215
  if (taskCtx) {
9733
11216
  taskCtx.status = "blocked";
@@ -9738,6 +11221,7 @@ ${clippedOutput}
9738
11221
  decision: "escalate",
9739
11222
  reasoning: "Human rejected the suggested action"
9740
11223
  });
11224
+ await this.syncTaskContext(taskCtx);
9741
11225
  }
9742
11226
  this.pendingDecisions.delete(sessionId);
9743
11227
  await this.taskRegistry.deletePendingDecision(sessionId);
@@ -9756,6 +11240,7 @@ ${clippedOutput}
9756
11240
  timestamp: Date.now(),
9757
11241
  data: { prompt: pending.promptText }
9758
11242
  });
11243
+ this.sendChatMessage(`"${pending.taskContext.label}" remains blocked after the suggested action was rejected. Prompt: ${pending.promptText}`, "coding-agent");
9759
11244
  }
9760
11245
  }
9761
11246
  log(message) {
@@ -9765,6 +11250,21 @@ ${clippedOutput}
9765
11250
 
9766
11251
  // src/services/pty-service.ts
9767
11252
  init_swarm_decision_loop();
11253
+ function buildWorkspaceLockMemory(workdir) {
11254
+ return `# Workspace
11255
+
11256
+ Your working directory is \`${workdir}\`. Stay inside it: do not \`cd\` to \`/tmp\`, \`/\`, \`$HOME\`, or any other path outside the workspace. Create all files, run all builds, and start all servers from this directory. If you need scratch space, make a subdirectory here.`;
11257
+ }
11258
+ function prependWorkspaceLockToTask(task, workspaceLock) {
11259
+ if (!task?.trim()) {
11260
+ return;
11261
+ }
11262
+ return `${workspaceLock}
11263
+
11264
+ ---
11265
+
11266
+ ${task}`;
11267
+ }
9768
11268
  function getCoordinator(runtime) {
9769
11269
  const ptyService = runtime.getService("PTY_SERVICE");
9770
11270
  return ptyService?.coordinator ?? undefined;
@@ -9781,9 +11281,12 @@ class PTYService {
9781
11281
  sessionMetadata = new Map;
9782
11282
  sessionWorkdirs = new Map;
9783
11283
  eventCallbacks = [];
11284
+ normalizedEventCallbacks = [];
9784
11285
  outputUnsubscribers = new Map;
9785
11286
  transcriptUnsubscribers = new Map;
9786
11287
  sessionOutputBuffers = new Map;
11288
+ completionReconcileTimers = new Map;
11289
+ completionSignalSince = new Map;
9787
11290
  terminalSessionStates = new Map;
9788
11291
  adapterCache = new Map;
9789
11292
  taskResponseMarkers = new Map;
@@ -9898,6 +11401,11 @@ class PTYService {
9898
11401
  unsubscribe();
9899
11402
  }
9900
11403
  this.transcriptUnsubscribers.clear();
11404
+ for (const timer of this.completionReconcileTimers.values()) {
11405
+ clearInterval(timer);
11406
+ }
11407
+ this.completionReconcileTimers.clear();
11408
+ this.completionSignalSince.clear();
9901
11409
  if (this.manager) {
9902
11410
  await this.manager.shutdown();
9903
11411
  this.manager = null;
@@ -9926,7 +11434,6 @@ class PTYService {
9926
11434
  }
9927
11435
  const piRequested = isPiAgentType(options.agentType);
9928
11436
  const resolvedAgentType = piRequested ? "shell" : options.agentType;
9929
- const resolvedInitialTask = piRequested ? toPiCommand(options.initialTask) : options.initialTask;
9930
11437
  const effectiveApprovalPreset = options.approvalPreset ?? (resolvedAgentType !== "shell" ? this.defaultApprovalPreset : undefined);
9931
11438
  const maxSessions = this.serviceConfig.maxConcurrentSessions ?? 8;
9932
11439
  const activeSessions = (await this.listSessions()).length;
@@ -9935,10 +11442,19 @@ class PTYService {
9935
11442
  }
9936
11443
  const sessionId = this.generateSessionId();
9937
11444
  const workdir = options.workdir ?? process.cwd();
11445
+ const workspaceLock = buildWorkspaceLockMemory(workdir);
11446
+ const shouldWriteMemoryFile = resolvedAgentType !== "shell" && Boolean(options.memoryContent?.trim());
11447
+ const effectiveInitialTask = shouldWriteMemoryFile ? options.initialTask : prependWorkspaceLockToTask(options.initialTask, workspaceLock);
11448
+ const resolvedInitialTask = piRequested ? toPiCommand(effectiveInitialTask) : effectiveInitialTask;
9938
11449
  this.sessionWorkdirs.set(sessionId, workdir);
9939
- if (options.memoryContent && resolvedAgentType !== "shell") {
11450
+ if (shouldWriteMemoryFile) {
11451
+ const fullMemory = options.memoryContent ? `${workspaceLock}
11452
+
11453
+ ---
11454
+
11455
+ ${options.memoryContent}` : workspaceLock;
9940
11456
  try {
9941
- const writtenPath = await this.writeMemoryFile(resolvedAgentType, workdir, options.memoryContent);
11457
+ const writtenPath = await this.writeMemoryFile(resolvedAgentType, workdir, fullMemory);
9942
11458
  this.log(`Wrote memory file for ${resolvedAgentType}: ${writtenPath}`);
9943
11459
  } catch (err) {
9944
11460
  this.log(`Failed to write memory file for ${resolvedAgentType}: ${err}`);
@@ -9978,8 +11494,8 @@ class PTYService {
9978
11494
  settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
9979
11495
  this.log(`Injecting HTTP hooks for session ${sessionId}`);
9980
11496
  }
9981
- await mkdir3(dirname2(settingsPath), { recursive: true });
9982
- await writeFile4(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
11497
+ await mkdir4(dirname2(settingsPath), { recursive: true });
11498
+ await writeFile5(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
9983
11499
  this.log(`Wrote allowedDirectories [${workdir}] to ${settingsPath}`);
9984
11500
  } catch (err) {
9985
11501
  this.log(`Failed to write Claude settings: ${err}`);
@@ -10002,8 +11518,8 @@ class PTYService {
10002
11518
  settings.hooks = { ...existingHooks, ...hookProtocol.settingsHooks };
10003
11519
  this.log(`Injecting Gemini CLI hooks for session ${sessionId}`);
10004
11520
  }
10005
- await mkdir3(dirname2(settingsPath), { recursive: true });
10006
- await writeFile4(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
11521
+ await mkdir4(dirname2(settingsPath), { recursive: true });
11522
+ await writeFile5(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
10007
11523
  } catch (err) {
10008
11524
  this.log(`Failed to write Gemini settings: ${err}`);
10009
11525
  }
@@ -10017,20 +11533,6 @@ class PTYService {
10017
11533
  initialTask: resolvedInitialTask,
10018
11534
  approvalPreset: effectiveApprovalPreset
10019
11535
  }, workdir);
10020
- {
10021
- const ac = spawnConfig.adapterConfig;
10022
- const mask = (v) => typeof v === "string" && v.length > 12 ? `${v.slice(0, 8)}...${v.slice(-4)}` : String(v);
10023
- const parts = [];
10024
- if (ac?.anthropicKey)
10025
- parts.push(`anthropicKey=${mask(ac.anthropicKey)}`);
10026
- if (ac?.anthropicBaseUrl)
10027
- parts.push(`anthropicBaseUrl=${ac.anthropicBaseUrl}`);
10028
- if (ac?.openaiKey)
10029
- parts.push(`openaiKey=${mask(ac.openaiKey)}`);
10030
- if (ac?.openaiBaseUrl)
10031
- parts.push(`openaiBaseUrl=${ac.openaiBaseUrl}`);
10032
- this.log(`[DEBUG] PTY spawn ${resolvedAgentType} adapterConfig credentials: ${parts.join(", ") || "(none)"}`);
10033
- }
10034
11536
  const session = await this.manager.spawn(spawnConfig);
10035
11537
  this.terminalSessionStates.delete(session.id);
10036
11538
  this.sessionNames.set(session.id, options.name);
@@ -10120,7 +11622,13 @@ class PTYService {
10120
11622
  throw new Error("PTYService not initialized");
10121
11623
  captureFeed(sessionId, input, "stdin");
10122
11624
  this.persistTranscript(sessionId, "stdin", input);
10123
- return sendToSession(this.ioContext(), sessionId, input);
11625
+ const metadata = this.sessionMetadata.get(sessionId);
11626
+ if (metadata) {
11627
+ metadata.lastSentInput = input;
11628
+ }
11629
+ const message = await sendToSession(this.ioContext(), sessionId, input);
11630
+ this.scheduleCompletionReconcile(sessionId);
11631
+ return message;
10124
11632
  }
10125
11633
  async sendKeysToSession(sessionId, keys) {
10126
11634
  if (!this.manager)
@@ -10136,6 +11644,7 @@ class PTYService {
10136
11644
  try {
10137
11645
  return await stopSession(this.ioContext(), sessionId, this.sessionMetadata, this.sessionWorkdirs, (msg) => this.log(msg), force);
10138
11646
  } finally {
11647
+ this.clearCompletionReconcile(sessionId);
10139
11648
  this.clearTranscriptCapture(sessionId);
10140
11649
  }
10141
11650
  }
@@ -10154,19 +11663,36 @@ class PTYService {
10154
11663
  return "fixed";
10155
11664
  }
10156
11665
  get defaultAgentType() {
11666
+ return this.explicitDefaultAgentType ?? "claude";
11667
+ }
11668
+ get explicitDefaultAgentType() {
10157
11669
  const fromConfig = readConfigEnvKey("PARALLAX_DEFAULT_AGENT_TYPE");
10158
11670
  const fromRuntimeOrEnv = fromConfig || this.runtime.getSetting("PARALLAX_DEFAULT_AGENT_TYPE");
10159
11671
  if (fromRuntimeOrEnv && ["claude", "gemini", "codex", "aider"].includes(fromRuntimeOrEnv.toLowerCase())) {
10160
11672
  return fromRuntimeOrEnv.toLowerCase();
10161
11673
  }
10162
- return "claude";
11674
+ return null;
10163
11675
  }
10164
- async resolveAgentType() {
10165
- const frameworkState = await this.getFrameworkState();
11676
+ async resolveAgentType(selection) {
11677
+ if (this.agentSelectionStrategy === "fixed" && this.explicitDefaultAgentType) {
11678
+ return this.explicitDefaultAgentType;
11679
+ }
11680
+ const frameworkState = await this.getFrameworkState(selection);
10166
11681
  return frameworkState.preferred.id;
10167
11682
  }
10168
- async getFrameworkState() {
10169
- return getTaskAgentFrameworkState(this.runtime, this);
11683
+ async getFrameworkState(selection) {
11684
+ const profile = selection ? buildTaskAgentTaskProfile(selection) : undefined;
11685
+ return getTaskAgentFrameworkState(this.runtime, {
11686
+ checkAvailableAgents: (types) => this.checkAvailableAgents(types),
11687
+ getAgentMetrics: () => this.metricsTracker.getAll()
11688
+ }, profile ? {
11689
+ task: selection?.task,
11690
+ repo: selection?.repo,
11691
+ workdir: selection?.workdir,
11692
+ threadKind: profile.kind,
11693
+ subtaskCount: profile.subtaskCount,
11694
+ acceptanceCriteria: selection?.acceptanceCriteria
11695
+ } : selection);
10170
11696
  }
10171
11697
  getSession(sessionId) {
10172
11698
  if (!this.manager)
@@ -10180,7 +11706,10 @@ class PTYService {
10180
11706
  if (!this.manager)
10181
11707
  return [];
10182
11708
  const sessions = this.usingBunWorker ? await this.manager.list() : this.manager.list(filter);
10183
- const liveSessions = sessions.map((s) => this.toSessionInfo(s, this.sessionWorkdirs.get(s.id)));
11709
+ const liveSessions = sessions.map((session) => {
11710
+ const cached = this.manager?.get(session.id);
11711
+ return this.toSessionInfo(cached ?? session, this.sessionWorkdirs.get(session.id));
11712
+ });
10184
11713
  const terminalSessions = Array.from(this.terminalSessionStates.keys()).filter((sessionId) => !sessions.some((session) => session.id === sessionId)).map((sessionId) => this.toTerminalSessionInfo(sessionId)).filter((session) => session !== undefined);
10185
11714
  return [...liveSessions, ...terminalSessions];
10186
11715
  }
@@ -10198,9 +11727,9 @@ class PTYService {
10198
11727
  if (!this.manager)
10199
11728
  return false;
10200
11729
  if (this.usingBunWorker) {
10201
- return this.manager.isSessionLoading(sessionId);
11730
+ return this.manager.isSessionLoading?.(sessionId) ?? false;
10202
11731
  }
10203
- return this.manager.isSessionLoading(sessionId);
11732
+ return this.manager.isSessionLoading?.(sessionId) ?? false;
10204
11733
  }
10205
11734
  clearTranscriptCapture(sessionId) {
10206
11735
  const unsubscribe = this.transcriptUnsubscribers.get(sessionId);
@@ -10280,20 +11809,21 @@ class PTYService {
10280
11809
  }
10281
11810
  switch (event) {
10282
11811
  case "tool_running":
10283
- this.emitEvent(sessionId, "tool_running", data);
11812
+ this.emitEvent(sessionId, "tool_running", { ...data, source: "hook" });
10284
11813
  break;
10285
11814
  case "task_complete":
10286
- this.emitEvent(sessionId, "task_complete", data);
11815
+ this.emitEvent(sessionId, "task_complete", { ...data, source: "hook" });
10287
11816
  break;
10288
11817
  case "permission_approved":
10289
11818
  break;
10290
11819
  case "notification":
10291
- this.emitEvent(sessionId, "message", data);
11820
+ this.emitEvent(sessionId, "message", { ...data, source: "hook" });
10292
11821
  break;
10293
11822
  case "session_end":
10294
11823
  this.emitEvent(sessionId, "stopped", {
10295
11824
  ...data,
10296
- reason: "session_end"
11825
+ reason: "session_end",
11826
+ source: "hook"
10297
11827
  });
10298
11828
  break;
10299
11829
  default:
@@ -10330,6 +11860,7 @@ class PTYService {
10330
11860
  manager: this.manager,
10331
11861
  metricsTracker: this.metricsTracker,
10332
11862
  debugSnapshots: this.serviceConfig.debug === true,
11863
+ lastSentInput: typeof meta?.lastSentInput === "string" ? meta.lastSentInput : undefined,
10333
11864
  log: (msg) => this.log(msg),
10334
11865
  taskContext: {
10335
11866
  sessionId: taskCtx.sessionId,
@@ -10359,6 +11890,7 @@ class PTYService {
10359
11890
  manager: this.manager,
10360
11891
  metricsTracker: this.metricsTracker,
10361
11892
  debugSnapshots: this.serviceConfig.debug === true,
11893
+ lastSentInput: typeof meta?.lastSentInput === "string" ? meta.lastSentInput : undefined,
10362
11894
  log: (msg) => this.log(msg)
10363
11895
  });
10364
11896
  if (classification && meta?.coordinatorManaged && classification.suggestedResponse) {
@@ -10422,7 +11954,7 @@ class PTYService {
10422
11954
  ];
10423
11955
  try {
10424
11956
  if (existing.length === 0) {
10425
- await writeFile4(gitignorePath, `${entries.join(`
11957
+ await writeFile5(gitignorePath, `${entries.join(`
10426
11958
  `)}
10427
11959
  `, "utf-8");
10428
11960
  } else {
@@ -10445,6 +11977,14 @@ class PTYService {
10445
11977
  this.eventCallbacks.splice(idx, 1);
10446
11978
  };
10447
11979
  }
11980
+ onNormalizedSessionEvent(callback) {
11981
+ this.normalizedEventCallbacks.push(callback);
11982
+ return () => {
11983
+ const idx = this.normalizedEventCallbacks.indexOf(callback);
11984
+ if (idx !== -1)
11985
+ this.normalizedEventCallbacks.splice(idx, 1);
11986
+ };
11987
+ }
10448
11988
  registerAdapter(adapter) {
10449
11989
  if (!this.manager) {
10450
11990
  throw new Error("PTYService not initialized");
@@ -10491,6 +12031,12 @@ class PTYService {
10491
12031
  };
10492
12032
  }
10493
12033
  emitEvent(sessionId, event, data) {
12034
+ if (event === "blocked" && this.shouldSuppressBlockedEvent(sessionId, data)) {
12035
+ return;
12036
+ }
12037
+ if (event === "ready" || event === "task_complete" || event === "stopped" || event === "error") {
12038
+ this.clearCompletionReconcile(sessionId);
12039
+ }
10494
12040
  if (event === "stopped" || event === "error") {
10495
12041
  const liveSession = this.manager?.get(sessionId);
10496
12042
  const createdAt = liveSession?.startedAt instanceof Date ? liveSession.startedAt : liveSession?.startedAt ? new Date(liveSession.startedAt) : new Date;
@@ -10510,6 +12056,16 @@ class PTYService {
10510
12056
  this.log(`Event callback error: ${err}`);
10511
12057
  }
10512
12058
  }
12059
+ const normalized = normalizeCoordinatorEvent(sessionId, event, data);
12060
+ if (!normalized)
12061
+ return;
12062
+ for (const callback of this.normalizedEventCallbacks) {
12063
+ try {
12064
+ callback(normalized);
12065
+ } catch (err) {
12066
+ this.log(`Normalized event callback error: ${err}`);
12067
+ }
12068
+ }
10513
12069
  }
10514
12070
  getAgentMetrics() {
10515
12071
  return this.metricsTracker.getAll();
@@ -10525,17 +12081,146 @@ class PTYService {
10525
12081
  if (trackedSessionIds.size === 0) {
10526
12082
  return;
10527
12083
  }
10528
- const reason = info.signal ? `PTY worker exited unexpectedly (signal ${info.signal})` : `PTY worker exited unexpectedly (code ${info.code ?? "unknown"})`;
10529
- for (const sessionId of trackedSessionIds) {
10530
- const terminalState = this.terminalSessionStates.get(sessionId);
10531
- if (terminalState?.status === "stopped" || terminalState?.status === "error") {
10532
- continue;
10533
- }
10534
- this.emitEvent(sessionId, "error", {
10535
- message: reason,
10536
- workerExit: info
10537
- });
12084
+ const reason = info.signal ? `PTY worker exited unexpectedly (signal ${info.signal})` : `PTY worker exited unexpectedly (code ${info.code ?? "unknown"})`;
12085
+ for (const sessionId of trackedSessionIds) {
12086
+ const terminalState = this.terminalSessionStates.get(sessionId);
12087
+ if (terminalState?.status === "stopped" || terminalState?.status === "error") {
12088
+ continue;
12089
+ }
12090
+ this.emitEvent(sessionId, "error", {
12091
+ message: reason,
12092
+ workerExit: info,
12093
+ source: "pty_manager"
12094
+ });
12095
+ }
12096
+ }
12097
+ clearCompletionReconcile(sessionId) {
12098
+ const timer = this.completionReconcileTimers.get(sessionId);
12099
+ if (timer) {
12100
+ clearInterval(timer);
12101
+ this.completionReconcileTimers.delete(sessionId);
12102
+ }
12103
+ this.completionSignalSince.delete(sessionId);
12104
+ }
12105
+ scheduleCompletionReconcile(sessionId) {
12106
+ this.clearCompletionReconcile(sessionId);
12107
+ const timer = setInterval(() => {
12108
+ this.reconcileBusySessionFromOutput(sessionId);
12109
+ }, 1000);
12110
+ this.completionReconcileTimers.set(sessionId, timer);
12111
+ this.reconcileBusySessionFromOutput(sessionId);
12112
+ }
12113
+ isAdapterBackedAgentType(value) {
12114
+ return value === "claude" || value === "gemini" || value === "codex" || value === "aider" || value === "hermes";
12115
+ }
12116
+ shouldSuppressBlockedEvent(sessionId, data) {
12117
+ const payload = data;
12118
+ if (payload?.source !== "pty_manager") {
12119
+ return false;
12120
+ }
12121
+ const promptInfo = payload.promptInfo && typeof payload.promptInfo === "object" && !Array.isArray(payload.promptInfo) ? payload.promptInfo : undefined;
12122
+ if (!promptInfo) {
12123
+ return false;
12124
+ }
12125
+ const promptType = typeof promptInfo.type === "string" ? promptInfo.type.toLowerCase() : "";
12126
+ if (promptType && promptType !== "unknown") {
12127
+ return false;
12128
+ }
12129
+ const promptText = typeof promptInfo.prompt === "string" ? cleanForChat(promptInfo.prompt) : "";
12130
+ if (!promptText) {
12131
+ return false;
12132
+ }
12133
+ const compactPrompt = promptText.replace(/\s+/g, " ").trim();
12134
+ const hasWorkspacePath = /(\/private\/|\/var\/folders\/)/.test(compactPrompt);
12135
+ const looksLikeWorkingStatus = /working \(\d+s .*esc to interrupt\)/i.test(compactPrompt) || /messages to be submitted after next tool call/i.test(compactPrompt) || /find and fix a bug in @filename/i.test(compactPrompt) || /use \/skills to list available skills/i.test(compactPrompt);
12136
+ const looksLikeSpinnerTail = /\b\d+% left\b/i.test(compactPrompt) && hasWorkspacePath;
12137
+ const looksLikeSpinnerFragments2 = hasWorkspacePath && /(?:\bW Wo\b|• Wor|• Work|Worki|Workin|Working)/i.test(compactPrompt);
12138
+ if (!looksLikeWorkingStatus && !looksLikeSpinnerTail && !looksLikeSpinnerFragments2) {
12139
+ return false;
12140
+ }
12141
+ this.log(`Suppressing false blocked prompt noise for ${sessionId}: ${compactPrompt.slice(0, 160)}`);
12142
+ return true;
12143
+ }
12144
+ responseLooksMeaningful(response, rawOutput) {
12145
+ if (extractCompletionSummary(rawOutput).trim().length > 0) {
12146
+ return true;
12147
+ }
12148
+ const cleaned = response.trim();
12149
+ if (!cleaned)
12150
+ return false;
12151
+ const substantiveLines = cleaned.split(`
12152
+ `).map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !line.startsWith("› ") && !/^Work(?:i|in|ing)?(?:\s+\d+)?$/i.test(line) && !/^\d+% left\b/i.test(line) && !/context left/i.test(line) && !/esc to interrupt/i.test(line) && !/Use \/skills/i.test(line) && !/Messages to be submitted after next tool call/i.test(line));
12153
+ if (substantiveLines.some((line) => /\b(Added|Created|Creating|Updated|Wrote|Deleted|Renamed|Verified|Completed|Finished|Saved|Ran|LIVE_)\b/i.test(line))) {
12154
+ return true;
12155
+ }
12156
+ return false;
12157
+ }
12158
+ async reconcileBusySessionFromOutput(sessionId) {
12159
+ if (!this.manager) {
12160
+ this.clearCompletionReconcile(sessionId);
12161
+ return;
12162
+ }
12163
+ const liveSession = this.manager.get(sessionId);
12164
+ if (!liveSession) {
12165
+ this.clearCompletionReconcile(sessionId);
12166
+ return;
12167
+ }
12168
+ if (liveSession.status !== "busy") {
12169
+ this.clearCompletionReconcile(sessionId);
12170
+ return;
12171
+ }
12172
+ const agentType = this.sessionMetadata.get(sessionId)?.agentType;
12173
+ if (!this.isAdapterBackedAgentType(agentType)) {
12174
+ this.clearCompletionReconcile(sessionId);
12175
+ return;
12176
+ }
12177
+ const adapter = this.getAdapter(agentType);
12178
+ const rawOutput = await this.getSessionOutput(sessionId);
12179
+ if (!rawOutput.trim()) {
12180
+ this.completionSignalSince.delete(sessionId);
12181
+ return;
12182
+ }
12183
+ if (adapter.detectLoading?.(rawOutput)) {
12184
+ this.completionSignalSince.delete(sessionId);
12185
+ return;
12186
+ }
12187
+ if (adapter.detectLogin(rawOutput).required) {
12188
+ this.completionSignalSince.delete(sessionId);
12189
+ return;
12190
+ }
12191
+ if (adapter.detectBlockingPrompt(rawOutput).detected) {
12192
+ this.completionSignalSince.delete(sessionId);
12193
+ return;
12194
+ }
12195
+ const completionSignal = adapter.detectTaskComplete ? adapter.detectTaskComplete(rawOutput) : adapter.detectReady(rawOutput);
12196
+ if (!completionSignal) {
12197
+ this.completionSignalSince.delete(sessionId);
12198
+ return;
12199
+ }
12200
+ const previewResponse = this.taskResponseMarkers.has(sessionId) ? peekTaskResponse(sessionId, this.sessionOutputBuffers, this.taskResponseMarkers) : cleanForChat(rawOutput);
12201
+ if (!this.responseLooksMeaningful(previewResponse, rawOutput)) {
12202
+ this.completionSignalSince.delete(sessionId);
12203
+ return;
12204
+ }
12205
+ const firstSeenAt = this.completionSignalSince.get(sessionId);
12206
+ if (firstSeenAt === undefined) {
12207
+ this.completionSignalSince.set(sessionId, Date.now());
12208
+ return;
10538
12209
  }
12210
+ if (Date.now() - firstSeenAt < 2500) {
12211
+ return;
12212
+ }
12213
+ const response = this.taskResponseMarkers.has(sessionId) ? captureTaskResponse(sessionId, this.sessionOutputBuffers, this.taskResponseMarkers) : previewResponse;
12214
+ const durationMs = liveSession.startedAt ? Date.now() - new Date(liveSession.startedAt).getTime() : 0;
12215
+ liveSession.status = "ready";
12216
+ liveSession.lastActivityAt = new Date;
12217
+ this.metricsTracker.recordCompletion(agentType, "output-reconcile", durationMs);
12218
+ this.log(`Reconciled ${sessionId} from busy to task_complete using stable adapter output`);
12219
+ this.emitEvent(sessionId, "task_complete", {
12220
+ session: liveSession,
12221
+ response,
12222
+ source: "output_reconcile"
12223
+ });
10539
12224
  }
10540
12225
  }
10541
12226
 
@@ -10583,7 +12268,16 @@ var listAgentsAction = {
10583
12268
  const ptyService = runtime.getService("PTY_SERVICE");
10584
12269
  return ptyService != null;
10585
12270
  },
10586
- handler: async (runtime, _message, _state, _options, callback) => {
12271
+ handler: async (runtime, message, _state, _options, callback) => {
12272
+ const access = await requireTaskAgentAccess(runtime, message, "interact");
12273
+ if (!access.allowed) {
12274
+ if (callback) {
12275
+ await callback({
12276
+ text: access.reason
12277
+ });
12278
+ }
12279
+ return { success: false, error: "FORBIDDEN", text: access.reason };
12280
+ }
10587
12281
  const ptyService = runtime.getService("PTY_SERVICE");
10588
12282
  if (!ptyService) {
10589
12283
  if (callback) {
@@ -10682,169 +12376,6 @@ var listAgentsAction = {
10682
12376
  };
10683
12377
  var listTaskAgentsAction = listAgentsAction;
10684
12378
 
10685
- // src/services/task-policy.ts
10686
- var ROLE_RANK = {
10687
- GUEST: 0,
10688
- USER: 1,
10689
- ADMIN: 2,
10690
- OWNER: 3
10691
- };
10692
- var DEFAULT_POLICY = {
10693
- default: "GUEST",
10694
- connectors: {
10695
- discord: {
10696
- create: "ADMIN",
10697
- interact: "ADMIN"
10698
- }
10699
- }
10700
- };
10701
- function normalizeRole(value) {
10702
- const upper = typeof value === "string" ? value.trim().toUpperCase() : "";
10703
- switch (upper) {
10704
- case "OWNER":
10705
- case "ADMIN":
10706
- case "USER":
10707
- return upper;
10708
- default:
10709
- return "GUEST";
10710
- }
10711
- }
10712
- function normalizeConnectorPolicy(value) {
10713
- if (!value)
10714
- return {};
10715
- if (typeof value === "string") {
10716
- const role = normalizeRole(value);
10717
- return {
10718
- create: role,
10719
- interact: role
10720
- };
10721
- }
10722
- return {
10723
- ...value.create ? { create: normalizeRole(value.create) } : {},
10724
- ...value.interact ? { interact: normalizeRole(value.interact) } : {}
10725
- };
10726
- }
10727
- function parseTaskAgentPolicy(runtime) {
10728
- const configured = runtime.getSetting("TASK_AGENT_ROLE_POLICY") ?? runtime.getSetting("TASK_AGENT_CONNECTOR_ROLE_POLICY");
10729
- if (!configured) {
10730
- return DEFAULT_POLICY;
10731
- }
10732
- let parsed = configured;
10733
- if (typeof configured === "string") {
10734
- try {
10735
- parsed = JSON.parse(configured);
10736
- } catch {
10737
- return DEFAULT_POLICY;
10738
- }
10739
- }
10740
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
10741
- return DEFAULT_POLICY;
10742
- }
10743
- const record = parsed;
10744
- const connectors = record.connectors && typeof record.connectors === "object" && !Array.isArray(record.connectors) ? Object.fromEntries(Object.entries(record.connectors).map(([connector, value]) => [
10745
- connector,
10746
- normalizeConnectorPolicy(value)
10747
- ])) : DEFAULT_POLICY.connectors;
10748
- return {
10749
- default: normalizeConnectorPolicy(record.default ?? DEFAULT_POLICY.default),
10750
- connectors
10751
- };
10752
- }
10753
- function getConnectorFromBridgeMetadata(message) {
10754
- const metadata = message.content?.metadata;
10755
- if (!metadata || typeof metadata !== "object")
10756
- return null;
10757
- const bridgeSender = metadata.bridgeSender;
10758
- if (!bridgeSender || typeof bridgeSender !== "object")
10759
- return null;
10760
- const liveMetadata = bridgeSender.metadata;
10761
- if (!liveMetadata || typeof liveMetadata !== "object")
10762
- return null;
10763
- for (const [connector, value] of Object.entries(liveMetadata)) {
10764
- if (value && typeof value === "object") {
10765
- return connector;
10766
- }
10767
- }
10768
- return null;
10769
- }
10770
- async function resolveConnectorSource(runtime, message) {
10771
- const content = message.content;
10772
- const directSource = typeof content?.source === "string" && content.source !== "client_chat" ? content.source : null;
10773
- if (directSource)
10774
- return directSource;
10775
- const bridgeSource = getConnectorFromBridgeMetadata(message);
10776
- if (bridgeSource)
10777
- return bridgeSource;
10778
- try {
10779
- const room = await runtime.getRoom(message.roomId);
10780
- if (typeof room?.source === "string" && room.source.trim().length > 0) {
10781
- return room.source;
10782
- }
10783
- } catch {}
10784
- return null;
10785
- }
10786
- async function resolveSenderRole(runtime, message) {
10787
- try {
10788
- const rolesModuleSpecifier = "@miladyai/plugin-roles";
10789
- const rolesModule = await import(rolesModuleSpecifier);
10790
- if (typeof rolesModule.checkSenderRole !== "function") {
10791
- return null;
10792
- }
10793
- return await rolesModule.checkSenderRole(runtime, message);
10794
- } catch {
10795
- return null;
10796
- }
10797
- }
10798
- async function requireTaskAgentAccess(runtime, message, ability) {
10799
- if (message.entityId === runtime.agentId) {
10800
- return {
10801
- allowed: true,
10802
- connector: null,
10803
- requiredRole: "GUEST",
10804
- actualRole: "OWNER"
10805
- };
10806
- }
10807
- const connector = await resolveConnectorSource(runtime, message);
10808
- const policy = parseTaskAgentPolicy(runtime);
10809
- const connectorPolicy = connector ? normalizeConnectorPolicy(policy.connectors?.[connector]) : {};
10810
- const defaultPolicy = normalizeConnectorPolicy(policy.default);
10811
- const requiredRole = connectorPolicy[ability] ?? defaultPolicy[ability] ?? "GUEST";
10812
- if (requiredRole === "GUEST") {
10813
- return {
10814
- allowed: true,
10815
- connector,
10816
- requiredRole,
10817
- actualRole: "GUEST"
10818
- };
10819
- }
10820
- const roleCheck = await resolveSenderRole(runtime, message);
10821
- if (!roleCheck) {
10822
- return {
10823
- allowed: false,
10824
- connector,
10825
- requiredRole,
10826
- actualRole: "GUEST",
10827
- reason: connector === "discord" ? "Task-agent access in Discord requires a verified OWNER or ADMIN role." : "Task-agent access requires a verified role, but role context is unavailable."
10828
- };
10829
- }
10830
- const actualRole = normalizeRole(roleCheck.role);
10831
- if (ROLE_RANK[actualRole] < ROLE_RANK[requiredRole]) {
10832
- return {
10833
- allowed: false,
10834
- connector,
10835
- requiredRole,
10836
- actualRole,
10837
- reason: connector === "discord" ? `Task-agent access in Discord requires ${requiredRole} or higher. Current role: ${actualRole}.` : `Task-agent access requires ${requiredRole} or higher. Current role: ${actualRole}.`
10838
- };
10839
- }
10840
- return {
10841
- allowed: true,
10842
- connector,
10843
- requiredRole,
10844
- actualRole
10845
- };
10846
- }
10847
-
10848
12379
  // src/actions/task-thread-target.ts
10849
12380
  function stringValue(value) {
10850
12381
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
@@ -11400,17 +12931,17 @@ var taskHistoryAction = {
11400
12931
  // src/services/task-share.ts
11401
12932
  init_ansi_utils();
11402
12933
  import { readFileSync as readFileSync2 } from "node:fs";
11403
- import os4 from "node:os";
11404
- import path6 from "node:path";
12934
+ import os5 from "node:os";
12935
+ import path9 from "node:path";
11405
12936
  var URL_RE = /\bhttps?:\/\/[^\s<>"'`]+/gi;
11406
12937
  function resolveConfigPath() {
11407
12938
  const explicit = process.env.MILADY_CONFIG_PATH?.trim() || process.env.ELIZA_CONFIG_PATH?.trim();
11408
12939
  if (explicit)
11409
12940
  return explicit;
11410
- const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path6.join(os4.homedir(), ".milady");
12941
+ const stateDir = process.env.MILADY_STATE_DIR?.trim() || process.env.ELIZA_STATE_DIR?.trim() || path9.join(os5.homedir(), ".milady");
11411
12942
  const namespace = process.env.ELIZA_NAMESPACE?.trim();
11412
12943
  const filename = !namespace || namespace === "milady" ? "milady.json" : `${namespace}.json`;
11413
- return path6.join(stateDir, filename);
12944
+ return path9.join(stateDir, filename);
11414
12945
  }
11415
12946
  function readMiladyConfig() {
11416
12947
  try {
@@ -11758,6 +13289,13 @@ var manageIssuesAction = {
11758
13289
  return workspaceService != null;
11759
13290
  },
11760
13291
  handler: async (runtime, message, _state, options, callback) => {
13292
+ const access = await requireTaskAgentAccess(runtime, message, "interact");
13293
+ if (!access.allowed) {
13294
+ if (callback) {
13295
+ await callback({ text: access.reason });
13296
+ }
13297
+ return { success: false, error: "FORBIDDEN", text: access.reason };
13298
+ }
11761
13299
  const workspaceService = runtime.getService("CODING_WORKSPACE_SERVICE");
11762
13300
  if (!workspaceService) {
11763
13301
  if (callback) {
@@ -12140,6 +13678,15 @@ var provisionWorkspaceAction = {
12140
13678
  return workspaceService != null;
12141
13679
  },
12142
13680
  handler: async (runtime, message, state, _options, callback) => {
13681
+ const access = await requireTaskAgentAccess(runtime, message, "create");
13682
+ if (!access.allowed) {
13683
+ if (callback) {
13684
+ await callback({
13685
+ text: access.reason
13686
+ });
13687
+ }
13688
+ return { success: false, error: "FORBIDDEN", text: access.reason };
13689
+ }
12143
13690
  const workspaceService = runtime.getService("CODING_WORKSPACE_SERVICE");
12144
13691
  if (!workspaceService) {
12145
13692
  if (callback) {
@@ -12452,6 +13999,7 @@ var sendToAgentAction = {
12452
13999
  ...existingTask?.repo ? { repo: existingTask.repo } : {},
12453
14000
  metadata: session.metadata && typeof session.metadata === "object" && !Array.isArray(session.metadata) ? session.metadata : undefined
12454
14001
  });
14002
+ await coordinator.setTaskDelivered(sessionId);
12455
14003
  }
12456
14004
  }
12457
14005
  if (callback) {
@@ -12518,8 +14066,8 @@ var sendToAgentAction = {
12518
14066
  var sendToTaskAgentAction = sendToAgentAction;
12519
14067
 
12520
14068
  // src/actions/spawn-agent.ts
12521
- import * as os5 from "node:os";
12522
- import * as path7 from "node:path";
14069
+ import * as os6 from "node:os";
14070
+ import * as path10 from "node:path";
12523
14071
  import {
12524
14072
  logger as logger5
12525
14073
  } from "@elizaos/core";
@@ -12576,6 +14124,15 @@ var spawnAgentAction = {
12576
14124
  return true;
12577
14125
  },
12578
14126
  handler: async (runtime, message, state, options, callback) => {
14127
+ const access = await requireTaskAgentAccess(runtime, message, "create");
14128
+ if (!access.allowed) {
14129
+ if (callback) {
14130
+ await callback({
14131
+ text: access.reason
14132
+ });
14133
+ }
14134
+ return { success: false, error: "FORBIDDEN", text: access.reason };
14135
+ }
12579
14136
  const ptyService = runtime.getService("PTY_SERVICE");
12580
14137
  if (!ptyService) {
12581
14138
  if (callback) {
@@ -12588,9 +14145,12 @@ var spawnAgentAction = {
12588
14145
  const params = options?.parameters;
12589
14146
  const content = message.content;
12590
14147
  const explicitRawType = params?.agentType ?? content.agentType;
12591
- const rawAgentType = explicitRawType ?? await ptyService.resolveAgentType();
12592
- const agentType = normalizeAgentType(rawAgentType);
12593
14148
  const task = params?.task ?? content.task;
14149
+ const rawAgentType = explicitRawType ?? await ptyService.resolveAgentType({
14150
+ task,
14151
+ workdir: (params?.workdir ?? content.workdir) || undefined
14152
+ });
14153
+ const agentType = normalizeAgentType(rawAgentType);
12594
14154
  const piRequested = isPiAgentType(rawAgentType);
12595
14155
  const initialTask = piRequested ? toPiCommand(task) : task;
12596
14156
  let workdir = params?.workdir ?? content.workdir;
@@ -12614,13 +14174,13 @@ var spawnAgentAction = {
12614
14174
  }
12615
14175
  return { success: false, error: "NO_WORKSPACE" };
12616
14176
  }
12617
- const resolvedWorkdir = path7.resolve(workdir);
12618
- const workspaceBaseDir = path7.join(os5.homedir(), ".milady", "workspaces");
14177
+ const resolvedWorkdir = path10.resolve(workdir);
14178
+ const workspaceBaseDir = path10.join(os6.homedir(), ".milady", "workspaces");
12619
14179
  const allowedPrefixes = [
12620
- path7.resolve(workspaceBaseDir),
12621
- path7.resolve(process.cwd())
14180
+ path10.resolve(workspaceBaseDir),
14181
+ path10.resolve(process.cwd())
12622
14182
  ];
12623
- const isAllowed = allowedPrefixes.some((prefix) => resolvedWorkdir.startsWith(prefix + path7.sep) || resolvedWorkdir === prefix);
14183
+ const isAllowed = allowedPrefixes.some((prefix) => resolvedWorkdir.startsWith(prefix + path10.sep) || resolvedWorkdir === prefix);
12624
14184
  if (!isAllowed) {
12625
14185
  if (callback) {
12626
14186
  await callback({
@@ -12642,6 +14202,8 @@ var spawnAgentAction = {
12642
14202
  customCredentials[key] = val;
12643
14203
  }
12644
14204
  }
14205
+ const rawAnthropicKey = runtime.getSetting("ANTHROPIC_API_KEY");
14206
+ customCredentials = sanitizeCustomCredentials(customCredentials, isAnthropicOAuthToken(rawAnthropicKey) ? [rawAnthropicKey] : []);
12645
14207
  const llmProvider = readConfigEnvKey("PARALLAX_LLM_PROVIDER") || "subscription";
12646
14208
  let credentials;
12647
14209
  try {
@@ -12813,7 +14375,7 @@ import {
12813
14375
  // src/actions/coding-task-handlers.ts
12814
14376
  import {
12815
14377
  logger as logger7,
12816
- ModelType as ModelType7
14378
+ ModelType as ModelType8
12817
14379
  } from "@elizaos/core";
12818
14380
  // src/services/trajectory-feedback.ts
12819
14381
  import { logger as elizaLogger } from "@elizaos/core";
@@ -12828,13 +14390,13 @@ function withTimeout2(promise, ms) {
12828
14390
  function getTrajectoryLogger(runtime) {
12829
14391
  const runtimeAny = runtime;
12830
14392
  if (typeof runtimeAny.getService === "function") {
12831
- const svc = runtimeAny.getService("trajectory_logger");
14393
+ const svc = runtimeAny.getService("trajectories");
12832
14394
  if (svc && typeof svc === "object" && hasListMethod(svc)) {
12833
14395
  return svc;
12834
14396
  }
12835
14397
  }
12836
14398
  if (typeof runtimeAny.getServicesByType === "function") {
12837
- const services = runtimeAny.getServicesByType("trajectory_logger");
14399
+ const services = runtimeAny.getServicesByType("trajectories");
12838
14400
  if (Array.isArray(services)) {
12839
14401
  for (const svc of services) {
12840
14402
  if (svc && typeof svc === "object" && hasListMethod(svc)) {
@@ -13000,9 +14562,9 @@ function formatAge(timestamp) {
13000
14562
 
13001
14563
  // src/actions/coding-task-helpers.ts
13002
14564
  import { randomUUID } from "node:crypto";
13003
- import * as fs3 from "node:fs";
13004
- import * as os6 from "node:os";
13005
- import * as path8 from "node:path";
14565
+ import * as fs4 from "node:fs";
14566
+ import * as os7 from "node:os";
14567
+ import * as path11 from "node:path";
13006
14568
  import {
13007
14569
  logger as logger6
13008
14570
  } from "@elizaos/core";
@@ -13010,29 +14572,29 @@ function sanitizeDirName(label) {
13010
14572
  return label.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "scratch";
13011
14573
  }
13012
14574
  function resolveNonColliding(baseDir, name2) {
13013
- let candidate = path8.join(baseDir, name2);
13014
- if (!fs3.existsSync(candidate))
14575
+ let candidate = path11.join(baseDir, name2);
14576
+ if (!fs4.existsSync(candidate))
13015
14577
  return candidate;
13016
14578
  for (let i = 2;i < 100; i++) {
13017
- candidate = path8.join(baseDir, `${name2}-${i}`);
13018
- if (!fs3.existsSync(candidate))
14579
+ candidate = path11.join(baseDir, `${name2}-${i}`);
14580
+ if (!fs4.existsSync(candidate))
13019
14581
  return candidate;
13020
14582
  }
13021
- return path8.join(baseDir, `${name2}-${randomUUID().slice(0, 8)}`);
14583
+ return path11.join(baseDir, `${name2}-${randomUUID().slice(0, 8)}`);
13022
14584
  }
13023
14585
  function createScratchDir(runtime, label) {
13024
14586
  const codingDir = runtime?.getSetting("PARALLAX_CODING_DIRECTORY") ?? readConfigEnvKey("PARALLAX_CODING_DIRECTORY") ?? process.env.PARALLAX_CODING_DIRECTORY;
13025
14587
  if (codingDir?.trim()) {
13026
- const resolved = codingDir.startsWith("~") ? path8.join(os6.homedir(), codingDir.slice(1)) : path8.resolve(codingDir);
14588
+ const resolved = codingDir.startsWith("~") ? path11.join(os7.homedir(), codingDir.slice(1)) : path11.resolve(codingDir);
13027
14589
  const dirName = label ? sanitizeDirName(label) : `scratch-${randomUUID().slice(0, 8)}`;
13028
14590
  const scratchDir2 = resolveNonColliding(resolved, dirName);
13029
- fs3.mkdirSync(scratchDir2, { recursive: true });
14591
+ fs4.mkdirSync(scratchDir2, { recursive: true });
13030
14592
  return scratchDir2;
13031
14593
  }
13032
- const baseDir = path8.join(os6.homedir(), ".milady", "workspaces");
14594
+ const baseDir = path11.join(os7.homedir(), ".milady", "workspaces");
13033
14595
  const scratchId = randomUUID();
13034
- const scratchDir = path8.join(baseDir, scratchId);
13035
- fs3.mkdirSync(scratchDir, { recursive: true });
14596
+ const scratchDir = path11.join(baseDir, scratchId);
14597
+ fs4.mkdirSync(scratchDir, { recursive: true });
13036
14598
  return scratchDir;
13037
14599
  }
13038
14600
  function generateLabel(repo, task) {
@@ -13071,9 +14633,6 @@ function registerSessionEvents(ptyService, runtime, sessionId, label, scratchDir
13071
14633
  ${preview}` : `Agent "${label}" completed the task.`
13072
14634
  });
13073
14635
  }
13074
- ptyService.stopSession(sessionId, true).catch((err) => {
13075
- logger6.warn(`[START_CODING_TASK] Failed to stop session for "${label}" after task complete: ${err}`);
13076
- });
13077
14636
  }
13078
14637
  if (event === "error" && callback) {
13079
14638
  callback({
@@ -13171,7 +14730,7 @@ ${taskList}
13171
14730
 
13172
14731
  ` + `Output ONLY the bullet points, no preamble.`;
13173
14732
  try {
13174
- const result = await withTrajectoryContext(runtime, { source: "orchestrator", decisionType: "swarm-context-generation" }, () => runtime.useModel(ModelType7.TEXT_SMALL, {
14733
+ const result = await withTrajectoryContext(runtime, { source: "orchestrator", decisionType: "swarm-context-generation" }, () => runtime.useModel(ModelType8.TEXT_SMALL, {
13175
14734
  prompt,
13176
14735
  temperature: 0.3,
13177
14736
  stream: false
@@ -13195,6 +14754,7 @@ async function handleMultiAgent(ctx, agentsParam) {
13195
14754
  repo,
13196
14755
  defaultAgentType,
13197
14756
  rawAgentType,
14757
+ agentTypeExplicit,
13198
14758
  memoryContent,
13199
14759
  approvalPreset,
13200
14760
  explicitLabel
@@ -13261,15 +14821,17 @@ async function handleMultiAgent(ctx, agentsParam) {
13261
14821
  } : { subtasks: cleanSubtasks },
13262
14822
  metadata: evalMetadata.metadata
13263
14823
  }) : null;
13264
- const plannedAgents = agentSpecs.map((spec, i) => {
14824
+ const plannedAgents = await Promise.all(agentSpecs.map(async (spec, i) => {
13265
14825
  let specAgentType = defaultAgentType;
13266
14826
  let specPiRequested = isPiAgentType(rawAgentType);
13267
14827
  let specRequestedType = rawAgentType;
13268
14828
  let specTask = spec;
14829
+ let hasExplicitPrefix = false;
13269
14830
  const colonIdx = spec.indexOf(":");
13270
14831
  if (ctx.agentSelectionStrategy !== "fixed" && colonIdx > 0 && colonIdx < 20) {
13271
14832
  const prefix = spec.slice(0, colonIdx).trim().toLowerCase();
13272
14833
  if (KNOWN_AGENT_PREFIXES.includes(prefix)) {
14834
+ hasExplicitPrefix = true;
13273
14835
  specRequestedType = prefix;
13274
14836
  specPiRequested = isPiAgentType(prefix);
13275
14837
  specAgentType = normalizeAgentType(prefix);
@@ -13279,6 +14841,15 @@ async function handleMultiAgent(ctx, agentsParam) {
13279
14841
  specTask = stripAgentPrefix(spec);
13280
14842
  }
13281
14843
  const specLabel = explicitLabel ? `${explicitLabel}-${i + 1}` : generateLabel(repo, specTask);
14844
+ if (!agentTypeExplicit && !hasExplicitPrefix) {
14845
+ specRequestedType = await ptyService.resolveAgentType({
14846
+ task: specTask,
14847
+ repo,
14848
+ subtaskCount: agentSpecs.length
14849
+ });
14850
+ specPiRequested = isPiAgentType(specRequestedType);
14851
+ specAgentType = normalizeAgentType(specRequestedType);
14852
+ }
13282
14853
  return {
13283
14854
  specAgentType,
13284
14855
  specPiRequested,
@@ -13286,7 +14857,7 @@ async function handleMultiAgent(ctx, agentsParam) {
13286
14857
  specTask,
13287
14858
  specLabel
13288
14859
  };
13289
- });
14860
+ }));
13290
14861
  const graphPlan = coordinator && taskThread ? await coordinator.planTaskThreadGraph({
13291
14862
  threadId: taskThread.id,
13292
14863
  title: threadTitle,
@@ -13348,6 +14919,8 @@ ${swarmContext}
13348
14919
  const agentMemory = [memoryContent, swarmMemory, pastExperienceBlock].filter(Boolean).join(`
13349
14920
 
13350
14921
  `) || undefined;
14922
+ const coordinatorManagedSession = !!coordinator && llmProvider === "subscription";
14923
+ const useDirectCallbackResponses = Boolean(callback);
13351
14924
  const session = await ptyService.spawnSession({
13352
14925
  name: `coding-${Date.now()}-${i}`,
13353
14926
  agentType: specAgentType,
@@ -13357,7 +14930,7 @@ ${swarmContext}
13357
14930
  credentials,
13358
14931
  approvalPreset: approvalPreset ?? ptyService.defaultApprovalPreset,
13359
14932
  customCredentials,
13360
- ...coordinator && llmProvider === "subscription" ? { skipAdapterAutoResponse: true } : {},
14933
+ ...coordinatorManagedSession ? { skipAdapterAutoResponse: true } : {},
13361
14934
  metadata: {
13362
14935
  threadId: taskThread?.id,
13363
14936
  taskNodeId,
@@ -13366,12 +14939,15 @@ ${swarmContext}
13366
14939
  userId: message.userId,
13367
14940
  workspaceId,
13368
14941
  label: specLabel,
13369
- multiAgentIndex: i
14942
+ multiAgentIndex: i,
14943
+ roomId: message.roomId,
14944
+ worldId: message.worldId,
14945
+ source: message.content?.source
13370
14946
  }
13371
14947
  });
13372
14948
  const isScratch = !repo;
13373
14949
  const scratchDir = isScratch ? workdir : null;
13374
- registerSessionEvents(ptyService, runtime, session.id, specLabel, scratchDir, callback, !!coordinator);
14950
+ registerSessionEvents(ptyService, runtime, session.id, specLabel, scratchDir, callback, coordinatorManagedSession && !useDirectCallbackResponses);
13375
14951
  if (coordinator && specTask) {
13376
14952
  await coordinator.registerTask(session.id, {
13377
14953
  threadId: taskThread?.id ?? session.id,
@@ -13506,8 +15082,6 @@ var startCodingTaskAction = {
13506
15082
  const params = options?.parameters;
13507
15083
  const content = message.content;
13508
15084
  const explicitRawType = params?.agentType ?? content.agentType;
13509
- const rawAgentType = explicitRawType ?? await ptyService.resolveAgentType();
13510
- const defaultAgentType = normalizeAgentType(rawAgentType);
13511
15085
  const memoryContent = params?.memoryContent ?? content.memoryContent;
13512
15086
  const approvalPreset = params?.approvalPreset ?? content.approvalPreset;
13513
15087
  let repo = params?.repo ?? content.repo;
@@ -13534,6 +15108,13 @@ var startCodingTaskAction = {
13534
15108
  }
13535
15109
  }
13536
15110
  }
15111
+ const selectionTask = params?.task ?? content.task ?? content.text;
15112
+ const rawAgentType = explicitRawType ?? await ptyService.resolveAgentType({
15113
+ task: selectionTask,
15114
+ repo,
15115
+ subtaskCount: typeof params?.agents === "string" || typeof content.agents === "string" ? (params?.agents ?? content.agents).split("|").map((value) => value.trim()).filter(Boolean).length || 1 : 1
15116
+ });
15117
+ const defaultAgentType = normalizeAgentType(rawAgentType);
13537
15118
  const customCredentialKeys = runtime.getSetting("CUSTOM_CREDENTIAL_KEYS");
13538
15119
  let customCredentials;
13539
15120
  if (customCredentialKeys) {
@@ -13544,6 +15125,8 @@ var startCodingTaskAction = {
13544
15125
  customCredentials[key] = val;
13545
15126
  }
13546
15127
  }
15128
+ const rawAnthropicKey = runtime.getSetting("ANTHROPIC_API_KEY");
15129
+ customCredentials = sanitizeCustomCredentials(customCredentials, isAnthropicOAuthToken(rawAnthropicKey) ? [rawAnthropicKey] : []);
13547
15130
  let credentials;
13548
15131
  try {
13549
15132
  credentials = buildAgentCredentials(runtime);
@@ -13568,6 +15151,7 @@ var startCodingTaskAction = {
13568
15151
  repo,
13569
15152
  defaultAgentType,
13570
15153
  rawAgentType,
15154
+ agentTypeExplicit: Boolean(explicitRawType),
13571
15155
  agentSelectionStrategy: ptyService.agentSelectionStrategy,
13572
15156
  memoryContent,
13573
15157
  approvalPreset,
@@ -13692,6 +15276,15 @@ var stopAgentAction = {
13692
15276
  }
13693
15277
  },
13694
15278
  handler: async (runtime, message, state, options, callback) => {
15279
+ const access = await requireTaskAgentAccess(runtime, message, "interact");
15280
+ if (!access.allowed) {
15281
+ if (callback) {
15282
+ await callback({
15283
+ text: access.reason
15284
+ });
15285
+ }
15286
+ return { success: false, error: "FORBIDDEN", text: access.reason };
15287
+ }
13695
15288
  const ptyService = runtime.getService("PTY_SERVICE");
13696
15289
  if (!ptyService) {
13697
15290
  if (callback) {
@@ -14055,9 +15648,9 @@ var activeWorkspaceContextProvider = {
14055
15648
  };
14056
15649
 
14057
15650
  // src/services/workspace-service.ts
14058
- import * as os8 from "node:os";
14059
- import * as path10 from "node:path";
14060
- import * as fs5 from "node:fs/promises";
15651
+ import * as os9 from "node:os";
15652
+ import * as path13 from "node:path";
15653
+ import * as fs6 from "node:fs/promises";
14061
15654
  import {
14062
15655
  CredentialService,
14063
15656
  GitHubPatClient as GitHubPatClient2,
@@ -14270,23 +15863,23 @@ async function createPR(workspaceService, workspace, workspaceId, options, log)
14270
15863
  }
14271
15864
 
14272
15865
  // src/services/workspace-lifecycle.ts
14273
- import * as fs4 from "node:fs";
14274
- import * as os7 from "node:os";
14275
- import * as path9 from "node:path";
15866
+ import * as fs5 from "node:fs";
15867
+ import * as os8 from "node:os";
15868
+ import * as path12 from "node:path";
14276
15869
  async function removeScratchDir(dirPath, baseDir, log, allowedDirs) {
14277
- const resolved = path9.resolve(dirPath);
14278
- const expandTilde = (p) => p.startsWith("~") ? path9.join(os7.homedir(), p.slice(1)) : p;
15870
+ const resolved = path12.resolve(dirPath);
15871
+ const expandTilde = (p) => p.startsWith("~") ? path12.join(os8.homedir(), p.slice(1)) : p;
14279
15872
  const allAllowed = [baseDir, ...allowedDirs ?? []];
14280
15873
  const isAllowed = allAllowed.some((dir) => {
14281
- const resolvedDir = path9.resolve(expandTilde(dir)) + path9.sep;
14282
- return resolved.startsWith(resolvedDir) || resolved === path9.resolve(expandTilde(dir));
15874
+ const resolvedDir = path12.resolve(expandTilde(dir)) + path12.sep;
15875
+ return resolved.startsWith(resolvedDir) || resolved === path12.resolve(expandTilde(dir));
14283
15876
  });
14284
15877
  if (!isAllowed) {
14285
15878
  console.warn(`[CodingWorkspaceService] Refusing to remove dir outside allowed paths: ${resolved}`);
14286
15879
  return;
14287
15880
  }
14288
15881
  try {
14289
- await fs4.promises.rm(resolved, { recursive: true, force: true });
15882
+ await fs5.promises.rm(resolved, { recursive: true, force: true });
14290
15883
  log(`Removed scratch dir ${resolved}`);
14291
15884
  } catch (err) {
14292
15885
  console.warn(`[CodingWorkspaceService] Failed to remove scratch dir ${resolved}:`, err);
@@ -14299,7 +15892,7 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
14299
15892
  }
14300
15893
  let entries;
14301
15894
  try {
14302
- entries = await fs4.promises.readdir(baseDir, { withFileTypes: true });
15895
+ entries = await fs5.promises.readdir(baseDir, { withFileTypes: true });
14303
15896
  } catch {
14304
15897
  return;
14305
15898
  }
@@ -14313,12 +15906,12 @@ async function gcOrphanedWorkspaces(baseDir, workspaceTtlMs, trackedWorkspaceIds
14313
15906
  skipped++;
14314
15907
  continue;
14315
15908
  }
14316
- const dirPath = path9.join(baseDir, entry.name);
15909
+ const dirPath = path12.join(baseDir, entry.name);
14317
15910
  try {
14318
- const stat3 = await fs4.promises.stat(dirPath);
15911
+ const stat3 = await fs5.promises.stat(dirPath);
14319
15912
  const age = now - stat3.mtimeMs;
14320
15913
  if (age > workspaceTtlMs) {
14321
- await fs4.promises.rm(dirPath, { recursive: true, force: true });
15914
+ await fs5.promises.rm(dirPath, { recursive: true, force: true });
14322
15915
  removed++;
14323
15916
  } else {
14324
15917
  skipped++;
@@ -14353,7 +15946,7 @@ class CodingWorkspaceService {
14353
15946
  constructor(runtime, config = {}) {
14354
15947
  this.runtime = runtime;
14355
15948
  this.serviceConfig = {
14356
- baseDir: config.baseDir ?? path10.join(os8.homedir(), ".milady", "workspaces"),
15949
+ baseDir: config.baseDir ?? path13.join(os9.homedir(), ".milady", "workspaces"),
14357
15950
  branchPrefix: config.branchPrefix ?? "milady",
14358
15951
  debug: config.debug ?? false,
14359
15952
  workspaceTtlMs: config.workspaceTtlMs ?? 24 * 60 * 60 * 1000
@@ -14610,7 +16203,7 @@ class CodingWorkspaceService {
14610
16203
  }
14611
16204
  async removeScratchDir(dirPath) {
14612
16205
  const rawCodingDir = this.runtime.getSetting("PARALLAX_CODING_DIRECTORY") ?? this.readConfigEnvKey("PARALLAX_CODING_DIRECTORY") ?? process.env.PARALLAX_CODING_DIRECTORY;
14613
- const codingDir = rawCodingDir?.trim() ? rawCodingDir.trim().startsWith("~") ? path10.join(os8.homedir(), rawCodingDir.trim().slice(1)) : path10.resolve(rawCodingDir.trim()) : undefined;
16206
+ const codingDir = rawCodingDir?.trim() ? rawCodingDir.trim().startsWith("~") ? path13.join(os9.homedir(), rawCodingDir.trim().slice(1)) : path13.resolve(rawCodingDir.trim()) : undefined;
14614
16207
  const allowedDirs = codingDir ? [codingDir] : undefined;
14615
16208
  return removeScratchDir(dirPath, this.serviceConfig.baseDir, (msg) => this.log(msg), allowedDirs);
14616
16209
  }
@@ -14687,14 +16280,14 @@ class CodingWorkspaceService {
14687
16280
  const suggestedName = this.sanitizeWorkspaceName(name2 || record.label);
14688
16281
  const targetPath = await this.allocatePromotedPath(baseDir, suggestedName);
14689
16282
  try {
14690
- await fs5.rename(record.path, targetPath);
16283
+ await fs6.rename(record.path, targetPath);
14691
16284
  } catch (error) {
14692
16285
  const isExdev = typeof error === "object" && error !== null && "code" in error && error.code === "EXDEV";
14693
16286
  if (!isExdev)
14694
16287
  throw error;
14695
- await fs5.cp(record.path, targetPath, { recursive: true });
14696
- await fs5.access(targetPath);
14697
- await fs5.rm(record.path, { recursive: true, force: true });
16288
+ await fs6.cp(record.path, targetPath, { recursive: true });
16289
+ await fs6.access(targetPath);
16290
+ await fs6.rm(record.path, { recursive: true, force: true });
14698
16291
  }
14699
16292
  const next = {
14700
16293
  ...record,
@@ -14775,15 +16368,15 @@ class CodingWorkspaceService {
14775
16368
  return compact || `scratch-${Date.now().toString(36)}`;
14776
16369
  }
14777
16370
  async allocatePromotedPath(baseDir, baseName) {
14778
- const baseResolved = path10.resolve(baseDir);
16371
+ const baseResolved = path13.resolve(baseDir);
14779
16372
  for (let i = 0;i < 1000; i++) {
14780
16373
  const candidateName = i === 0 ? baseName : `${baseName}-${i}`;
14781
- const candidate = path10.resolve(baseResolved, candidateName);
14782
- if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path10.sep}`)) {
16374
+ const candidate = path13.resolve(baseResolved, candidateName);
16375
+ if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path13.sep}`)) {
14783
16376
  continue;
14784
16377
  }
14785
16378
  try {
14786
- await fs5.access(candidate);
16379
+ await fs6.access(candidate);
14787
16380
  } catch {
14788
16381
  return candidate;
14789
16382
  }
@@ -14793,9 +16386,9 @@ class CodingWorkspaceService {
14793
16386
  }
14794
16387
  // src/api/agent-routes.ts
14795
16388
  import { access as access2, readFile as readFile4, realpath, rm as rm2 } from "node:fs/promises";
14796
- import { createHash as createHash2 } from "node:crypto";
14797
- import * as os9 from "node:os";
14798
- import * as path11 from "node:path";
16389
+ import { createHash as createHash3 } from "node:crypto";
16390
+ import * as os10 from "node:os";
16391
+ import * as path14 from "node:path";
14799
16392
  import { execFile as execFile2 } from "node:child_process";
14800
16393
  import { promisify as promisify2 } from "node:util";
14801
16394
  var execFileAsync2 = promisify2(execFile2);
@@ -14807,23 +16400,23 @@ function shouldAutoPreflight() {
14807
16400
  return false;
14808
16401
  }
14809
16402
  function isPathInside(parent, candidate) {
14810
- return candidate === parent || candidate.startsWith(`${parent}${path11.sep}`);
16403
+ return candidate === parent || candidate.startsWith(`${parent}${path14.sep}`);
14811
16404
  }
14812
16405
  async function resolveSafeVenvPath(workdir, venvDirRaw) {
14813
16406
  const venvDir = venvDirRaw.trim();
14814
16407
  if (!venvDir) {
14815
16408
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be non-empty");
14816
16409
  }
14817
- if (path11.isAbsolute(venvDir)) {
16410
+ if (path14.isAbsolute(venvDir)) {
14818
16411
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be relative to workdir");
14819
16412
  }
14820
- const normalized = path11.normalize(venvDir);
14821
- if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path11.sep}`)) {
16413
+ const normalized = path14.normalize(venvDir);
16414
+ if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path14.sep}`)) {
14822
16415
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must stay within workdir");
14823
16416
  }
14824
- const workdirResolved = path11.resolve(workdir);
16417
+ const workdirResolved = path14.resolve(workdir);
14825
16418
  const workdirReal = await realpath(workdirResolved);
14826
- const resolved = path11.resolve(workdirReal, normalized);
16419
+ const resolved = path14.resolve(workdirReal, normalized);
14827
16420
  if (!isPathInside(workdirReal, resolved)) {
14828
16421
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV resolves outside workdir");
14829
16422
  }
@@ -14839,7 +16432,7 @@ async function resolveSafeVenvPath(workdir, venvDirRaw) {
14839
16432
  const maybeErr = err;
14840
16433
  if (maybeErr?.code !== "ENOENT")
14841
16434
  throw err;
14842
- const parentReal = await realpath(path11.dirname(resolved));
16435
+ const parentReal = await realpath(path14.dirname(resolved));
14843
16436
  if (!isPathInside(workdirReal, parentReal)) {
14844
16437
  throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV parent resolves outside workdir");
14845
16438
  }
@@ -14855,10 +16448,10 @@ async function fileExists(filePath) {
14855
16448
  }
14856
16449
  }
14857
16450
  async function resolveRequirementsPath(workdir) {
14858
- const workdirReal = await realpath(path11.resolve(workdir));
16451
+ const workdirReal = await realpath(path14.resolve(workdir));
14859
16452
  const candidates = [
14860
- path11.join(workdir, "apps", "api", "requirements.txt"),
14861
- path11.join(workdir, "requirements.txt")
16453
+ path14.join(workdir, "apps", "api", "requirements.txt"),
16454
+ path14.join(workdir, "requirements.txt")
14862
16455
  ];
14863
16456
  for (const candidate of candidates) {
14864
16457
  if (!await fileExists(candidate))
@@ -14873,7 +16466,7 @@ async function resolveRequirementsPath(workdir) {
14873
16466
  }
14874
16467
  async function fingerprintRequirementsFile(requirementsPath) {
14875
16468
  const file = await readFile4(requirementsPath);
14876
- return createHash2("sha256").update(file).digest("hex");
16469
+ return createHash3("sha256").update(file).digest("hex");
14877
16470
  }
14878
16471
  async function runBenchmarkPreflight(workdir) {
14879
16472
  if (!shouldAutoPreflight())
@@ -14885,7 +16478,7 @@ async function runBenchmarkPreflight(workdir) {
14885
16478
  const mode = process.env.PARALLAX_BENCHMARK_PREFLIGHT_MODE?.toLowerCase() === "warm" ? "warm" : "cold";
14886
16479
  const venvDir = process.env.PARALLAX_BENCHMARK_PREFLIGHT_VENV || ".benchmark-venv";
14887
16480
  const venvPath = await resolveSafeVenvPath(workdir, venvDir);
14888
- const pythonInVenv = path11.join(venvPath, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
16481
+ const pythonInVenv = path14.join(venvPath, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
14889
16482
  const key = `${workdir}::${mode}::${venvPath}::${requirementsFingerprint}`;
14890
16483
  if (PREFLIGHT_DONE.has(key)) {
14891
16484
  if (await fileExists(pythonInVenv))
@@ -15128,21 +16721,21 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
15128
16721
  customCredentials,
15129
16722
  metadata
15130
16723
  } = body;
15131
- const workspaceBaseDir = path11.join(os9.homedir(), ".milady", "workspaces");
15132
- const workspaceBaseDirResolved = path11.resolve(workspaceBaseDir);
15133
- const cwdResolved = path11.resolve(process.cwd());
16724
+ const workspaceBaseDir = path14.join(os10.homedir(), ".milady", "workspaces");
16725
+ const workspaceBaseDirResolved = path14.resolve(workspaceBaseDir);
16726
+ const cwdResolved = path14.resolve(process.cwd());
15134
16727
  const workspaceBaseDirReal = await realpath(workspaceBaseDirResolved).catch(() => workspaceBaseDirResolved);
15135
16728
  const cwdReal = await realpath(cwdResolved).catch(() => cwdResolved);
15136
16729
  const allowedPrefixes = [workspaceBaseDirReal, cwdReal];
15137
16730
  let workdir = rawWorkdir;
15138
16731
  if (workdir) {
15139
- const resolved = path11.resolve(workdir);
16732
+ const resolved = path14.resolve(workdir);
15140
16733
  const resolvedReal = await realpath(resolved).catch(() => null);
15141
16734
  if (!resolvedReal) {
15142
16735
  sendError(res, "workdir must exist", 403);
15143
16736
  return true;
15144
16737
  }
15145
- const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 + path11.sep));
16738
+ const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 + path14.sep));
15146
16739
  if (!isAllowed) {
15147
16740
  sendError(res, "workdir must be within workspace base directory or cwd", 403);
15148
16741
  return true;
@@ -15162,12 +16755,15 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
15162
16755
  console.warn(`[coding-agent] benchmark preflight failed for ${workdir}:`, preflightError);
15163
16756
  }
15164
16757
  }
15165
- const credentials = {
15166
- anthropicKey: ctx.runtime.getSetting("ANTHROPIC_API_KEY"),
15167
- openaiKey: ctx.runtime.getSetting("OPENAI_API_KEY"),
15168
- googleKey: ctx.runtime.getSetting("GOOGLE_GENERATIVE_AI_API_KEY"),
15169
- githubToken: ctx.runtime.getSetting("GITHUB_TOKEN")
15170
- };
16758
+ const rawAnthropicKey = ctx.runtime.getSetting("ANTHROPIC_API_KEY");
16759
+ let credentials;
16760
+ try {
16761
+ credentials = buildAgentCredentials(ctx.runtime);
16762
+ } catch (error) {
16763
+ const message = error instanceof Error ? error.message : "Failed to build credentials";
16764
+ sendError(res, message, 400);
16765
+ return true;
16766
+ }
15171
16767
  const agentStr = agentType ? agentType.toLowerCase() : await ctx.ptyService.resolveAgentType();
15172
16768
  const piRequested = isPiAgentType(agentStr);
15173
16769
  const normalizedType = normalizeAgentType(agentStr);
@@ -15204,7 +16800,7 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
15204
16800
  memoryContent,
15205
16801
  credentials,
15206
16802
  approvalPreset,
15207
- customCredentials,
16803
+ customCredentials: sanitizeCustomCredentials(customCredentials, isAnthropicOAuthToken(rawAnthropicKey) ? [rawAnthropicKey] : []),
15208
16804
  metadata: {
15209
16805
  threadId: taskThread?.id ?? requestedThreadId,
15210
16806
  requestedType: agentStr,
@@ -16136,5 +17732,5 @@ export {
16136
17732
  CodingWorkspaceService
16137
17733
  };
16138
17734
 
16139
- //# debugId=EA5312D04A2FFC5264756E2164756E21
17735
+ //# debugId=FCF9DB7BF01E156E64756E2164756E21
16140
17736
  //# sourceMappingURL=index.js.map