@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/actions/coding-task-helpers.d.ts.map +1 -1
- package/dist/api/agent-routes.d.ts.map +1 -1
- package/dist/index.js +453 -26
- package/dist/index.js.map +9 -9
- package/dist/services/swarm-coordinator.d.ts +2 -0
- package/dist/services/swarm-coordinator.d.ts.map +1 -1
- package/dist/services/swarm-decision-loop.d.ts +2 -0
- package/dist/services/swarm-decision-loop.d.ts.map +1 -1
- package/dist/services/swarm-idle-watchdog.d.ts.map +1 -1
- package/dist/services/trajectory-feedback.d.ts +1 -1
- package/dist/services/trajectory-feedback.d.ts.map +1 -1
- package/dist/services/workspace-service.d.ts +25 -0
- package/dist/services/workspace-service.d.ts.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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 (
|
|
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
|
|
4867
|
-
|
|
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
|
-
|
|
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.
|
|
5002
|
-
logger5.warn(`[START_CODING_TASK] Failed to
|
|
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
|
|
6726
|
-
|
|
6727
|
-
|
|
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
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
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=
|
|
7898
|
+
//# debugId=C0B3DB8984D7685C64756E2164756E21
|
|
7472
7899
|
//# sourceMappingURL=index.js.map
|