@elizaos/plugin-agent-orchestrator 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -537,6 +537,7 @@ __export(exports_swarm_decision_loop, {
537
537
  handleBlocked: () => handleBlocked,
538
538
  handleAutonomousDecision: () => handleAutonomousDecision,
539
539
  executeDecision: () => executeDecision,
540
+ clearDeferredTurnCompleteTimers: () => clearDeferredTurnCompleteTimers,
540
541
  checkAllTasksComplete: () => checkAllTasksComplete,
541
542
  POST_SEND_COOLDOWN_MS: () => POST_SEND_COOLDOWN_MS
542
543
  });
@@ -554,6 +555,12 @@ function withTimeout(promise, ms, label) {
554
555
  });
555
556
  });
556
557
  }
558
+ function clearDeferredTurnCompleteTimers() {
559
+ for (const timer of deferredTurnCompleteTimers.values()) {
560
+ clearTimeout(timer);
561
+ }
562
+ deferredTurnCompleteTimers.clear();
563
+ }
557
564
  function toContextSummary(taskCtx) {
558
565
  return {
559
566
  sessionId: taskCtx.sessionId,
@@ -733,14 +740,23 @@ function checkAllTasksComplete(ctx) {
733
740
  };
734
741
  if (swarmCompleteCb) {
735
742
  ctx.log("checkAllTasksComplete: swarm complete callback is wired — calling synthesis");
736
- const taskSummaries = tasks.map((t) => ({
737
- sessionId: t.sessionId,
738
- label: t.label,
739
- agentType: t.agentType,
740
- originalTask: t.originalTask,
741
- status: t.status,
742
- completionSummary: t.completionSummary ?? ""
743
- }));
743
+ const taskSummaries = tasks.map((t) => {
744
+ const decisions = ctx.sharedDecisions.filter((sd) => sd.agentLabel === t.label).map((sd) => sd.summary);
745
+ const summaryParts = [];
746
+ if (decisions.length > 0)
747
+ summaryParts.push(decisions.join("; "));
748
+ if (t.completionSummary)
749
+ summaryParts.push(t.completionSummary);
750
+ return {
751
+ sessionId: t.sessionId,
752
+ label: t.label,
753
+ agentType: t.agentType,
754
+ originalTask: t.originalTask,
755
+ status: t.status,
756
+ completionSummary: summaryParts.join(`
757
+ `) || ""
758
+ };
759
+ });
744
760
  withTimeout(Promise.resolve().then(() => swarmCompleteCb({
745
761
  tasks: taskSummaries,
746
762
  total: tasks.length,
@@ -958,6 +974,8 @@ async function handleBlocked(ctx, sessionId, taskCtx, data) {
958
974
  }
959
975
  }
960
976
  async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
977
+ if (taskCtx.status !== "active")
978
+ return;
961
979
  if (ctx.inFlightDecisions.has(sessionId)) {
962
980
  ctx.log(`Buffering turn-complete for ${sessionId} (in-flight decision running)`);
963
981
  ctx.pendingTurnComplete.set(sessionId, data);
@@ -966,10 +984,35 @@ async function handleTurnComplete(ctx, sessionId, taskCtx, data) {
966
984
  if (taskCtx.lastInputSentAt) {
967
985
  const elapsed = Date.now() - taskCtx.lastInputSentAt;
968
986
  if (elapsed < POST_SEND_COOLDOWN_MS) {
987
+ ctx.pendingTurnComplete.set(sessionId, data);
988
+ if (!deferredTurnCompleteTimers.has(sessionId)) {
989
+ const delayMs = POST_SEND_COOLDOWN_MS - elapsed + 50;
990
+ const timer = setTimeout(() => {
991
+ deferredTurnCompleteTimers.delete(sessionId);
992
+ const pendingData = ctx.pendingTurnComplete.get(sessionId);
993
+ if (!pendingData)
994
+ return;
995
+ const currentTask = ctx.tasks.get(sessionId);
996
+ if (!currentTask || currentTask.status !== "active") {
997
+ ctx.pendingTurnComplete.delete(sessionId);
998
+ return;
999
+ }
1000
+ handleTurnComplete(ctx, sessionId, currentTask, pendingData).catch((err) => {
1001
+ ctx.log(`Deferred turn-complete replay failed for ${sessionId}: ${err}`);
1002
+ });
1003
+ }, delayMs);
1004
+ deferredTurnCompleteTimers.set(sessionId, timer);
1005
+ }
969
1006
  ctx.log(`Suppressing turn-complete for "${taskCtx.label}" — ` + `${Math.round(elapsed / 1000)}s since last input (cooldown ${POST_SEND_COOLDOWN_MS / 1000}s)`);
970
1007
  return;
971
1008
  }
972
1009
  }
1010
+ const deferredTimer = deferredTurnCompleteTimers.get(sessionId);
1011
+ if (deferredTimer) {
1012
+ clearTimeout(deferredTimer);
1013
+ deferredTurnCompleteTimers.delete(sessionId);
1014
+ }
1015
+ ctx.pendingTurnComplete.delete(sessionId);
973
1016
  ctx.inFlightDecisions.add(sessionId);
974
1017
  try {
975
1018
  ctx.log(`Turn complete for "${taskCtx.label}" — assessing whether task is done`);
@@ -1219,10 +1262,11 @@ async function handleConfirmDecision(ctx, sessionId, taskCtx, promptText, recent
1219
1262
  await drainPendingBlocked(ctx, sessionId);
1220
1263
  }
1221
1264
  }
1222
- var DECISION_CB_TIMEOUT_MS = 30000, MAX_AUTO_RESPONSES = 10, POST_SEND_COOLDOWN_MS = 15000;
1265
+ var DECISION_CB_TIMEOUT_MS = 30000, MAX_AUTO_RESPONSES = 10, POST_SEND_COOLDOWN_MS = 15000, deferredTurnCompleteTimers;
1223
1266
  var init_swarm_decision_loop = __esm(() => {
1224
1267
  init_ansi_utils();
1225
1268
  init_swarm_event_triage();
1269
+ deferredTurnCompleteTimers = new Map;
1226
1270
  });
1227
1271
 
1228
1272
  // src/actions/finalize-workspace.ts
@@ -3177,6 +3221,7 @@ async function scanIdleSessions(ctx) {
3177
3221
  if (!session) {
3178
3222
  ctx.log(`Idle watchdog: "${taskCtx.label}" — PTY session no longer exists, marking as stopped`);
3179
3223
  taskCtx.status = "stopped";
3224
+ taskCtx.stoppedAt = now;
3180
3225
  taskCtx.decisions.push({
3181
3226
  timestamp: now,
3182
3227
  event: "idle_watchdog",
@@ -3220,6 +3265,7 @@ async function scanIdleSessions(ctx) {
3220
3265
  if (taskCtx.idleCheckCount >= MAX_IDLE_CHECKS) {
3221
3266
  ctx.log(`Idle watchdog: force-stopping "${taskCtx.label}" after ${MAX_IDLE_CHECKS} checks`);
3222
3267
  taskCtx.status = "stopped";
3268
+ taskCtx.stoppedAt = now;
3223
3269
  taskCtx.decisions.push({
3224
3270
  timestamp: now,
3225
3271
  event: "idle_watchdog",
@@ -3354,6 +3400,7 @@ async function handleIdleCheck(ctx, taskCtx, idleMinutes) {
3354
3400
  var UNREGISTERED_BUFFER_MS = 2000;
3355
3401
  var IDLE_SCAN_INTERVAL_MS = 60 * 1000;
3356
3402
  var PAUSE_TIMEOUT_MS = 30000;
3403
+ var STOPPED_RECOVERY_WINDOW_MS = 90000;
3357
3404
 
3358
3405
  class SwarmCoordinator {
3359
3406
  static serviceType = "SWARM_COORDINATOR";
@@ -3454,6 +3501,7 @@ class SwarmCoordinator {
3454
3501
  this.pendingDecisions.clear();
3455
3502
  this.inFlightDecisions.clear();
3456
3503
  this.pendingTurnComplete.clear();
3504
+ clearDeferredTurnCompleteTimers();
3457
3505
  this.lastBlockedPromptFingerprint.clear();
3458
3506
  this.pendingBlocked.clear();
3459
3507
  this.unregisteredBuffer.clear();
@@ -3635,8 +3683,21 @@ class SwarmCoordinator {
3635
3683
  }
3636
3684
  return;
3637
3685
  }
3686
+ let recoveredFromStopped = false;
3638
3687
  if (taskCtx.status === "stopped" || taskCtx.status === "error" || taskCtx.status === "completed") {
3639
- if (event !== "stopped" && event !== "error") {
3688
+ if (taskCtx.status === "stopped" && event === "task_complete") {
3689
+ const stoppedAt = taskCtx.stoppedAt ?? 0;
3690
+ const ageMs = Date.now() - stoppedAt;
3691
+ if (stoppedAt > 0 && ageMs <= STOPPED_RECOVERY_WINDOW_MS) {
3692
+ this.log(`Recovering "${taskCtx.label}" from stopped on late task_complete (${Math.round(ageMs / 1000)}s old)`);
3693
+ taskCtx.status = "active";
3694
+ recoveredFromStopped = true;
3695
+ } else {
3696
+ this.log(`Ignoring "${event}" for ${taskCtx.label} (status: stopped, age=${Math.round(ageMs / 1000)}s)`);
3697
+ return;
3698
+ }
3699
+ }
3700
+ if (!recoveredFromStopped && event !== "stopped" && event !== "error") {
3640
3701
  this.log(`Ignoring "${event}" for ${taskCtx.label} (status: ${taskCtx.status})`);
3641
3702
  return;
3642
3703
  }
@@ -3687,6 +3748,7 @@ class SwarmCoordinator {
3687
3748
  case "stopped":
3688
3749
  if (taskCtx.status !== "completed" && taskCtx.status !== "error") {
3689
3750
  taskCtx.status = "stopped";
3751
+ taskCtx.stoppedAt = Date.now();
3690
3752
  }
3691
3753
  this.inFlightDecisions.delete(sessionId);
3692
3754
  this.broadcast({
@@ -4774,7 +4836,9 @@ import {
4774
4836
  ModelType as ModelType5
4775
4837
  } from "@elizaos/core";
4776
4838
  // src/services/trajectory-feedback.ts
4839
+ import { logger as elizaLogger } from "@elizaos/core";
4777
4840
  var QUERY_TIMEOUT_MS = 5000;
4841
+ var SLOW_PATH_BUDGET_MS = 15000;
4778
4842
  function withTimeout2(promise, ms) {
4779
4843
  return Promise.race([
4780
4844
  promise,
@@ -4860,18 +4924,37 @@ async function queryPastExperience(runtime, options = {}) {
4860
4924
  if (!result.trajectories || result.trajectories.length === 0)
4861
4925
  return [];
4862
4926
  const experiences = [];
4927
+ const slowPathDeadline = Date.now() + SLOW_PATH_BUDGET_MS;
4863
4928
  const maxScans = Math.min(result.trajectories.length, maxTrajectories);
4864
4929
  for (let scanIdx = 0;scanIdx < maxScans; scanIdx++) {
4865
4930
  const summary = result.trajectories[scanIdx];
4866
- const detail = await withTimeout2(logger5.getTrajectoryDetail(summary.id), QUERY_TIMEOUT_MS).catch(() => null);
4867
- if (!detail?.steps)
4868
- continue;
4869
- const metadata = detail.metadata;
4931
+ const metadata = summary.metadata;
4932
+ const metadataInsights = Array.isArray(metadata?.insights) ? metadata.insights.filter((value) => typeof value === "string" && value.trim().length > 0).slice(0, 50) : [];
4870
4933
  const decisionType = metadata?.orchestrator?.decisionType ?? "unknown";
4871
4934
  const taskLabel = metadata?.orchestrator?.taskLabel ?? "";
4872
4935
  const trajectoryRepo = metadata?.orchestrator?.repo;
4873
4936
  if (repo && (!trajectoryRepo || trajectoryRepo !== repo))
4874
4937
  continue;
4938
+ if (metadataInsights.length > 0) {
4939
+ elizaLogger.debug(`[trajectory-feedback] Fast path: ${metadataInsights.length} insight(s) from metadata for ${summary.id}`);
4940
+ for (const insight of metadataInsights) {
4941
+ experiences.push({
4942
+ timestamp: summary.startTime,
4943
+ decisionType,
4944
+ taskLabel,
4945
+ insight
4946
+ });
4947
+ }
4948
+ continue;
4949
+ }
4950
+ if (Date.now() > slowPathDeadline) {
4951
+ elizaLogger.debug(`[trajectory-feedback] Slow path budget exhausted; stopping detail loads`);
4952
+ break;
4953
+ }
4954
+ elizaLogger.debug(`[trajectory-feedback] Slow path: loading full detail for ${summary.id} (no metadata insights)`);
4955
+ const detail = await withTimeout2(logger5.getTrajectoryDetail(summary.id), QUERY_TIMEOUT_MS).catch(() => null);
4956
+ if (!detail?.steps)
4957
+ continue;
4875
4958
  for (const step of detail.steps) {
4876
4959
  if (!step.llmCalls)
4877
4960
  continue;
@@ -4904,7 +4987,7 @@ async function queryPastExperience(runtime, options = {}) {
4904
4987
  }
4905
4988
  return Array.from(seen.values()).sort((a, b) => b.timestamp - a.timestamp).slice(0, maxEntries);
4906
4989
  } catch (err) {
4907
- console.error("[trajectory-feedback] Failed to query past experience:", err);
4990
+ elizaLogger.error(`[trajectory-feedback] Failed to query past experience: ${err}`);
4908
4991
  return [];
4909
4992
  }
4910
4993
  }
@@ -4998,8 +5081,8 @@ ${preview}` : `Agent "${label}" completed the task.`
4998
5081
  if ((event === "stopped" || event === "task_complete" || event === "error") && scratchDir) {
4999
5082
  const wsService = runtime.getService("CODING_WORKSPACE_SERVICE");
5000
5083
  if (wsService) {
5001
- wsService.removeScratchDir(scratchDir).catch((err) => {
5002
- logger5.warn(`[START_CODING_TASK] Failed to cleanup scratch dir for "${label}": ${err}`);
5084
+ wsService.registerScratchWorkspace(sessionId, scratchDir, label, event).catch((err) => {
5085
+ logger5.warn(`[START_CODING_TASK] Failed to register scratch workspace for "${label}": ${err}`);
5003
5086
  });
5004
5087
  }
5005
5088
  }
@@ -6046,6 +6129,7 @@ var activeWorkspaceContextProvider = {
6046
6129
  // src/services/workspace-service.ts
6047
6130
  import * as os3 from "node:os";
6048
6131
  import * as path5 from "node:path";
6132
+ import * as fs3 from "node:fs/promises";
6049
6133
  import {
6050
6134
  CredentialService,
6051
6135
  GitHubPatClient as GitHubPatClient2,
@@ -6327,6 +6411,8 @@ class CodingWorkspaceService {
6327
6411
  serviceConfig;
6328
6412
  workspaces = new Map;
6329
6413
  labels = new Map;
6414
+ scratchBySession = new Map;
6415
+ scratchCleanupTimers = new Map;
6330
6416
  eventCallbacks = [];
6331
6417
  authPromptCallback = null;
6332
6418
  constructor(runtime, config = {}) {
@@ -6384,6 +6470,10 @@ class CodingWorkspaceService {
6384
6470
  });
6385
6471
  }
6386
6472
  async stop() {
6473
+ for (const timer of this.scratchCleanupTimers.values()) {
6474
+ clearTimeout(timer);
6475
+ }
6476
+ this.scratchCleanupTimers.clear();
6387
6477
  for (const [id] of this.workspaces) {
6388
6478
  try {
6389
6479
  await this.removeWorkspace(id);
@@ -6583,6 +6673,89 @@ class CodingWorkspaceService {
6583
6673
  async removeScratchDir(dirPath) {
6584
6674
  return removeScratchDir(dirPath, this.serviceConfig.baseDir, (msg) => this.log(msg));
6585
6675
  }
6676
+ listScratchWorkspaces() {
6677
+ return Array.from(this.scratchBySession.values()).sort((a, b) => b.terminalAt - a.terminalAt);
6678
+ }
6679
+ async registerScratchWorkspace(sessionId, dirPath, label, terminalEvent) {
6680
+ const now = Date.now();
6681
+ const existing = this.scratchBySession.get(sessionId);
6682
+ const base = existing ?? {
6683
+ sessionId,
6684
+ label,
6685
+ path: dirPath,
6686
+ createdAt: now,
6687
+ terminalAt: now,
6688
+ terminalEvent,
6689
+ status: "pending_decision"
6690
+ };
6691
+ const policy = this.getScratchRetentionPolicy();
6692
+ if (policy === "ephemeral") {
6693
+ await this.removeScratchDir(dirPath);
6694
+ this.scratchBySession.delete(sessionId);
6695
+ this.clearScratchCleanupTimer(sessionId);
6696
+ return null;
6697
+ }
6698
+ const record = {
6699
+ ...base,
6700
+ label,
6701
+ path: dirPath,
6702
+ terminalAt: now,
6703
+ terminalEvent,
6704
+ status: policy === "persistent" ? "kept" : "pending_decision",
6705
+ expiresAt: undefined
6706
+ };
6707
+ this.scratchBySession.set(sessionId, record);
6708
+ if (record.status === "pending_decision") {
6709
+ const ttlMs = this.getScratchDecisionTtlMs();
6710
+ record.expiresAt = now + ttlMs;
6711
+ this.scheduleScratchCleanup(sessionId, ttlMs);
6712
+ } else {
6713
+ this.clearScratchCleanupTimer(sessionId);
6714
+ }
6715
+ return record;
6716
+ }
6717
+ async keepScratchWorkspace(sessionId) {
6718
+ const record = this.requireScratchWorkspace(sessionId);
6719
+ const next = {
6720
+ ...record,
6721
+ status: "kept",
6722
+ expiresAt: undefined
6723
+ };
6724
+ this.scratchBySession.set(sessionId, next);
6725
+ this.clearScratchCleanupTimer(sessionId);
6726
+ return next;
6727
+ }
6728
+ async deleteScratchWorkspace(sessionId) {
6729
+ const record = this.requireScratchWorkspace(sessionId);
6730
+ await this.removeScratchDir(record.path);
6731
+ this.scratchBySession.delete(sessionId);
6732
+ this.clearScratchCleanupTimer(sessionId);
6733
+ }
6734
+ async promoteScratchWorkspace(sessionId, name) {
6735
+ const record = this.requireScratchWorkspace(sessionId);
6736
+ const baseDir = this.serviceConfig.baseDir;
6737
+ const suggestedName = this.sanitizeWorkspaceName(name || record.label);
6738
+ const targetPath = await this.allocatePromotedPath(baseDir, suggestedName);
6739
+ try {
6740
+ await fs3.rename(record.path, targetPath);
6741
+ } catch (error) {
6742
+ const isExdev = typeof error === "object" && error !== null && "code" in error && error.code === "EXDEV";
6743
+ if (!isExdev)
6744
+ throw error;
6745
+ await fs3.cp(record.path, targetPath, { recursive: true });
6746
+ await fs3.access(targetPath);
6747
+ await fs3.rm(record.path, { recursive: true, force: true });
6748
+ }
6749
+ const next = {
6750
+ ...record,
6751
+ path: targetPath,
6752
+ status: "promoted",
6753
+ expiresAt: undefined
6754
+ };
6755
+ this.scratchBySession.set(sessionId, next);
6756
+ this.clearScratchCleanupTimer(sessionId);
6757
+ return next;
6758
+ }
6586
6759
  async gcOrphanedWorkspaces() {
6587
6760
  return gcOrphanedWorkspaces(this.serviceConfig.baseDir, this.serviceConfig.workspaceTtlMs ?? 24 * 60 * 60 * 1000, new Set(this.workspaces.keys()), (msg) => this.log(msg));
6588
6761
  }
@@ -6591,10 +6764,213 @@ class CodingWorkspaceService {
6591
6764
  console.log(`[CodingWorkspaceService] ${message}`);
6592
6765
  }
6593
6766
  }
6767
+ getScratchRetentionPolicy() {
6768
+ const setting = this.runtime.getSetting("PARALLAX_SCRATCH_RETENTION") ?? process.env.PARALLAX_SCRATCH_RETENTION;
6769
+ const normalized = setting?.trim().toLowerCase();
6770
+ if (normalized === "ephemeral")
6771
+ return "ephemeral";
6772
+ if (normalized === "persistent" || normalized === "keep") {
6773
+ return "persistent";
6774
+ }
6775
+ return "pending_decision";
6776
+ }
6777
+ getScratchDecisionTtlMs() {
6778
+ const setting = this.runtime.getSetting("PARALLAX_SCRATCH_DECISION_TTL_MS");
6779
+ const parsed = Number(setting ?? process.env.PARALLAX_SCRATCH_DECISION_TTL_MS);
6780
+ if (Number.isFinite(parsed) && parsed > 0)
6781
+ return parsed;
6782
+ return 24 * 60 * 60 * 1000;
6783
+ }
6784
+ requireScratchWorkspace(sessionId) {
6785
+ const record = this.scratchBySession.get(sessionId);
6786
+ if (!record) {
6787
+ throw new Error(`Scratch workspace for session ${sessionId} not found`);
6788
+ }
6789
+ return record;
6790
+ }
6791
+ clearScratchCleanupTimer(sessionId) {
6792
+ const timer = this.scratchCleanupTimers.get(sessionId);
6793
+ if (timer) {
6794
+ clearTimeout(timer);
6795
+ this.scratchCleanupTimers.delete(sessionId);
6796
+ }
6797
+ }
6798
+ scheduleScratchCleanup(sessionId, ttlMs) {
6799
+ this.clearScratchCleanupTimer(sessionId);
6800
+ const timer = setTimeout(async () => {
6801
+ try {
6802
+ const record = this.scratchBySession.get(sessionId);
6803
+ if (!record || record.status !== "pending_decision")
6804
+ return;
6805
+ await this.removeScratchDir(record.path);
6806
+ } catch (error) {
6807
+ console.warn(`[CodingWorkspaceService] scratch cleanup failed for ${sessionId}: ${String(error)}`);
6808
+ } finally {
6809
+ this.scratchBySession.delete(sessionId);
6810
+ this.scratchCleanupTimers.delete(sessionId);
6811
+ }
6812
+ }, ttlMs);
6813
+ this.scratchCleanupTimers.set(sessionId, timer);
6814
+ }
6815
+ sanitizeWorkspaceName(raw) {
6816
+ const compact = raw.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
6817
+ return compact || `scratch-${Date.now().toString(36)}`;
6818
+ }
6819
+ async allocatePromotedPath(baseDir, baseName) {
6820
+ const baseResolved = path5.resolve(baseDir);
6821
+ for (let i = 0;i < 1000; i++) {
6822
+ const candidateName = i === 0 ? baseName : `${baseName}-${i}`;
6823
+ const candidate = path5.resolve(baseResolved, candidateName);
6824
+ if (candidate !== baseResolved && !candidate.startsWith(`${baseResolved}${path5.sep}`)) {
6825
+ continue;
6826
+ }
6827
+ try {
6828
+ await fs3.access(candidate);
6829
+ } catch {
6830
+ return candidate;
6831
+ }
6832
+ }
6833
+ throw new Error("Unable to allocate promoted workspace path");
6834
+ }
6594
6835
  }
6595
6836
  // src/api/agent-routes.ts
6837
+ import { access as access2, readFile as readFile3, realpath, rm as rm2 } from "node:fs/promises";
6838
+ import { createHash } from "node:crypto";
6596
6839
  import * as os4 from "node:os";
6597
6840
  import * as path6 from "node:path";
6841
+ import { execFile } from "node:child_process";
6842
+ import { promisify } from "node:util";
6843
+ var execFileAsync = promisify(execFile);
6844
+ var PREFLIGHT_DONE = new Set;
6845
+ var PREFLIGHT_INFLIGHT = new Map;
6846
+ function shouldAutoPreflight() {
6847
+ if (process.env.PARALLAX_BENCHMARK_PREFLIGHT_AUTO === "1")
6848
+ return true;
6849
+ return false;
6850
+ }
6851
+ function isPathInside(parent, candidate) {
6852
+ return candidate === parent || candidate.startsWith(`${parent}${path6.sep}`);
6853
+ }
6854
+ async function resolveSafeVenvPath(workdir, venvDirRaw) {
6855
+ const venvDir = venvDirRaw.trim();
6856
+ if (!venvDir) {
6857
+ throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be non-empty");
6858
+ }
6859
+ if (path6.isAbsolute(venvDir)) {
6860
+ throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must be relative to workdir");
6861
+ }
6862
+ const normalized = path6.normalize(venvDir);
6863
+ if (normalized === "." || normalized === ".." || normalized.startsWith(`..${path6.sep}`)) {
6864
+ throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must stay within workdir");
6865
+ }
6866
+ const workdirResolved = path6.resolve(workdir);
6867
+ const workdirReal = await realpath(workdirResolved);
6868
+ const resolved = path6.resolve(workdirReal, normalized);
6869
+ if (!isPathInside(workdirReal, resolved)) {
6870
+ throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV resolves outside workdir");
6871
+ }
6872
+ if (resolved === workdirReal) {
6873
+ throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV must not resolve to workdir root");
6874
+ }
6875
+ try {
6876
+ const resolvedReal = await realpath(resolved);
6877
+ if (!isPathInside(workdirReal, resolvedReal) || resolvedReal === workdirReal) {
6878
+ throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV resolves outside workdir");
6879
+ }
6880
+ } catch (err) {
6881
+ const maybeErr = err;
6882
+ if (maybeErr?.code !== "ENOENT")
6883
+ throw err;
6884
+ const parentReal = await realpath(path6.dirname(resolved));
6885
+ if (!isPathInside(workdirReal, parentReal)) {
6886
+ throw new Error("PARALLAX_BENCHMARK_PREFLIGHT_VENV parent resolves outside workdir");
6887
+ }
6888
+ }
6889
+ return resolved;
6890
+ }
6891
+ async function fileExists(filePath) {
6892
+ try {
6893
+ await access2(filePath);
6894
+ return true;
6895
+ } catch {
6896
+ return false;
6897
+ }
6898
+ }
6899
+ async function resolveRequirementsPath(workdir) {
6900
+ const workdirReal = await realpath(path6.resolve(workdir));
6901
+ const candidates = [
6902
+ path6.join(workdir, "apps", "api", "requirements.txt"),
6903
+ path6.join(workdir, "requirements.txt")
6904
+ ];
6905
+ for (const candidate of candidates) {
6906
+ if (!await fileExists(candidate))
6907
+ continue;
6908
+ try {
6909
+ const candidateReal = await realpath(candidate);
6910
+ if (isPathInside(workdirReal, candidateReal))
6911
+ return candidateReal;
6912
+ } catch {}
6913
+ }
6914
+ return null;
6915
+ }
6916
+ async function fingerprintRequirementsFile(requirementsPath) {
6917
+ const file = await readFile3(requirementsPath);
6918
+ return createHash("sha256").update(file).digest("hex");
6919
+ }
6920
+ async function runBenchmarkPreflight(workdir) {
6921
+ if (!shouldAutoPreflight())
6922
+ return;
6923
+ const requirementsPath = await resolveRequirementsPath(workdir);
6924
+ if (!requirementsPath)
6925
+ return;
6926
+ const requirementsFingerprint = await fingerprintRequirementsFile(requirementsPath);
6927
+ const mode = process.env.PARALLAX_BENCHMARK_PREFLIGHT_MODE?.toLowerCase() === "warm" ? "warm" : "cold";
6928
+ const venvDir = process.env.PARALLAX_BENCHMARK_PREFLIGHT_VENV || ".benchmark-venv";
6929
+ const venvPath = await resolveSafeVenvPath(workdir, venvDir);
6930
+ const pythonInVenv = path6.join(venvPath, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
6931
+ const key = `${workdir}::${mode}::${venvPath}::${requirementsFingerprint}`;
6932
+ if (PREFLIGHT_DONE.has(key)) {
6933
+ if (await fileExists(pythonInVenv))
6934
+ return;
6935
+ PREFLIGHT_DONE.delete(key);
6936
+ }
6937
+ const existing = PREFLIGHT_INFLIGHT.get(key);
6938
+ if (existing) {
6939
+ await existing;
6940
+ return;
6941
+ }
6942
+ const run = (async () => {
6943
+ const pythonCommand = process.platform === "win32" ? "python" : "python3";
6944
+ if (mode === "cold") {
6945
+ await rm2(venvPath, { recursive: true, force: true });
6946
+ }
6947
+ const hasVenv = await fileExists(pythonInVenv);
6948
+ if (!hasVenv) {
6949
+ await execFileAsync(pythonCommand, ["-m", "venv", venvPath], {
6950
+ cwd: workdir,
6951
+ timeout: 120000,
6952
+ maxBuffer: 8 * 1024 * 1024
6953
+ });
6954
+ }
6955
+ await execFileAsync(pythonInVenv, ["-m", "pip", "install", "--upgrade", "pip"], {
6956
+ cwd: workdir,
6957
+ timeout: 300000,
6958
+ maxBuffer: 8 * 1024 * 1024
6959
+ });
6960
+ await execFileAsync(pythonInVenv, ["-m", "pip", "install", "-r", requirementsPath], {
6961
+ cwd: workdir,
6962
+ timeout: 600000,
6963
+ maxBuffer: 16 * 1024 * 1024
6964
+ });
6965
+ PREFLIGHT_DONE.add(key);
6966
+ })();
6967
+ PREFLIGHT_INFLIGHT.set(key, run);
6968
+ try {
6969
+ await run;
6970
+ } finally {
6971
+ PREFLIGHT_INFLIGHT.delete(key);
6972
+ }
6973
+ }
6598
6974
  async function handleAgentRoutes(req, res, pathname, ctx) {
6599
6975
  const method = req.method?.toUpperCase();
6600
6976
  if (method === "GET" && pathname === "/api/coding-agents/preflight") {
@@ -6618,6 +6994,44 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
6618
6994
  sendJson(res, ctx.ptyService.getAgentMetrics());
6619
6995
  return true;
6620
6996
  }
6997
+ if (method === "GET" && pathname === "/api/coding-agents/scratch") {
6998
+ if (!ctx.workspaceService) {
6999
+ sendError(res, "Workspace Service not available", 503);
7000
+ return true;
7001
+ }
7002
+ sendJson(res, ctx.workspaceService.listScratchWorkspaces());
7003
+ return true;
7004
+ }
7005
+ const scratchActionMatch = pathname.match(/^\/api\/coding-agents\/([^/]+)\/scratch\/(keep|delete|promote)$/);
7006
+ if (method === "POST" && scratchActionMatch) {
7007
+ if (!ctx.workspaceService) {
7008
+ sendError(res, "Workspace Service not available", 503);
7009
+ return true;
7010
+ }
7011
+ const sessionId = scratchActionMatch[1];
7012
+ const action = scratchActionMatch[2];
7013
+ try {
7014
+ if (action === "keep") {
7015
+ const scratch2 = await ctx.workspaceService.keepScratchWorkspace(sessionId);
7016
+ sendJson(res, { success: true, scratch: scratch2 });
7017
+ return true;
7018
+ }
7019
+ if (action === "delete") {
7020
+ await ctx.workspaceService.deleteScratchWorkspace(sessionId);
7021
+ sendJson(res, { success: true, deleted: true, sessionId });
7022
+ return true;
7023
+ }
7024
+ const body = await parseBody(req);
7025
+ const promoteName = typeof body.name === "string" ? body.name : undefined;
7026
+ const scratch = await ctx.workspaceService.promoteScratchWorkspace(sessionId, promoteName);
7027
+ sendJson(res, { success: true, scratch });
7028
+ } catch (error) {
7029
+ const message = error instanceof Error ? error.message : String(error);
7030
+ const status = message.includes("not found") ? 404 : 500;
7031
+ sendError(res, message, status);
7032
+ }
7033
+ return true;
7034
+ }
6621
7035
  if (method === "GET" && pathname === "/api/coding-agents/workspace-files") {
6622
7036
  if (!ctx.ptyService) {
6623
7037
  sendError(res, "PTY Service not available", 503);
@@ -6722,19 +7136,25 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
6722
7136
  metadata
6723
7137
  } = body;
6724
7138
  const workspaceBaseDir = path6.join(os4.homedir(), ".milady", "workspaces");
6725
- const allowedPrefixes = [
6726
- path6.resolve(workspaceBaseDir),
6727
- path6.resolve(process.cwd())
6728
- ];
7139
+ const workspaceBaseDirResolved = path6.resolve(workspaceBaseDir);
7140
+ const cwdResolved = path6.resolve(process.cwd());
7141
+ const workspaceBaseDirReal = await realpath(workspaceBaseDirResolved).catch(() => workspaceBaseDirResolved);
7142
+ const cwdReal = await realpath(cwdResolved).catch(() => cwdResolved);
7143
+ const allowedPrefixes = [workspaceBaseDirReal, cwdReal];
6729
7144
  let workdir = rawWorkdir;
6730
7145
  if (workdir) {
6731
7146
  const resolved = path6.resolve(workdir);
6732
- const isAllowed = allowedPrefixes.some((prefix2) => resolved === prefix2 || resolved.startsWith(prefix2 + path6.sep));
7147
+ const resolvedReal = await realpath(resolved).catch(() => null);
7148
+ if (!resolvedReal) {
7149
+ sendError(res, "workdir must exist", 403);
7150
+ return true;
7151
+ }
7152
+ const isAllowed = allowedPrefixes.some((prefix2) => resolvedReal === prefix2 || resolvedReal.startsWith(prefix2 + path6.sep));
6733
7153
  if (!isAllowed) {
6734
7154
  sendError(res, "workdir must be within workspace base directory or cwd", 403);
6735
7155
  return true;
6736
7156
  }
6737
- workdir = resolved;
7157
+ workdir = resolvedReal;
6738
7158
  }
6739
7159
  const activeSessions = await ctx.ptyService.listSessions();
6740
7160
  const maxSessions = 8;
@@ -6742,6 +7162,13 @@ async function handleAgentRoutes(req, res, pathname, ctx) {
6742
7162
  sendError(res, `Concurrent session limit reached (${maxSessions})`, 429);
6743
7163
  return true;
6744
7164
  }
7165
+ if (workdir) {
7166
+ try {
7167
+ await runBenchmarkPreflight(workdir);
7168
+ } catch (preflightError) {
7169
+ console.warn(`[coding-agent] benchmark preflight failed for ${workdir}:`, preflightError);
7170
+ }
7171
+ }
6745
7172
  const credentials = {
6746
7173
  anthropicKey: ctx.runtime.getSetting("ANTHROPIC_API_KEY"),
6747
7174
  openaiKey: ctx.runtime.getSetting("OPENAI_API_KEY"),
@@ -7366,7 +7793,7 @@ async function handleWorkspaceRoutes(req, res, pathname, ctx) {
7366
7793
  // src/api/routes.ts
7367
7794
  var MAX_BODY_SIZE = 1024 * 1024;
7368
7795
  async function parseBody(req) {
7369
- return new Promise((resolve5, reject) => {
7796
+ return new Promise((resolve6, reject) => {
7370
7797
  let body = "";
7371
7798
  let size = 0;
7372
7799
  req.on("data", (chunk) => {
@@ -7380,7 +7807,7 @@ async function parseBody(req) {
7380
7807
  });
7381
7808
  req.on("end", () => {
7382
7809
  try {
7383
- resolve5(body ? JSON.parse(body) : {});
7810
+ resolve6(body ? JSON.parse(body) : {});
7384
7811
  } catch {
7385
7812
  reject(new Error("Invalid JSON body"));
7386
7813
  }
@@ -7468,5 +7895,5 @@ export {
7468
7895
  CodingWorkspaceService
7469
7896
  };
7470
7897
 
7471
- //# debugId=4383E51DF7A1F4A764756E2164756E21
7898
+ //# debugId=C0B3DB8984D7685C64756E2164756E21
7472
7899
  //# sourceMappingURL=index.js.map