@strideops/bridge 0.1.3 → 0.1.5
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/{autostart-77BPPTEG.js → autostart-ASW5MDQS.js} +46 -14
- package/dist/autostart-ASW5MDQS.js.map +1 -0
- package/dist/{chunk-GBLMB3XB.js → chunk-6LRDE2ZS.js} +9 -3
- package/dist/chunk-6LRDE2ZS.js.map +1 -0
- package/dist/cli.js +1293 -187
- package/dist/cli.js.map +1 -1
- package/package.json +7 -2
- package/dist/autostart-77BPPTEG.js.map +0 -1
- package/dist/chunk-GBLMB3XB.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { requireConfig, log, readConfig, CONFIG_PATH, isConfigured, writeConfig, logError, logWarn,
|
|
2
|
+
import { BRIDGE_DIR, requireConfig, log, readConfig, CONFIG_PATH, isConfigured, writeConfig, logError, logWarn, PENDING_REPORTS_PATH, AGENTS_DIR, readJsonSafe, writeJsonAtomic, __require } from './chunk-6LRDE2ZS.js';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import { homedir, cpus, totalmem, release, platform, arch, hostname } from 'os';
|
|
4
|
+
import os, { homedir, cpus, totalmem, release, platform, arch, hostname, loadavg, freemem } from 'os';
|
|
5
5
|
import { join, dirname } from 'path';
|
|
6
6
|
import { existsSync, mkdirSync, accessSync, constants, writeFileSync, chmodSync } from 'fs';
|
|
7
7
|
import { execSync, spawn } from 'child_process';
|
|
8
|
+
import { readFile, statfs } from 'fs/promises';
|
|
8
9
|
|
|
9
10
|
// src/version.ts
|
|
10
|
-
var VERSION = "0.1.
|
|
11
|
+
var VERSION = "0.1.5";
|
|
11
12
|
async function runPair(pairingCode, apiBaseUrl) {
|
|
12
13
|
const url = `${apiBaseUrl.replace(/\/$/, "")}/api/bridge/v1/pair`;
|
|
13
14
|
log(`Pairing with ${apiBaseUrl} ...`);
|
|
@@ -82,8 +83,38 @@ async function runPair(pairingCode, apiBaseUrl) {
|
|
|
82
83
|
console.log(` Auth mode : ${authMode}`);
|
|
83
84
|
console.log(` Config : ~/.stride-bridge/config.json`);
|
|
84
85
|
console.log();
|
|
86
|
+
await offerAutostart();
|
|
85
87
|
console.log(`Run \`stride-bridge start\` to begin accepting work.`);
|
|
86
88
|
}
|
|
89
|
+
async function offerAutostart() {
|
|
90
|
+
if (process.platform !== "win32" || !process.stdin.isTTY) {
|
|
91
|
+
if (process.platform === "win32") {
|
|
92
|
+
console.log(
|
|
93
|
+
`Tip: run \`stride-bridge autostart install\` so the daemon starts at login.`
|
|
94
|
+
);
|
|
95
|
+
console.log();
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const { createInterface } = await import('readline/promises');
|
|
100
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
101
|
+
try {
|
|
102
|
+
const answer = (await rl.question(
|
|
103
|
+
"Start the daemon automatically at Windows login? [Y/n] "
|
|
104
|
+
)).trim().toLowerCase();
|
|
105
|
+
if (answer === "" || answer === "y" || answer === "yes") {
|
|
106
|
+
const { autostartInstall } = await import('./autostart-ASW5MDQS.js');
|
|
107
|
+
autostartInstall();
|
|
108
|
+
} else {
|
|
109
|
+
console.log(
|
|
110
|
+
`Skipped. You can enable it later with \`stride-bridge autostart install\`.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
rl.close();
|
|
115
|
+
console.log();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
87
118
|
|
|
88
119
|
// src/api.ts
|
|
89
120
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
@@ -386,8 +417,7 @@ if (!query) {
|
|
|
386
417
|
|
|
387
418
|
const body = JSON.stringify({ query, limit: 5 });
|
|
388
419
|
const ts = Math.floor(Date.now() / 1000).toString();
|
|
389
|
-
const sig = createHmac("sha256", HOOK_SECRET).update(ts + "
|
|
390
|
-
" + body).digest("hex");
|
|
420
|
+
const sig = createHmac("sha256", HOOK_SECRET).update(ts + "\\n" + body).digest("hex");
|
|
391
421
|
|
|
392
422
|
const res = await fetch(SEARCH_URL, {
|
|
393
423
|
method: "POST",
|
|
@@ -418,6 +448,136 @@ if (results.length === 0) {
|
|
|
418
448
|
mkdirSync(claudeDir, { recursive: true });
|
|
419
449
|
writeFileSync(join(claudeDir, "memory-search.mjs"), script, { encoding: "utf-8", mode: 448 });
|
|
420
450
|
}
|
|
451
|
+
function writeApprovalRequestScript(opts) {
|
|
452
|
+
const { agentId, hookSecret, apiBaseUrl, workspaceDir } = opts;
|
|
453
|
+
const base = apiBaseUrl.replace(/\/$/, "");
|
|
454
|
+
const approvalsUrl = `${base}/api/internal/build/agents/${agentId}/approvals`;
|
|
455
|
+
const script = `#!/usr/bin/env node
|
|
456
|
+
// Auto-generated by stride-bridge. Do not edit.
|
|
457
|
+
// Posts a plan/question/action approval request to Stride (HMAC-signed).
|
|
458
|
+
// Usage: node .claude/approval-request.mjs <title> <plan>
|
|
459
|
+
// After calling this, END your current run and wait to be re-woken.
|
|
460
|
+
|
|
461
|
+
import { createHmac } from "node:crypto";
|
|
462
|
+
|
|
463
|
+
const HOOK_SECRET = ${JSON.stringify(hookSecret)};
|
|
464
|
+
const APPROVALS_URL = ${JSON.stringify(approvalsUrl)};
|
|
465
|
+
|
|
466
|
+
const args = process.argv.slice(2);
|
|
467
|
+
const title = (args[0] ?? "").trim();
|
|
468
|
+
const plan = (args[1] ?? "").trim();
|
|
469
|
+
|
|
470
|
+
if (!title) {
|
|
471
|
+
console.error("approval-request: title is required");
|
|
472
|
+
console.error("usage: node .claude/approval-request.mjs <title> <plan>");
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const body = JSON.stringify({ type: "plan", title: title.slice(0, 200), body: plan.slice(0, 8000) });
|
|
477
|
+
const ts = Math.floor(Date.now() / 1000).toString();
|
|
478
|
+
const sig = createHmac("sha256", HOOK_SECRET).update(ts + "\\n" + body).digest("hex");
|
|
479
|
+
|
|
480
|
+
const res = await fetch(APPROVALS_URL, {
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers: {
|
|
483
|
+
"Content-Type": "application/json",
|
|
484
|
+
"x-stride-hook-ts": ts,
|
|
485
|
+
"x-stride-hook-sig": sig,
|
|
486
|
+
},
|
|
487
|
+
body,
|
|
488
|
+
}).catch((err) => ({ ok: false, statusText: String(err) }));
|
|
489
|
+
|
|
490
|
+
if (!res.ok) {
|
|
491
|
+
console.error(\`approval-request: failed (\${res.statusText ?? res.status})\`);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const data = await res.json();
|
|
496
|
+
console.log(\`approval-request: submitted (approvalId=\${data?.approvalId ?? "?"})\`);
|
|
497
|
+
console.log(data?.message ?? "End your current run and wait to be re-woken.");
|
|
498
|
+
`;
|
|
499
|
+
const claudeDir = join(workspaceDir, ".claude");
|
|
500
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
501
|
+
writeFileSync(join(claudeDir, "approval-request.mjs"), script, { encoding: "utf-8", mode: 448 });
|
|
502
|
+
}
|
|
503
|
+
var PROBE_TIMEOUT_MS = 3e3;
|
|
504
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
505
|
+
var PROBES = [
|
|
506
|
+
{ runtime: "claude-code", cmd: "claude", args: ["--version"] },
|
|
507
|
+
{ runtime: "codex", cmd: "codex", args: ["--version"] },
|
|
508
|
+
{ runtime: "hermes", cmd: "hermes", args: ["--version"] }
|
|
509
|
+
];
|
|
510
|
+
function probeRuntime(cmd, args) {
|
|
511
|
+
return new Promise((resolve) => {
|
|
512
|
+
let child;
|
|
513
|
+
let resolved = false;
|
|
514
|
+
const done = (value) => {
|
|
515
|
+
if (!resolved) {
|
|
516
|
+
resolved = true;
|
|
517
|
+
resolve(value);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
const useShell = process.platform === "win32";
|
|
521
|
+
try {
|
|
522
|
+
child = spawn(cmd, args, {
|
|
523
|
+
shell: useShell,
|
|
524
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
525
|
+
env: process.env
|
|
526
|
+
});
|
|
527
|
+
} catch {
|
|
528
|
+
done("");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
let stdoutBuf = "";
|
|
532
|
+
child.stdout?.on("data", (chunk) => {
|
|
533
|
+
stdoutBuf += chunk.toString("utf-8");
|
|
534
|
+
});
|
|
535
|
+
const timer = setTimeout(() => {
|
|
536
|
+
try {
|
|
537
|
+
child.kill("SIGTERM");
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
540
|
+
done("");
|
|
541
|
+
}, PROBE_TIMEOUT_MS);
|
|
542
|
+
child.on("close", () => {
|
|
543
|
+
clearTimeout(timer);
|
|
544
|
+
const firstLine = stdoutBuf.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
|
|
545
|
+
done(firstLine);
|
|
546
|
+
});
|
|
547
|
+
child.on("error", () => {
|
|
548
|
+
clearTimeout(timer);
|
|
549
|
+
done("");
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
function extractVersion(raw) {
|
|
554
|
+
if (!raw.trim()) return "";
|
|
555
|
+
const match = raw.match(/\d+\.\d+(?:\.\d+)*/);
|
|
556
|
+
return match ? match[0] : raw.trim().split(/\s+/).pop() ?? "";
|
|
557
|
+
}
|
|
558
|
+
var cached = null;
|
|
559
|
+
async function detectRuntimes(force = false) {
|
|
560
|
+
if (!force && cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
561
|
+
return cached.capabilities;
|
|
562
|
+
}
|
|
563
|
+
const results = [];
|
|
564
|
+
for (const probe of PROBES) {
|
|
565
|
+
try {
|
|
566
|
+
const raw = await probeRuntime(probe.cmd, probe.args);
|
|
567
|
+
results.push({ runtime: probe.runtime, version: extractVersion(raw) });
|
|
568
|
+
} catch (err) {
|
|
569
|
+
logWarn(
|
|
570
|
+
`Runtime probe failed for "${probe.runtime}": ${err instanceof Error ? err.message : String(err)}`
|
|
571
|
+
);
|
|
572
|
+
results.push({ runtime: probe.runtime, version: "" });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
cached = { capabilities: results, at: Date.now() };
|
|
576
|
+
return results;
|
|
577
|
+
}
|
|
578
|
+
function invalidateRuntimeCache() {
|
|
579
|
+
cached = null;
|
|
580
|
+
}
|
|
421
581
|
|
|
422
582
|
// src/agents.ts
|
|
423
583
|
var cachedAgents = [];
|
|
@@ -431,10 +591,14 @@ async function refreshAgents(config) {
|
|
|
431
591
|
for (const agent of cachedAgents) {
|
|
432
592
|
await ensureAgentWorkspace(config, agent);
|
|
433
593
|
}
|
|
594
|
+
invalidateRuntimeCache();
|
|
434
595
|
} catch (err) {
|
|
435
596
|
logError("Failed to refresh agents", err);
|
|
436
597
|
}
|
|
437
598
|
}
|
|
599
|
+
function getCachedAgents() {
|
|
600
|
+
return cachedAgents;
|
|
601
|
+
}
|
|
438
602
|
function findAgent(agentId) {
|
|
439
603
|
return cachedAgents.find((a) => a.id === agentId);
|
|
440
604
|
}
|
|
@@ -449,15 +613,30 @@ async function ensureAgentWorkspace(config, agent) {
|
|
|
449
613
|
sections.push(
|
|
450
614
|
'## Memory sync (local agent)\n\nAfter updating MEMORY.md or today\'s daily journal, sync them to Stride by running:\n\n```\nnode .claude/memory-sync.mjs\n```\n\nBefore re-researching something you may have learned before, search your own past memory semantically:\n\n```\nnode .claude/memory-search.mjs "your question"\n```\n'
|
|
451
615
|
);
|
|
616
|
+
if (agent.approvalProtocolMd) {
|
|
617
|
+
sections.push(
|
|
618
|
+
agent.approvalProtocolMd + '\n\n### Approval Request (local agent)\n\nPost a plan or question for human review using the Windows-safe Node script:\n\n```\nnode .claude/approval-request.mjs "<title>" "<plan>"\n```\n\nAfter posting, **end your current run immediately** and wait to be re-woken.'
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
if (agent.experimentProtocolMd) {
|
|
622
|
+
sections.push(agent.experimentProtocolMd);
|
|
623
|
+
if (agent.experimentSnippet) {
|
|
624
|
+
sections.push(agent.experimentSnippet);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
452
627
|
if (!agent.onboardedAt && agent.onboardingMd) {
|
|
453
628
|
sections.push(
|
|
454
629
|
"## FIRST BOOT \u2014 onboarding required\n\nYou have not been onboarded. Read ONBOARDING.md in this workspace and complete the interview BEFORE regular work. To sync your identity after the interview, run:\n\n```\nnode .claude/identity-sync.mjs\n```\n"
|
|
455
630
|
);
|
|
456
631
|
}
|
|
457
|
-
|
|
632
|
+
const claudeMdContent = sections.filter(Boolean).join("\n\n");
|
|
633
|
+
writeFileSync(join(workspaceDir, "CLAUDE.md"), claudeMdContent, "utf-8");
|
|
634
|
+
writeFileSync(join(workspaceDir, "AGENTS.md"), claudeMdContent, "utf-8");
|
|
458
635
|
if (!agent.onboardedAt && agent.onboardingMd) {
|
|
459
636
|
writeFileSync(join(workspaceDir, "ONBOARDING.md"), agent.onboardingMd, "utf-8");
|
|
460
637
|
}
|
|
638
|
+
const soulContent = agent.soulMd ?? agent.defaultSoulMd;
|
|
639
|
+
if (soulContent) writeFileSync(join(workspaceDir, "SOUL.md"), soulContent, "utf-8");
|
|
461
640
|
if (agent.identityMd) writeFileSync(join(workspaceDir, "IDENTITY.md"), agent.identityMd, "utf-8");
|
|
462
641
|
if (agent.userMd) writeFileSync(join(workspaceDir, "USER.md"), agent.userMd, "utf-8");
|
|
463
642
|
if (agent.heartbeatChecklist) {
|
|
@@ -495,6 +674,12 @@ async function ensureAgentWorkspace(config, agent) {
|
|
|
495
674
|
apiBaseUrl: config.apiBaseUrl,
|
|
496
675
|
workspaceDir
|
|
497
676
|
});
|
|
677
|
+
writeApprovalRequestScript({
|
|
678
|
+
agentId: agent.id,
|
|
679
|
+
hookSecret: agent.hookSecret,
|
|
680
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
681
|
+
workspaceDir
|
|
682
|
+
});
|
|
498
683
|
log(`Provisioned workspace for agent "${agent.name}" (${agent.id})`);
|
|
499
684
|
}
|
|
500
685
|
function agentWorkspaceDir(agentId) {
|
|
@@ -550,6 +735,151 @@ function recordRunSession(agentId, newSessionId, previous, nowMs) {
|
|
|
550
735
|
return next;
|
|
551
736
|
}
|
|
552
737
|
|
|
738
|
+
// src/prompt.ts
|
|
739
|
+
function buildPromptText(opts) {
|
|
740
|
+
try {
|
|
741
|
+
const { workPrompt, agent, cwd, agentWorkspace, rotated } = opts;
|
|
742
|
+
const parts = [];
|
|
743
|
+
const soulText = agent?.soulMd ?? agent?.defaultSoulMd;
|
|
744
|
+
const shouldInjectSoul = cwd !== agentWorkspace && soulText != null && soulText.trim().length > 0;
|
|
745
|
+
if (shouldInjectSoul) {
|
|
746
|
+
let identityBlock = `## Who you are
|
|
747
|
+
|
|
748
|
+
${soulText}`;
|
|
749
|
+
if (agent?.identityMd && agent.identityMd.trim().length > 0) {
|
|
750
|
+
identityBlock += `
|
|
751
|
+
|
|
752
|
+
${agent.identityMd}`;
|
|
753
|
+
}
|
|
754
|
+
parts.push(identityBlock);
|
|
755
|
+
}
|
|
756
|
+
if (rotated) {
|
|
757
|
+
parts.push(
|
|
758
|
+
"## Session rotated\nYour previous session ended (context rotation). Read HANDOFF.md and your recent memory/ journal entries before starting."
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
parts.push(workPrompt);
|
|
762
|
+
return parts.join("\n\n---\n\n");
|
|
763
|
+
} catch {
|
|
764
|
+
return opts.workPrompt;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
var DEAD_REPORTS_PATH = join(BRIDGE_DIR, "dead-reports.json");
|
|
768
|
+
var MAX_ATTEMPTS = 20;
|
|
769
|
+
function isPermanentClientError(err) {
|
|
770
|
+
if (!(err instanceof ApiError)) return false;
|
|
771
|
+
const { status } = err;
|
|
772
|
+
if (status === null) return false;
|
|
773
|
+
if (status < 400 || status >= 500) return false;
|
|
774
|
+
if (status === 408 || status === 429) return false;
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
function normalizeEntry(raw) {
|
|
778
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
779
|
+
logWarn("[report-queue] Dropping unrecognised queue entry (not an object)");
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
const r = raw;
|
|
783
|
+
if (typeof r["runId"] !== "string" || !r["runId"]) {
|
|
784
|
+
logWarn("[report-queue] Dropping queue entry missing runId");
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
const runId = r["runId"];
|
|
788
|
+
if (typeof r["status"] === "string" && (r["report"] === void 0 || r["report"] === null)) {
|
|
789
|
+
logWarn(
|
|
790
|
+
`[report-queue] Normalising legacy flat RunReport for run ${runId} (pre-WS1 daemon)`
|
|
791
|
+
);
|
|
792
|
+
return {
|
|
793
|
+
runId,
|
|
794
|
+
report: r,
|
|
795
|
+
attempts: 0,
|
|
796
|
+
firstFailedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (r["report"] === null || typeof r["report"] !== "object" || Array.isArray(r["report"])) {
|
|
800
|
+
logWarn(`[report-queue] Dropping queue entry for run ${runId}: invalid .report field`);
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
const rawAttempts = r["attempts"];
|
|
804
|
+
const attempts = Number.isFinite(rawAttempts) ? rawAttempts : 0;
|
|
805
|
+
const firstFailedAt = typeof r["firstFailedAt"] === "string" ? r["firstFailedAt"] : (/* @__PURE__ */ new Date()).toISOString();
|
|
806
|
+
return {
|
|
807
|
+
runId,
|
|
808
|
+
report: r["report"],
|
|
809
|
+
attempts,
|
|
810
|
+
firstFailedAt
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function readQueue(path) {
|
|
814
|
+
const raw = readJsonSafe(path);
|
|
815
|
+
if (!raw) return [];
|
|
816
|
+
const normalised = [];
|
|
817
|
+
for (const entry of raw) {
|
|
818
|
+
const item = normalizeEntry(entry);
|
|
819
|
+
if (item !== null) normalised.push(item);
|
|
820
|
+
}
|
|
821
|
+
return normalised;
|
|
822
|
+
}
|
|
823
|
+
function writeQueue(path, items) {
|
|
824
|
+
writeJsonAtomic(path, items);
|
|
825
|
+
}
|
|
826
|
+
function removePendingById(runId, path) {
|
|
827
|
+
const items = readQueue(path);
|
|
828
|
+
const filtered = items.filter((r) => r.runId !== runId);
|
|
829
|
+
if (filtered.length !== items.length) {
|
|
830
|
+
writeQueue(path, filtered);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function updatePending(item, path) {
|
|
834
|
+
const items = readQueue(path);
|
|
835
|
+
const updated = items.map((r) => r.runId === item.runId ? item : r);
|
|
836
|
+
writeQueue(path, updated);
|
|
837
|
+
}
|
|
838
|
+
function deadLetter(item, reason) {
|
|
839
|
+
logError(
|
|
840
|
+
`[report-queue] Dead-lettering run ${item.runId} (${reason}, attempts=${item.attempts})`
|
|
841
|
+
);
|
|
842
|
+
const existing = readQueue(DEAD_REPORTS_PATH);
|
|
843
|
+
const deduped = existing.filter((r) => r.runId !== item.runId);
|
|
844
|
+
deduped.push(item);
|
|
845
|
+
writeQueue(DEAD_REPORTS_PATH, deduped);
|
|
846
|
+
}
|
|
847
|
+
function enqueue(item) {
|
|
848
|
+
const existing = readQueue(PENDING_REPORTS_PATH);
|
|
849
|
+
const deduped = existing.filter((r) => r.runId !== item.runId);
|
|
850
|
+
deduped.push(item);
|
|
851
|
+
writeQueue(PENDING_REPORTS_PATH, deduped);
|
|
852
|
+
}
|
|
853
|
+
function removePendingReport(runId) {
|
|
854
|
+
removePendingById(runId, PENDING_REPORTS_PATH);
|
|
855
|
+
}
|
|
856
|
+
async function drain(send) {
|
|
857
|
+
const items = readQueue(PENDING_REPORTS_PATH);
|
|
858
|
+
if (items.length === 0) return;
|
|
859
|
+
for (const item of items) {
|
|
860
|
+
try {
|
|
861
|
+
await send(item);
|
|
862
|
+
removePendingById(item.runId, PENDING_REPORTS_PATH);
|
|
863
|
+
} catch (err) {
|
|
864
|
+
if (isPermanentClientError(err)) {
|
|
865
|
+
removePendingById(item.runId, PENDING_REPORTS_PATH);
|
|
866
|
+
deadLetter(item, `permanent ${err.status}`);
|
|
867
|
+
} else {
|
|
868
|
+
const updated = { ...item, attempts: item.attempts + 1 };
|
|
869
|
+
if (updated.attempts >= MAX_ATTEMPTS) {
|
|
870
|
+
removePendingById(item.runId, PENDING_REPORTS_PATH);
|
|
871
|
+
deadLetter(updated, "max_attempts_exceeded");
|
|
872
|
+
} else {
|
|
873
|
+
updatePending(updated, PENDING_REPORTS_PATH);
|
|
874
|
+
logWarn(
|
|
875
|
+
`[report-queue] Transient failure for run ${item.runId} (attempt ${updated.attempts}/${MAX_ATTEMPTS})`
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
553
883
|
// src/runner.ts
|
|
554
884
|
var RUN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
555
885
|
var STDOUT_CAP = 1e4;
|
|
@@ -562,6 +892,12 @@ function resolveClaudeCommand() {
|
|
|
562
892
|
if (existsSync(nativeExe)) return { cmd: nativeExe, shell: false };
|
|
563
893
|
return { cmd: "claude", shell: true };
|
|
564
894
|
}
|
|
895
|
+
function shellEscapeArgs(args) {
|
|
896
|
+
return args.map((a) => /[\s"^&|<>%]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a);
|
|
897
|
+
}
|
|
898
|
+
function isValidModelSlug(model) {
|
|
899
|
+
return /^[A-Za-z0-9._:+/-]+$/.test(model);
|
|
900
|
+
}
|
|
565
901
|
function parseClaudeOutput(raw) {
|
|
566
902
|
const trimmed = raw.trim();
|
|
567
903
|
const lastBrace = trimmed.lastIndexOf("}");
|
|
@@ -580,45 +916,71 @@ function parseClaudeOutput(raw) {
|
|
|
580
916
|
}
|
|
581
917
|
if (start === -1) return {};
|
|
582
918
|
try {
|
|
583
|
-
const
|
|
584
|
-
const
|
|
585
|
-
if (typeof
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (typeof
|
|
589
|
-
|
|
919
|
+
const p = JSON.parse(trimmed.slice(start, lastBrace + 1));
|
|
920
|
+
const r = {};
|
|
921
|
+
if (typeof p["total_cost_usd"] === "number") r.costUsd = p["total_cost_usd"];
|
|
922
|
+
if (typeof p["session_id"] === "string") r.sessionId = p["session_id"];
|
|
923
|
+
if (typeof p["is_error"] === "boolean") r.isError = p["is_error"];
|
|
924
|
+
if (typeof p["result"] === "string") r.resultText = p["result"];
|
|
925
|
+
if (typeof p["api_error_status"] === "number") r.apiErrorStatus = p["api_error_status"];
|
|
926
|
+
const u = p["usage"];
|
|
927
|
+
if (u && typeof u === "object") {
|
|
928
|
+
const uo = u;
|
|
929
|
+
if (typeof uo["input_tokens"] === "number") r.tokensInput = uo["input_tokens"];
|
|
930
|
+
if (typeof uo["output_tokens"] === "number") r.tokensOutput = uo["output_tokens"];
|
|
590
931
|
}
|
|
591
|
-
|
|
592
|
-
result.isError = parsed["is_error"];
|
|
593
|
-
}
|
|
594
|
-
if (typeof parsed["result"] === "string") {
|
|
595
|
-
result.resultText = parsed["result"];
|
|
596
|
-
}
|
|
597
|
-
if (typeof parsed["api_error_status"] === "number") {
|
|
598
|
-
result.apiErrorStatus = parsed["api_error_status"];
|
|
599
|
-
}
|
|
600
|
-
const usage = parsed["usage"];
|
|
601
|
-
if (usage && typeof usage === "object") {
|
|
602
|
-
const u = usage;
|
|
603
|
-
if (typeof u["input_tokens"] === "number") result.tokensInput = u["input_tokens"];
|
|
604
|
-
if (typeof u["output_tokens"] === "number") result.tokensOutput = u["output_tokens"];
|
|
605
|
-
}
|
|
606
|
-
return result;
|
|
932
|
+
return r;
|
|
607
933
|
} catch {
|
|
608
934
|
return {};
|
|
609
935
|
}
|
|
610
936
|
}
|
|
611
937
|
function classifyFailure(stderr, resultText, apiErrorStatus) {
|
|
612
|
-
const
|
|
938
|
+
const h = `${stderr}
|
|
613
939
|
${resultText ?? ""}`.toLowerCase();
|
|
614
|
-
if (apiErrorStatus === 429 || apiErrorStatus === 529 || /rate.?limit|overloaded|too many requests|usage limit|quota|exhausted/.test(
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
if (/enoent|not recognized|command not found|login|authenticate|credentials|api key/.test(haystack)) {
|
|
940
|
+
if (apiErrorStatus === 429 || apiErrorStatus === 529 || /rate.?limit|overloaded|too many requests|usage limit|quota|exhausted/.test(h)) return "rate_limited";
|
|
941
|
+
if (/enoent|not recognized|command not found|login|authenticate|credentials|api key/.test(h))
|
|
618
942
|
return "environment";
|
|
619
|
-
}
|
|
620
943
|
return "error";
|
|
621
944
|
}
|
|
945
|
+
function resolveRuntime(agentRuntime) {
|
|
946
|
+
switch (agentRuntime) {
|
|
947
|
+
case "codex":
|
|
948
|
+
return "codex";
|
|
949
|
+
case "hermes":
|
|
950
|
+
return "hermes";
|
|
951
|
+
default:
|
|
952
|
+
return "claude-code";
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function parseCodexOutput(raw) {
|
|
956
|
+
const trimmed = raw.trim();
|
|
957
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
958
|
+
if (lastBrace === -1) return {};
|
|
959
|
+
let depth = 0;
|
|
960
|
+
let start = -1;
|
|
961
|
+
for (let i = lastBrace; i >= 0; i--) {
|
|
962
|
+
if (trimmed[i] === "}") depth++;
|
|
963
|
+
else if (trimmed[i] === "{") {
|
|
964
|
+
depth--;
|
|
965
|
+
if (depth === 0) {
|
|
966
|
+
start = i;
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (start === -1) return {};
|
|
972
|
+
try {
|
|
973
|
+
const p = JSON.parse(trimmed.slice(start, lastBrace + 1));
|
|
974
|
+
const r = {};
|
|
975
|
+
if (typeof p["result"] === "string") r.resultText = p["result"];
|
|
976
|
+
if (typeof p["output"] === "string") r.resultText = p["output"];
|
|
977
|
+
if (typeof p["cost"] === "number") r.costUsd = p["cost"];
|
|
978
|
+
if (p["error"] !== void 0) r.isError = true;
|
|
979
|
+
return r;
|
|
980
|
+
} catch {
|
|
981
|
+
return {};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
622
984
|
var HANDOFF_TIMEOUT_MS = 4 * 60 * 1e3;
|
|
623
985
|
async function runHandoffTurn(claude, agentWorkspace, settingsPath, model, oldSessionId) {
|
|
624
986
|
const rawArgs = [
|
|
@@ -634,7 +996,7 @@ async function runHandoffTurn(claude, agentWorkspace, settingsPath, model, oldSe
|
|
|
634
996
|
"--resume",
|
|
635
997
|
oldSessionId
|
|
636
998
|
];
|
|
637
|
-
const args = claude.shell ? rawArgs
|
|
999
|
+
const args = claude.shell ? shellEscapeArgs(rawArgs) : rawArgs;
|
|
638
1000
|
await new Promise((resolve) => {
|
|
639
1001
|
let child;
|
|
640
1002
|
try {
|
|
@@ -671,81 +1033,489 @@ async function runHandoffTurn(claude, agentWorkspace, settingsPath, model, oldSe
|
|
|
671
1033
|
});
|
|
672
1034
|
});
|
|
673
1035
|
}
|
|
1036
|
+
async function spawnMemorySync(agentWorkspace) {
|
|
1037
|
+
const scriptPath = join(agentWorkspace, ".claude", "memory-sync.mjs");
|
|
1038
|
+
if (!existsSync(scriptPath)) return;
|
|
1039
|
+
await new Promise((resolve) => {
|
|
1040
|
+
let child;
|
|
1041
|
+
try {
|
|
1042
|
+
child = spawn(process.execPath, [scriptPath], {
|
|
1043
|
+
cwd: agentWorkspace,
|
|
1044
|
+
env: { ...process.env },
|
|
1045
|
+
shell: false,
|
|
1046
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
1047
|
+
});
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
logWarn(`Memory sync spawn failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1050
|
+
resolve();
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
let stderrBuf = "";
|
|
1054
|
+
child.stderr?.on("data", (c) => {
|
|
1055
|
+
stderrBuf += c.toString("utf-8");
|
|
1056
|
+
});
|
|
1057
|
+
const timer = setTimeout(() => {
|
|
1058
|
+
logWarn("Memory sync timed out after 60s \u2014 killing");
|
|
1059
|
+
try {
|
|
1060
|
+
child.kill("SIGTERM");
|
|
1061
|
+
} catch {
|
|
1062
|
+
}
|
|
1063
|
+
}, 6e4);
|
|
1064
|
+
child.on("close", (code) => {
|
|
1065
|
+
clearTimeout(timer);
|
|
1066
|
+
if (code !== 0) logWarn(`Memory sync exited ${String(code)}${stderrBuf ? `: ${stderrBuf.slice(-500)}` : ""}`);
|
|
1067
|
+
resolve();
|
|
1068
|
+
});
|
|
1069
|
+
child.on("error", (err) => {
|
|
1070
|
+
clearTimeout(timer);
|
|
1071
|
+
logWarn(`Memory sync error: ${err.message}`);
|
|
1072
|
+
resolve();
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
function buildCodexSpawnConfig(model, allowedTools) {
|
|
1077
|
+
const useShell = process.platform === "win32";
|
|
1078
|
+
const rawArgs = ["exec", "--json"];
|
|
1079
|
+
if (model && model.trim().length > 0) {
|
|
1080
|
+
rawArgs.push("--model", model);
|
|
1081
|
+
}
|
|
1082
|
+
if (allowedTools.length > 0) {
|
|
1083
|
+
log(`codex runtime: --allowedTools [${allowedTools.join(",")}] not forwarded (no codex equivalent); use sandbox isolation for tool restriction`);
|
|
1084
|
+
}
|
|
1085
|
+
const args = useShell ? shellEscapeArgs(rawArgs) : rawArgs;
|
|
1086
|
+
return { cmd: "codex", args, shell: useShell, promptAsArg: false };
|
|
1087
|
+
}
|
|
1088
|
+
function buildHermesSpawnConfig(model, allowedTools) {
|
|
1089
|
+
const useShell = process.platform === "win32";
|
|
1090
|
+
const rawArgs = ["-z"];
|
|
1091
|
+
if (model && model.trim().length > 0) {
|
|
1092
|
+
rawArgs.push("--model", model);
|
|
1093
|
+
}
|
|
1094
|
+
if (allowedTools.length > 0) {
|
|
1095
|
+
const hermesToolsets = allowedTools.filter(
|
|
1096
|
+
(t) => /^(web|terminal|development|safe|memory|messaging|calendar|files|code|search)$/i.test(t)
|
|
1097
|
+
);
|
|
1098
|
+
if (hermesToolsets.length > 0) {
|
|
1099
|
+
rawArgs.push("--toolsets", hermesToolsets.join(","));
|
|
1100
|
+
} else {
|
|
1101
|
+
log(`hermes runtime: allowedTools [${allowedTools.join(",")}] not in hermes toolset format; skipping --toolsets`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const args = useShell ? shellEscapeArgs(rawArgs) : rawArgs;
|
|
1105
|
+
return { cmd: "hermes", args, shell: useShell, promptAsArg: false };
|
|
1106
|
+
}
|
|
674
1107
|
async function runWorkItem(config, work) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1108
|
+
if (!work.agent) {
|
|
1109
|
+
logWarn(`Run ${work.runId} has no agent \u2014 not an agent_run work item; skipping`);
|
|
1110
|
+
await sendReport(config, {
|
|
1111
|
+
runId: work.runId,
|
|
1112
|
+
wakeupId: work.wakeupId,
|
|
1113
|
+
status: "failed",
|
|
1114
|
+
failureKind: "config_error",
|
|
1115
|
+
stdoutExcerpt: "",
|
|
1116
|
+
stderrExcerpt: "",
|
|
1117
|
+
error: "Work item has no agent (wrong kind reached runWorkItem)",
|
|
1118
|
+
exitCode: null
|
|
1119
|
+
});
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const workAgent = work.agent;
|
|
1123
|
+
const agent = findAgent(workAgent.id);
|
|
1124
|
+
const model = agent?.model ?? workAgent.model;
|
|
1125
|
+
const agentWorkspace = agentWorkspaceDir(workAgent.id);
|
|
1126
|
+
const runtime = resolveRuntime(
|
|
1127
|
+
workAgent.agentRuntime ?? agent?.agentRuntime
|
|
1128
|
+
);
|
|
678
1129
|
let cwd = agentWorkspace;
|
|
679
1130
|
if (work.workingDir && existsSync(work.workingDir)) {
|
|
680
1131
|
cwd = work.workingDir;
|
|
681
1132
|
} else if (work.workingDir) {
|
|
682
|
-
logWarn(
|
|
683
|
-
`workingDir "${work.workingDir}" does not exist; falling back to agent workspace`
|
|
684
|
-
);
|
|
1133
|
+
logWarn(`workingDir "${work.workingDir}" does not exist; falling back to agent workspace`);
|
|
685
1134
|
}
|
|
686
|
-
log(`Starting run ${work.runId} (agent "${
|
|
1135
|
+
log(`Starting run ${work.runId} (agent "${workAgent.name}", runtime "${runtime}", model "${model}")`);
|
|
687
1136
|
log(` cwd: ${cwd}`);
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1137
|
+
const env = { ...process.env };
|
|
1138
|
+
if (runtime === "hermes") {
|
|
1139
|
+
env["HERMES_HOME"] = join(agentWorkspace, ".hermes");
|
|
1140
|
+
}
|
|
1141
|
+
const promptText = buildPromptText({
|
|
1142
|
+
workPrompt: work.prompt,
|
|
1143
|
+
agent,
|
|
1144
|
+
cwd,
|
|
1145
|
+
agentWorkspace,
|
|
1146
|
+
rotated: false
|
|
1147
|
+
});
|
|
1148
|
+
let stdoutBuf = "", stderrBuf = "";
|
|
1149
|
+
let exitCode = null;
|
|
1150
|
+
let spawnError;
|
|
1151
|
+
if (runtime === "claude-code") {
|
|
1152
|
+
const claude = resolveClaudeCommand();
|
|
1153
|
+
const settingsPath = join(agentWorkspace, ".claude", "settings.json").replace(/\\/g, "/");
|
|
1154
|
+
const rawArgs = [
|
|
1155
|
+
"-p",
|
|
1156
|
+
"--output-format",
|
|
1157
|
+
"json",
|
|
1158
|
+
"--max-turns",
|
|
1159
|
+
"25",
|
|
1160
|
+
"--model",
|
|
1161
|
+
model,
|
|
1162
|
+
"--settings",
|
|
1163
|
+
settingsPath
|
|
1164
|
+
];
|
|
1165
|
+
if (Array.isArray(workAgent.allowedTools) && workAgent.allowedTools.length > 0) {
|
|
1166
|
+
rawArgs.push("--allowedTools", workAgent.allowedTools.join(","));
|
|
1167
|
+
}
|
|
1168
|
+
const persistent = workAgent.localSessionMode === "persistent";
|
|
1169
|
+
let sessionState = loadSessionState(workAgent.id);
|
|
1170
|
+
let rotated = false;
|
|
1171
|
+
if (persistent) {
|
|
1172
|
+
if (shouldRotateSession(sessionState, Date.now())) {
|
|
1173
|
+
if (sessionState.sessionId) {
|
|
1174
|
+
log(`Rotating session for agent "${workAgent.name}" (age/run limit reached)`);
|
|
1175
|
+
await runHandoffTurn(claude, agentWorkspace, settingsPath, model, sessionState.sessionId);
|
|
1176
|
+
rotated = true;
|
|
1177
|
+
}
|
|
1178
|
+
sessionState = { sessionId: null, sessionStartedAt: null, runCount: 0 };
|
|
1179
|
+
} else if (sessionState.sessionId) {
|
|
1180
|
+
rawArgs.push("--resume", sessionState.sessionId);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
const claudePromptText = buildPromptText({
|
|
1184
|
+
workPrompt: work.prompt,
|
|
1185
|
+
agent,
|
|
1186
|
+
cwd,
|
|
1187
|
+
agentWorkspace,
|
|
1188
|
+
rotated
|
|
1189
|
+
});
|
|
1190
|
+
const args = claude.shell ? shellEscapeArgs(rawArgs) : rawArgs;
|
|
1191
|
+
await new Promise((resolve) => {
|
|
1192
|
+
let child;
|
|
1193
|
+
try {
|
|
1194
|
+
child = spawn(claude.cmd, args, {
|
|
1195
|
+
cwd,
|
|
1196
|
+
env,
|
|
1197
|
+
shell: claude.shell,
|
|
1198
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1199
|
+
});
|
|
1200
|
+
child.stdin?.write(claudePromptText);
|
|
1201
|
+
child.stdin?.end();
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
spawnError = err instanceof Error ? err.message : String(err);
|
|
1204
|
+
resolve();
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const timeoutHandle = setTimeout(() => {
|
|
1208
|
+
logWarn(`Run ${work.runId} exceeded timeout \u2014 sending SIGTERM`);
|
|
1209
|
+
try {
|
|
1210
|
+
child.kill("SIGTERM");
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
setTimeout(() => {
|
|
1214
|
+
try {
|
|
1215
|
+
child.kill("SIGKILL");
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1218
|
+
}, 5e3);
|
|
1219
|
+
}, RUN_TIMEOUT_MS);
|
|
1220
|
+
child.stdout?.on("data", (c) => {
|
|
1221
|
+
stdoutBuf += c.toString("utf-8");
|
|
1222
|
+
});
|
|
1223
|
+
child.stderr?.on("data", (c) => {
|
|
1224
|
+
stderrBuf += c.toString("utf-8");
|
|
1225
|
+
});
|
|
1226
|
+
child.on("close", (code) => {
|
|
1227
|
+
clearTimeout(timeoutHandle);
|
|
1228
|
+
exitCode = code;
|
|
1229
|
+
resolve();
|
|
1230
|
+
});
|
|
1231
|
+
child.on("error", (err) => {
|
|
1232
|
+
clearTimeout(timeoutHandle);
|
|
1233
|
+
spawnError = err.message;
|
|
1234
|
+
resolve();
|
|
1235
|
+
});
|
|
1236
|
+
});
|
|
1237
|
+
const stdoutExcerpt = stdoutBuf.length > STDOUT_CAP ? stdoutBuf.slice(-STDOUT_CAP) : stdoutBuf;
|
|
1238
|
+
const stderrExcerpt = stderrBuf.length > STDERR_CAP ? stderrBuf.slice(-STDERR_CAP) : stderrBuf;
|
|
1239
|
+
const parsed = parseClaudeOutput(stdoutBuf);
|
|
1240
|
+
const succeeded = spawnError === void 0 && exitCode === 0 && parsed.isError !== true;
|
|
1241
|
+
const errorMessage = succeeded ? void 0 : spawnError ?? (parsed.isError ? parsed.resultText?.slice(0, 1e3) : void 0) ?? (stderrExcerpt.trim() ? stderrExcerpt.trim().slice(-1e3) : void 0) ?? `claude exited with code ${exitCode ?? "unknown"}`;
|
|
1242
|
+
const failureKind = succeeded ? void 0 : classifyFailure(stderrExcerpt, parsed.resultText, parsed.apiErrorStatus);
|
|
1243
|
+
const { isError: _ie, resultText: _rt, apiErrorStatus: _aes, ...reportFields } = parsed;
|
|
1244
|
+
const report = {
|
|
1245
|
+
runId: work.runId,
|
|
1246
|
+
wakeupId: work.wakeupId,
|
|
1247
|
+
status: succeeded ? "completed" : "failed",
|
|
1248
|
+
failureKind,
|
|
1249
|
+
stdoutExcerpt,
|
|
1250
|
+
stderrExcerpt,
|
|
1251
|
+
error: errorMessage,
|
|
1252
|
+
exitCode,
|
|
1253
|
+
...reportFields
|
|
1254
|
+
};
|
|
1255
|
+
log(`Run ${work.runId} ${report.status} (exit ${exitCode ?? "n/a"}${parsed.costUsd !== void 0 ? `, cost $${parsed.costUsd.toFixed(4)}` : ""})`);
|
|
1256
|
+
if (persistent && report.status === "completed") {
|
|
1257
|
+
recordRunSession(workAgent.id, parsed.sessionId, sessionState, Date.now());
|
|
1258
|
+
}
|
|
1259
|
+
await sendReport(config, report);
|
|
1260
|
+
if (report.status === "completed") {
|
|
1261
|
+
await spawnMemorySync(agentWorkspace);
|
|
1262
|
+
}
|
|
1263
|
+
} else if (runtime === "codex") {
|
|
1264
|
+
if (!isValidModelSlug(model)) {
|
|
1265
|
+
logWarn(`Run ${work.runId} aborted \u2014 model slug failed validation: "${model}"`);
|
|
1266
|
+
const report2 = {
|
|
1267
|
+
runId: work.runId,
|
|
1268
|
+
wakeupId: work.wakeupId,
|
|
1269
|
+
status: "failed",
|
|
1270
|
+
failureKind: "config_error",
|
|
1271
|
+
stdoutExcerpt: "",
|
|
1272
|
+
stderrExcerpt: "",
|
|
1273
|
+
error: `Invalid model slug: "${model}" \u2014 must match ^[A-Za-z0-9._:+/-]+$`,
|
|
1274
|
+
exitCode: null
|
|
1275
|
+
};
|
|
1276
|
+
await sendReport(config, report2);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const spawnCfg = buildCodexSpawnConfig(model, workAgent.allowedTools ?? []);
|
|
1280
|
+
const args = spawnCfg.args;
|
|
1281
|
+
await new Promise((resolve) => {
|
|
1282
|
+
let child;
|
|
1283
|
+
try {
|
|
1284
|
+
child = spawn(spawnCfg.cmd, args, {
|
|
1285
|
+
cwd,
|
|
1286
|
+
env,
|
|
1287
|
+
shell: spawnCfg.shell,
|
|
1288
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1289
|
+
});
|
|
1290
|
+
if (!spawnCfg.promptAsArg) {
|
|
1291
|
+
child.stdin?.write(promptText);
|
|
1292
|
+
}
|
|
1293
|
+
child.stdin?.end();
|
|
1294
|
+
} catch (err) {
|
|
1295
|
+
spawnError = err instanceof Error ? err.message : String(err);
|
|
1296
|
+
resolve();
|
|
1297
|
+
return;
|
|
710
1298
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1299
|
+
const timeoutHandle = setTimeout(() => {
|
|
1300
|
+
logWarn(`Run ${work.runId} exceeded timeout \u2014 sending SIGTERM`);
|
|
1301
|
+
try {
|
|
1302
|
+
child.kill("SIGTERM");
|
|
1303
|
+
} catch {
|
|
1304
|
+
}
|
|
1305
|
+
setTimeout(() => {
|
|
1306
|
+
try {
|
|
1307
|
+
child.kill("SIGKILL");
|
|
1308
|
+
} catch {
|
|
1309
|
+
}
|
|
1310
|
+
}, 5e3);
|
|
1311
|
+
}, RUN_TIMEOUT_MS);
|
|
1312
|
+
child.stdout?.on("data", (c) => {
|
|
1313
|
+
stdoutBuf += c.toString("utf-8");
|
|
1314
|
+
});
|
|
1315
|
+
child.stderr?.on("data", (c) => {
|
|
1316
|
+
stderrBuf += c.toString("utf-8");
|
|
1317
|
+
});
|
|
1318
|
+
child.on("close", (code) => {
|
|
1319
|
+
clearTimeout(timeoutHandle);
|
|
1320
|
+
exitCode = code;
|
|
1321
|
+
resolve();
|
|
1322
|
+
});
|
|
1323
|
+
child.on("error", (err) => {
|
|
1324
|
+
clearTimeout(timeoutHandle);
|
|
1325
|
+
spawnError = err.message;
|
|
1326
|
+
resolve();
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
const stdoutExcerpt = stdoutBuf.length > STDOUT_CAP ? stdoutBuf.slice(-STDOUT_CAP) : stdoutBuf;
|
|
1330
|
+
const stderrExcerpt = stderrBuf.length > STDERR_CAP ? stderrBuf.slice(-STDERR_CAP) : stderrBuf;
|
|
1331
|
+
const parsed = parseCodexOutput(stdoutBuf);
|
|
1332
|
+
const succeeded = spawnError === void 0 && exitCode === 0 && parsed.isError !== true;
|
|
1333
|
+
const errorMessage = succeeded ? void 0 : spawnError ?? (stderrExcerpt.trim() ? stderrExcerpt.trim().slice(-1e3) : void 0) ?? `codex exited with code ${exitCode ?? "unknown"}`;
|
|
1334
|
+
const failureKind = succeeded ? void 0 : classifyFailure(stderrExcerpt, parsed.resultText ?? stdoutBuf.slice(0, 500), void 0);
|
|
1335
|
+
const report = {
|
|
1336
|
+
runId: work.runId,
|
|
1337
|
+
wakeupId: work.wakeupId,
|
|
1338
|
+
status: succeeded ? "completed" : "failed",
|
|
1339
|
+
failureKind,
|
|
1340
|
+
stdoutExcerpt,
|
|
1341
|
+
stderrExcerpt,
|
|
1342
|
+
error: errorMessage,
|
|
1343
|
+
exitCode,
|
|
1344
|
+
costUsd: parsed.costUsd
|
|
1345
|
+
};
|
|
1346
|
+
log(`Run ${work.runId} ${report.status} \u2014 codex (exit ${exitCode ?? "n/a"}${parsed.costUsd !== void 0 ? `, cost $${parsed.costUsd.toFixed(4)}` : ""})`);
|
|
1347
|
+
await sendReport(config, report);
|
|
1348
|
+
if (report.status === "completed") {
|
|
1349
|
+
await spawnMemorySync(agentWorkspace);
|
|
1350
|
+
}
|
|
1351
|
+
} else {
|
|
1352
|
+
log(`Hermes runtime: HERMES_YOLO_MODE is forced on by -z \u2014 ensure this bridge machine is sandboxed/isolated`);
|
|
1353
|
+
log(` HERMES_HOME: ${env["HERMES_HOME"] ?? "(default)"}`);
|
|
1354
|
+
if (!isValidModelSlug(model)) {
|
|
1355
|
+
logWarn(`Run ${work.runId} aborted \u2014 model slug failed validation: "${model}"`);
|
|
1356
|
+
const report2 = {
|
|
1357
|
+
runId: work.runId,
|
|
1358
|
+
wakeupId: work.wakeupId,
|
|
1359
|
+
status: "failed",
|
|
1360
|
+
failureKind: "config_error",
|
|
1361
|
+
stdoutExcerpt: "",
|
|
1362
|
+
stderrExcerpt: "",
|
|
1363
|
+
error: `Invalid model slug: "${model}" \u2014 must match ^[A-Za-z0-9._:+/-]+$`,
|
|
1364
|
+
exitCode: null
|
|
1365
|
+
};
|
|
1366
|
+
await sendReport(config, report2);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const spawnCfg = buildHermesSpawnConfig(model, workAgent.allowedTools ?? []);
|
|
1370
|
+
const args = spawnCfg.args;
|
|
1371
|
+
await new Promise((resolve) => {
|
|
1372
|
+
let child;
|
|
1373
|
+
try {
|
|
1374
|
+
child = spawn(spawnCfg.cmd, args, {
|
|
1375
|
+
cwd,
|
|
1376
|
+
env,
|
|
1377
|
+
shell: spawnCfg.shell,
|
|
1378
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1379
|
+
});
|
|
1380
|
+
child.stdin?.write(promptText);
|
|
1381
|
+
child.stdin?.end();
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
spawnError = err instanceof Error ? err.message : String(err);
|
|
1384
|
+
resolve();
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const timeoutHandle = setTimeout(() => {
|
|
1388
|
+
logWarn(`Run ${work.runId} exceeded timeout \u2014 sending SIGTERM`);
|
|
1389
|
+
try {
|
|
1390
|
+
child.kill("SIGTERM");
|
|
1391
|
+
} catch {
|
|
1392
|
+
}
|
|
1393
|
+
setTimeout(() => {
|
|
1394
|
+
try {
|
|
1395
|
+
child.kill("SIGKILL");
|
|
1396
|
+
} catch {
|
|
1397
|
+
}
|
|
1398
|
+
}, 5e3);
|
|
1399
|
+
}, RUN_TIMEOUT_MS);
|
|
1400
|
+
child.stdout?.on("data", (c) => {
|
|
1401
|
+
stdoutBuf += c.toString("utf-8");
|
|
1402
|
+
});
|
|
1403
|
+
child.stderr?.on("data", (c) => {
|
|
1404
|
+
stderrBuf += c.toString("utf-8");
|
|
1405
|
+
});
|
|
1406
|
+
child.on("close", (code) => {
|
|
1407
|
+
clearTimeout(timeoutHandle);
|
|
1408
|
+
exitCode = code;
|
|
1409
|
+
resolve();
|
|
1410
|
+
});
|
|
1411
|
+
child.on("error", (err) => {
|
|
1412
|
+
clearTimeout(timeoutHandle);
|
|
1413
|
+
spawnError = err.message;
|
|
1414
|
+
resolve();
|
|
1415
|
+
});
|
|
1416
|
+
});
|
|
1417
|
+
const stdoutExcerpt = stdoutBuf.length > STDOUT_CAP ? stdoutBuf.slice(-STDOUT_CAP) : stdoutBuf;
|
|
1418
|
+
const stderrExcerpt = stderrBuf.length > STDERR_CAP ? stderrBuf.slice(-STDERR_CAP) : stderrBuf;
|
|
1419
|
+
const succeeded = spawnError === void 0 && exitCode === 0;
|
|
1420
|
+
const errorMessage = succeeded ? void 0 : spawnError ?? (stderrExcerpt.trim() ? stderrExcerpt.trim().slice(-1e3) : void 0) ?? `hermes exited with code ${exitCode ?? "unknown"}`;
|
|
1421
|
+
const failureKind = succeeded ? void 0 : classifyFailure(stderrExcerpt, stdoutBuf.slice(0, 500), void 0);
|
|
1422
|
+
const report = {
|
|
1423
|
+
runId: work.runId,
|
|
1424
|
+
wakeupId: work.wakeupId,
|
|
1425
|
+
status: succeeded ? "completed" : "failed",
|
|
1426
|
+
failureKind,
|
|
1427
|
+
stdoutExcerpt,
|
|
1428
|
+
stderrExcerpt,
|
|
1429
|
+
error: errorMessage,
|
|
1430
|
+
exitCode
|
|
1431
|
+
// No costUsd / tokensInput / tokensOutput from hermes -z (FLAG above).
|
|
1432
|
+
};
|
|
1433
|
+
log(`Run ${work.runId} ${report.status} \u2014 hermes (exit ${exitCode ?? "n/a"})`);
|
|
1434
|
+
await sendReport(config, report);
|
|
1435
|
+
if (report.status === "completed") {
|
|
1436
|
+
await spawnMemorySync(agentWorkspace);
|
|
714
1437
|
}
|
|
715
1438
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
${
|
|
723
|
-
|
|
1439
|
+
}
|
|
1440
|
+
async function sendReport(config, report) {
|
|
1441
|
+
const path = `/api/bridge/v1/runs/${report.runId}`;
|
|
1442
|
+
for (let attempt = 1; attempt <= REPORT_RETRY_COUNT; attempt++) {
|
|
1443
|
+
try {
|
|
1444
|
+
await post(config, path, report);
|
|
1445
|
+
log(`Reported run ${report.runId} (attempt ${attempt})`);
|
|
1446
|
+
removePendingReport(report.runId);
|
|
1447
|
+
return;
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
logError(`Failed to report run ${report.runId} (attempt ${attempt})`, err);
|
|
1450
|
+
if (attempt < REPORT_RETRY_COUNT) await sleep(REPORT_RETRY_BASE_MS * Math.pow(2, attempt - 1));
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
logWarn(`Persisting failed report for run ${report.runId} to pending-reports.json`);
|
|
1454
|
+
enqueue({ runId: report.runId, report, attempts: REPORT_RETRY_COUNT, firstFailedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1455
|
+
}
|
|
1456
|
+
async function retryPendingReports(config) {
|
|
1457
|
+
await drain(async (item) => {
|
|
1458
|
+
const path = `/api/bridge/v1/runs/${item.report.runId}`;
|
|
1459
|
+
await post(config, path, item.report);
|
|
1460
|
+
log(`Reported run ${item.report.runId} (from queue)`);
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
function sleep(ms) {
|
|
1464
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1465
|
+
}
|
|
1466
|
+
var STDOUT_CAP2 = 1e4;
|
|
1467
|
+
var STDERR_CAP2 = 4e3;
|
|
1468
|
+
var DEFAULT_EXEC_TIMEOUT_MS = 3e4;
|
|
1469
|
+
var MAX_EXEC_TIMEOUT_MS = 6e4;
|
|
1470
|
+
var KILL_GRACE_MS = 5e3;
|
|
1471
|
+
function resolveExecTimeout(timeoutMs) {
|
|
1472
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1473
|
+
return DEFAULT_EXEC_TIMEOUT_MS;
|
|
1474
|
+
}
|
|
1475
|
+
return Math.min(timeoutMs, MAX_EXEC_TIMEOUT_MS);
|
|
1476
|
+
}
|
|
1477
|
+
function capExcerpt(buf, cap) {
|
|
1478
|
+
return buf.length > cap ? buf.slice(-cap) : buf;
|
|
1479
|
+
}
|
|
1480
|
+
function classifyExec(opts) {
|
|
1481
|
+
if (opts.timedOut) return { exitCode: opts.exitCode, status: "timeout" };
|
|
1482
|
+
if (opts.spawnError !== void 0) return { exitCode: opts.exitCode, status: "failed" };
|
|
1483
|
+
if (opts.exitCode === 0) return { exitCode: 0, status: "completed" };
|
|
1484
|
+
return { exitCode: opts.exitCode, status: "failed" };
|
|
1485
|
+
}
|
|
1486
|
+
function resolveExecShell(command) {
|
|
1487
|
+
if (process.platform === "win32") {
|
|
1488
|
+
return { cmd: process.env["ComSpec"] ?? "cmd.exe", args: ["/d", "/s", "/c", command], shell: false };
|
|
1489
|
+
}
|
|
1490
|
+
const sh = process.env["SHELL"] ?? "/bin/sh";
|
|
1491
|
+
return { cmd: sh, args: ["-c", command], shell: false };
|
|
1492
|
+
}
|
|
1493
|
+
async function execShellCommand(command, options = {}) {
|
|
1494
|
+
const timeoutMs = resolveExecTimeout(options.timeoutMs);
|
|
1495
|
+
const { cmd, args, shell } = resolveExecShell(command);
|
|
1496
|
+
const spawnCwd = options.cwd && options.cwd.length > 0 ? options.cwd : void 0;
|
|
724
1497
|
let stdoutBuf = "";
|
|
725
1498
|
let stderrBuf = "";
|
|
726
1499
|
let exitCode = null;
|
|
727
1500
|
let spawnError;
|
|
1501
|
+
let timedOut = false;
|
|
728
1502
|
await new Promise((resolve) => {
|
|
729
1503
|
let child;
|
|
730
1504
|
try {
|
|
731
|
-
child = spawn(
|
|
732
|
-
cwd,
|
|
733
|
-
env,
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
// stdin: pipe — the prompt is delivered via stdin, not argv.
|
|
738
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1505
|
+
child = spawn(cmd, args, {
|
|
1506
|
+
cwd: spawnCwd,
|
|
1507
|
+
env: { ...process.env },
|
|
1508
|
+
shell,
|
|
1509
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1510
|
+
windowsHide: true
|
|
739
1511
|
});
|
|
740
|
-
child.stdin?.write(promptText);
|
|
741
|
-
child.stdin?.end();
|
|
742
1512
|
} catch (err) {
|
|
743
1513
|
spawnError = err instanceof Error ? err.message : String(err);
|
|
744
1514
|
resolve();
|
|
745
1515
|
return;
|
|
746
1516
|
}
|
|
747
1517
|
const timeoutHandle = setTimeout(() => {
|
|
748
|
-
|
|
1518
|
+
timedOut = true;
|
|
749
1519
|
try {
|
|
750
1520
|
child.kill("SIGTERM");
|
|
751
1521
|
} catch {
|
|
@@ -755,13 +1525,13 @@ ${work.prompt}` : work.prompt;
|
|
|
755
1525
|
child.kill("SIGKILL");
|
|
756
1526
|
} catch {
|
|
757
1527
|
}
|
|
758
|
-
},
|
|
759
|
-
},
|
|
760
|
-
child.stdout?.on("data", (
|
|
761
|
-
stdoutBuf +=
|
|
1528
|
+
}, KILL_GRACE_MS);
|
|
1529
|
+
}, timeoutMs);
|
|
1530
|
+
child.stdout?.on("data", (c) => {
|
|
1531
|
+
stdoutBuf += c.toString("utf-8");
|
|
762
1532
|
});
|
|
763
|
-
child.stderr?.on("data", (
|
|
764
|
-
stderrBuf +=
|
|
1533
|
+
child.stderr?.on("data", (c) => {
|
|
1534
|
+
stderrBuf += c.toString("utf-8");
|
|
765
1535
|
});
|
|
766
1536
|
child.on("close", (code) => {
|
|
767
1537
|
clearTimeout(timeoutHandle);
|
|
@@ -774,87 +1544,154 @@ ${work.prompt}` : work.prompt;
|
|
|
774
1544
|
resolve();
|
|
775
1545
|
});
|
|
776
1546
|
});
|
|
777
|
-
const stdoutExcerpt = stdoutBuf
|
|
778
|
-
const
|
|
779
|
-
const
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
wakeupId: work.wakeupId,
|
|
792
|
-
status: succeeded ? "completed" : "failed",
|
|
793
|
-
failureKind,
|
|
794
|
-
stdoutExcerpt,
|
|
795
|
-
stderrExcerpt,
|
|
796
|
-
error: errorMessage,
|
|
797
|
-
exitCode,
|
|
798
|
-
...reportFields
|
|
799
|
-
};
|
|
800
|
-
log(
|
|
801
|
-
`Run ${work.runId} ${report.status} (exit ${exitCode ?? "n/a"}${parsed.costUsd !== void 0 ? `, cost $${parsed.costUsd.toFixed(4)}` : ""})`
|
|
802
|
-
);
|
|
803
|
-
if (persistent && report.status === "completed") {
|
|
804
|
-
recordRunSession(work.agent.id, parsed.sessionId, sessionState, Date.now());
|
|
1547
|
+
const stdoutExcerpt = capExcerpt(stdoutBuf, STDOUT_CAP2);
|
|
1548
|
+
const stderrSource = spawnError !== void 0 && stderrBuf.length === 0 ? spawnError : stderrBuf;
|
|
1549
|
+
const stderrExcerpt = capExcerpt(stderrSource, STDERR_CAP2);
|
|
1550
|
+
const { exitCode: finalExit, status } = classifyExec({ exitCode, spawnError, timedOut });
|
|
1551
|
+
return { exitCode: finalExit, stdoutExcerpt, stderrExcerpt, status };
|
|
1552
|
+
}
|
|
1553
|
+
var DEFAULT_COLS = 120;
|
|
1554
|
+
var DEFAULT_ROWS = 30;
|
|
1555
|
+
function loadNodePty() {
|
|
1556
|
+
try {
|
|
1557
|
+
return __require("node-pty");
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
logWarn(`node-pty unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
1560
|
+
return null;
|
|
805
1561
|
}
|
|
806
|
-
await sendReport(config, report);
|
|
807
1562
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1563
|
+
function resolveShell(override) {
|
|
1564
|
+
if (override && override.length > 0) {
|
|
1565
|
+
return { shell: override, args: os.platform() === "win32" ? [] : ["--login"] };
|
|
1566
|
+
}
|
|
1567
|
+
if (os.platform() === "win32") {
|
|
1568
|
+
const gitBashPaths = [
|
|
1569
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
1570
|
+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe"
|
|
1571
|
+
];
|
|
1572
|
+
const bashPath = gitBashPaths.find((p) => existsSync(p));
|
|
1573
|
+
if (bashPath) return { shell: bashPath, args: ["--login", "-i"] };
|
|
1574
|
+
return { shell: "powershell.exe", args: ["-NoLogo"] };
|
|
1575
|
+
}
|
|
1576
|
+
return { shell: process.env["SHELL"] ?? "/bin/bash", args: ["--login"] };
|
|
1577
|
+
}
|
|
1578
|
+
function buildDaemonJoinUrl(relayWsUrl, joinToken) {
|
|
1579
|
+
const base = relayWsUrl.replace(/\/+$/, "");
|
|
1580
|
+
return `${base}/daemon?token=${encodeURIComponent(joinToken)}`;
|
|
1581
|
+
}
|
|
1582
|
+
async function openTerminalSession(opts) {
|
|
1583
|
+
const { WebSocket } = await import('ws');
|
|
1584
|
+
const url = buildDaemonJoinUrl(opts.relayWsUrl, opts.joinToken);
|
|
1585
|
+
const cols = opts.cols && opts.cols > 0 ? opts.cols : DEFAULT_COLS;
|
|
1586
|
+
const rows = opts.rows && opts.rows > 0 ? opts.rows : DEFAULT_ROWS;
|
|
1587
|
+
const ws = new WebSocket(url);
|
|
1588
|
+
let pty = null;
|
|
1589
|
+
let closed = false;
|
|
1590
|
+
const send = (frame) => {
|
|
1591
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1592
|
+
ws.send(JSON.stringify(frame));
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
const cleanup = (reason) => {
|
|
1596
|
+
if (closed) return;
|
|
1597
|
+
closed = true;
|
|
1598
|
+
if (pty) {
|
|
1599
|
+
try {
|
|
1600
|
+
pty.kill();
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
pty = null;
|
|
1604
|
+
}
|
|
811
1605
|
try {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1606
|
+
ws.close();
|
|
1607
|
+
} catch {
|
|
1608
|
+
}
|
|
1609
|
+
log(`[terminal] session ${opts.sessionId} closed (${reason})`);
|
|
1610
|
+
if (opts.onClosed) {
|
|
1611
|
+
try {
|
|
1612
|
+
opts.onClosed(opts.sessionId);
|
|
1613
|
+
} catch {
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
const handle = {
|
|
1618
|
+
sessionId: opts.sessionId,
|
|
1619
|
+
close: () => cleanup("handle.close")
|
|
1620
|
+
};
|
|
1621
|
+
ws.on("open", () => {
|
|
1622
|
+
send({ type: "auth", joinToken: opts.joinToken, sessionId: opts.sessionId });
|
|
1623
|
+
const nodePty = loadNodePty();
|
|
1624
|
+
if (!nodePty) {
|
|
1625
|
+
send({
|
|
1626
|
+
type: "output",
|
|
1627
|
+
data: "\r\n[stride-bridge] Interactive terminal unavailable on this machine: node-pty is not installed. Non-interactive commands (Processes, Services, Logs, Actions, Snippets) still work.\r\n"
|
|
1628
|
+
});
|
|
1629
|
+
send({ type: "error", message: "node-pty unavailable on daemon host" });
|
|
1630
|
+
cleanup("node-pty unavailable");
|
|
815
1631
|
return;
|
|
1632
|
+
}
|
|
1633
|
+
const { shell, args } = resolveShell(opts.shell);
|
|
1634
|
+
try {
|
|
1635
|
+
pty = nodePty.spawn(shell, args, {
|
|
1636
|
+
name: "xterm-256color",
|
|
1637
|
+
cols,
|
|
1638
|
+
rows,
|
|
1639
|
+
cwd: opts.cwd && opts.cwd.length > 0 ? opts.cwd : os.homedir(),
|
|
1640
|
+
env: process.env
|
|
1641
|
+
});
|
|
816
1642
|
} catch (err) {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1643
|
+
send({
|
|
1644
|
+
type: "error",
|
|
1645
|
+
message: `Failed to spawn shell: ${err instanceof Error ? err.message : String(err)}`
|
|
1646
|
+
});
|
|
1647
|
+
cleanup("pty spawn failed");
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
pty.onData((data) => {
|
|
1651
|
+
send({ type: "output", data });
|
|
1652
|
+
});
|
|
1653
|
+
pty.onExit(({ exitCode }) => {
|
|
1654
|
+
send({ type: "exit", code: exitCode });
|
|
1655
|
+
cleanup(`pty exit ${exitCode}`);
|
|
1656
|
+
});
|
|
1657
|
+
send({ type: "ready" });
|
|
1658
|
+
log(`[terminal] session ${opts.sessionId} ready (${shell})`);
|
|
1659
|
+
});
|
|
1660
|
+
ws.on("message", (raw) => {
|
|
1661
|
+
const text = Buffer.isBuffer(raw) ? raw.toString("utf-8") : Array.isArray(raw) ? Buffer.concat(raw).toString("utf-8") : Buffer.from(raw).toString("utf-8");
|
|
1662
|
+
let frame;
|
|
1663
|
+
try {
|
|
1664
|
+
frame = JSON.parse(text);
|
|
1665
|
+
} catch {
|
|
1666
|
+
if (pty) pty.write(text);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
if (frame.type === "input" && typeof frame.data === "string") {
|
|
1670
|
+
if (pty) pty.write(frame.data);
|
|
1671
|
+
} else if (frame.type === "resize" && typeof frame.cols === "number" && typeof frame.rows === "number") {
|
|
1672
|
+
if (pty) {
|
|
1673
|
+
try {
|
|
1674
|
+
pty.resize(frame.cols, frame.rows);
|
|
1675
|
+
} catch {
|
|
1676
|
+
}
|
|
821
1677
|
}
|
|
1678
|
+
} else if (frame.type === "ping") {
|
|
1679
|
+
send({ type: "pong" });
|
|
822
1680
|
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
log(`Retrying ${pending.length} pending report(s)`);
|
|
831
|
-
for (const report of pending) {
|
|
832
|
-
await sendReport(config, report);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
function appendPendingReport(report) {
|
|
836
|
-
const existing = readJsonSafe(PENDING_REPORTS_PATH) ?? [];
|
|
837
|
-
const deduped = existing.filter((r) => r.runId !== report.runId);
|
|
838
|
-
deduped.push(report);
|
|
839
|
-
writeJsonAtomic(PENDING_REPORTS_PATH, deduped);
|
|
840
|
-
}
|
|
841
|
-
function removePendingReport(runId) {
|
|
842
|
-
const existing = readJsonSafe(PENDING_REPORTS_PATH);
|
|
843
|
-
if (!existing || existing.length === 0) return;
|
|
844
|
-
const filtered = existing.filter((r) => r.runId !== runId);
|
|
845
|
-
if (filtered.length !== existing.length) {
|
|
846
|
-
writeJsonAtomic(PENDING_REPORTS_PATH, filtered);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
function sleep(ms) {
|
|
850
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1681
|
+
});
|
|
1682
|
+
ws.on("close", () => cleanup("ws close"));
|
|
1683
|
+
ws.on("error", (err) => {
|
|
1684
|
+
logWarn(`[terminal] session ${opts.sessionId} ws error: ${err.message}`);
|
|
1685
|
+
cleanup("ws error");
|
|
1686
|
+
});
|
|
1687
|
+
return handle;
|
|
851
1688
|
}
|
|
852
1689
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
853
|
-
var
|
|
1690
|
+
var CACHE_TTL_MS2 = 6e4;
|
|
854
1691
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
855
1692
|
function remainingPct(utilization) {
|
|
856
1693
|
if (typeof utilization !== "number" || !Number.isFinite(utilization)) return null;
|
|
857
|
-
return Math.max(0, Math.min(100, Math.round(
|
|
1694
|
+
return Math.max(0, Math.min(100, Math.round(100 - utilization)));
|
|
858
1695
|
}
|
|
859
1696
|
function parseUsageResponse(body, nowIso) {
|
|
860
1697
|
return {
|
|
@@ -870,10 +1707,10 @@ function readOauthToken() {
|
|
|
870
1707
|
);
|
|
871
1708
|
return creds?.claudeAiOauth?.accessToken ?? null;
|
|
872
1709
|
}
|
|
873
|
-
var
|
|
1710
|
+
var cached2 = null;
|
|
874
1711
|
var warnedNoToken = false;
|
|
875
1712
|
async function getQuotaSnapshot() {
|
|
876
|
-
if (
|
|
1713
|
+
if (cached2 && Date.now() - cached2.at < CACHE_TTL_MS2) return cached2.snapshot;
|
|
877
1714
|
const token = readOauthToken();
|
|
878
1715
|
if (!token) {
|
|
879
1716
|
if (!warnedNoToken) {
|
|
@@ -890,20 +1727,243 @@ async function getQuotaSnapshot() {
|
|
|
890
1727
|
signal: controller.signal
|
|
891
1728
|
});
|
|
892
1729
|
clearTimeout(timer);
|
|
893
|
-
if (!res.ok) return
|
|
1730
|
+
if (!res.ok) return cached2?.snapshot ?? null;
|
|
894
1731
|
const body = await res.json();
|
|
895
1732
|
const snapshot = parseUsageResponse(body, (/* @__PURE__ */ new Date()).toISOString());
|
|
896
|
-
|
|
1733
|
+
cached2 = { snapshot, at: Date.now() };
|
|
897
1734
|
return snapshot;
|
|
898
1735
|
} catch {
|
|
899
|
-
return
|
|
1736
|
+
return cached2?.snapshot ?? null;
|
|
900
1737
|
}
|
|
901
1738
|
}
|
|
1739
|
+
function readCpuTotals() {
|
|
1740
|
+
let idle = 0;
|
|
1741
|
+
let total = 0;
|
|
1742
|
+
for (const cpu of cpus()) {
|
|
1743
|
+
idle += cpu.times.idle;
|
|
1744
|
+
total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
|
|
1745
|
+
}
|
|
1746
|
+
return { idle, total };
|
|
1747
|
+
}
|
|
1748
|
+
function computeCpuPct(prev, curr) {
|
|
1749
|
+
const dTotal = curr.total - prev.total;
|
|
1750
|
+
if (dTotal <= 0) return void 0;
|
|
1751
|
+
const dIdle = curr.idle - prev.idle;
|
|
1752
|
+
const pct = 100 * (1 - dIdle / dTotal);
|
|
1753
|
+
return Math.round(Math.min(100, Math.max(0, pct)) * 10) / 10;
|
|
1754
|
+
}
|
|
1755
|
+
function parseMeminfo(content) {
|
|
1756
|
+
const totalMatch = content.match(/MemTotal:\s+(\d+)\s*kB/);
|
|
1757
|
+
const availMatch = content.match(/MemAvailable:\s+(\d+)\s*kB/);
|
|
1758
|
+
if (!totalMatch || !availMatch) return void 0;
|
|
1759
|
+
const totalMb = Math.round(Number(totalMatch[1]) / 1024);
|
|
1760
|
+
const availMb = Math.round(Number(availMatch[1]) / 1024);
|
|
1761
|
+
return { memUsedMb: totalMb - availMb, memTotalMb: totalMb };
|
|
1762
|
+
}
|
|
1763
|
+
function parseNetDev(content) {
|
|
1764
|
+
let rxBytes = 0;
|
|
1765
|
+
let txBytes = 0;
|
|
1766
|
+
for (const line of content.split("\n")) {
|
|
1767
|
+
const match = line.match(/^\s*([^:\s]+):\s*(\d+)(?:\s+\d+){7}\s+(\d+)/);
|
|
1768
|
+
if (!match || match[1] === "lo") continue;
|
|
1769
|
+
rxBytes += Number(match[2]);
|
|
1770
|
+
txBytes += Number(match[3]);
|
|
1771
|
+
}
|
|
1772
|
+
return { rxBytes, txBytes };
|
|
1773
|
+
}
|
|
1774
|
+
function computeNetKbps(prev, curr) {
|
|
1775
|
+
const seconds = (curr.at - prev.at) / 1e3;
|
|
1776
|
+
if (seconds <= 0) return void 0;
|
|
1777
|
+
const rx = (curr.rxBytes - prev.rxBytes) / 1024 / seconds;
|
|
1778
|
+
const tx = (curr.txBytes - prev.txBytes) / 1024 / seconds;
|
|
1779
|
+
if (rx < 0 || tx < 0) return void 0;
|
|
1780
|
+
return { netRxKbps: Math.round(rx * 10) / 10, netTxKbps: Math.round(tx * 10) / 10 };
|
|
1781
|
+
}
|
|
1782
|
+
var prevCpu = null;
|
|
1783
|
+
var prevNet = null;
|
|
1784
|
+
async function collectHostMetrics() {
|
|
1785
|
+
const metrics = {};
|
|
1786
|
+
try {
|
|
1787
|
+
const curr = readCpuTotals();
|
|
1788
|
+
if (prevCpu) {
|
|
1789
|
+
const pct = computeCpuPct(prevCpu, curr);
|
|
1790
|
+
if (pct !== void 0) metrics.cpuPct = pct;
|
|
1791
|
+
}
|
|
1792
|
+
prevCpu = curr;
|
|
1793
|
+
} catch {
|
|
1794
|
+
}
|
|
1795
|
+
try {
|
|
1796
|
+
const load = loadavg()[0];
|
|
1797
|
+
if (Number.isFinite(load)) metrics.load1 = Math.round(load * 100) / 100;
|
|
1798
|
+
} catch {
|
|
1799
|
+
}
|
|
1800
|
+
try {
|
|
1801
|
+
const meminfo = parseMeminfo(await readFile("/proc/meminfo", "utf8"));
|
|
1802
|
+
if (meminfo) Object.assign(metrics, meminfo);
|
|
1803
|
+
} catch {
|
|
1804
|
+
const totalMb = Math.round(totalmem() / 1024 / 1024);
|
|
1805
|
+
metrics.memTotalMb = totalMb;
|
|
1806
|
+
metrics.memUsedMb = totalMb - Math.round(freemem() / 1024 / 1024);
|
|
1807
|
+
}
|
|
1808
|
+
try {
|
|
1809
|
+
const root = platform() === "win32" ? "C:\\" : "/";
|
|
1810
|
+
const stats = await statfs(root);
|
|
1811
|
+
const totalGb = stats.blocks * stats.bsize / 1024 ** 3;
|
|
1812
|
+
const freeGb = stats.bfree * stats.bsize / 1024 ** 3;
|
|
1813
|
+
metrics.diskTotalGb = Math.round(totalGb * 10) / 10;
|
|
1814
|
+
metrics.diskUsedGb = Math.round((totalGb - freeGb) * 10) / 10;
|
|
1815
|
+
} catch {
|
|
1816
|
+
}
|
|
1817
|
+
try {
|
|
1818
|
+
const parsed = parseNetDev(await readFile("/proc/net/dev", "utf8"));
|
|
1819
|
+
const curr = { ...parsed, at: Date.now() };
|
|
1820
|
+
if (prevNet) {
|
|
1821
|
+
const kbps = computeNetKbps(prevNet, curr);
|
|
1822
|
+
if (kbps) Object.assign(metrics, kbps);
|
|
1823
|
+
}
|
|
1824
|
+
prevNet = curr;
|
|
1825
|
+
} catch {
|
|
1826
|
+
}
|
|
1827
|
+
return metrics;
|
|
1828
|
+
}
|
|
902
1829
|
|
|
903
1830
|
// src/poller.ts
|
|
904
1831
|
var POLL_INTERVAL_MS = 2500;
|
|
905
1832
|
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
906
1833
|
var BACKOFF_STEPS_MS = [2500, 5e3, 1e4, 3e4];
|
|
1834
|
+
var activeRunAgentId = null;
|
|
1835
|
+
var activeRunTask = void 0;
|
|
1836
|
+
var activeTerminalSessions = /* @__PURE__ */ new Map();
|
|
1837
|
+
var MAX_CONCURRENT_TERMINALS = 8;
|
|
1838
|
+
function classifyWorkItem(work) {
|
|
1839
|
+
switch (work.kind) {
|
|
1840
|
+
case "exec":
|
|
1841
|
+
return "exec";
|
|
1842
|
+
case "terminal_open":
|
|
1843
|
+
return "terminal_open";
|
|
1844
|
+
default:
|
|
1845
|
+
return "agent_run";
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
async function handleExecWork(config, work) {
|
|
1849
|
+
if (!work.execId || typeof work.command !== "string") {
|
|
1850
|
+
logWarn(`exec work item missing execId/command (run ${work.runId}); ignoring`);
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
const execId = work.execId;
|
|
1854
|
+
log(`Running exec ${execId}`);
|
|
1855
|
+
const result = await execShellCommand(work.command, {
|
|
1856
|
+
cwd: work.cwd,
|
|
1857
|
+
timeoutMs: work.timeoutMs
|
|
1858
|
+
});
|
|
1859
|
+
const payload = {
|
|
1860
|
+
execId,
|
|
1861
|
+
status: result.status,
|
|
1862
|
+
exitCode: result.exitCode,
|
|
1863
|
+
stdoutExcerpt: result.stdoutExcerpt,
|
|
1864
|
+
stderrExcerpt: result.stderrExcerpt
|
|
1865
|
+
};
|
|
1866
|
+
try {
|
|
1867
|
+
await post(config, `/api/bridge/v1/exec/${execId}`, payload);
|
|
1868
|
+
log(`Reported exec ${execId} (${result.status})`);
|
|
1869
|
+
} catch (err) {
|
|
1870
|
+
logError(`Failed to report exec ${execId}`, err);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
function dispatchTerminalWork(work) {
|
|
1874
|
+
if (!work.sessionId || !work.relayWsUrl || !work.joinToken) {
|
|
1875
|
+
logWarn(`terminal_open work item missing sessionId/relayWsUrl/joinToken (run ${work.runId}); ignoring`);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
const sessionId = work.sessionId;
|
|
1879
|
+
if (activeTerminalSessions.has(sessionId)) {
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (activeTerminalSessions.size >= MAX_CONCURRENT_TERMINALS) {
|
|
1883
|
+
logWarn(
|
|
1884
|
+
`terminal_open ignored \u2014 concurrency cap reached (${MAX_CONCURRENT_TERMINALS} active sessions)`
|
|
1885
|
+
);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const relayWsUrl = work.relayWsUrl;
|
|
1889
|
+
const joinToken = work.joinToken;
|
|
1890
|
+
const onClosed = (id) => {
|
|
1891
|
+
activeTerminalSessions.delete(id);
|
|
1892
|
+
};
|
|
1893
|
+
let realHandle = null;
|
|
1894
|
+
activeTerminalSessions.set(sessionId, {
|
|
1895
|
+
sessionId,
|
|
1896
|
+
close: () => {
|
|
1897
|
+
if (realHandle) realHandle.close();
|
|
1898
|
+
activeTerminalSessions.delete(sessionId);
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
log(`Opening terminal session ${sessionId} (detached)`);
|
|
1902
|
+
void openTerminalSession({
|
|
1903
|
+
relayWsUrl,
|
|
1904
|
+
joinToken,
|
|
1905
|
+
sessionId,
|
|
1906
|
+
cols: work.cols,
|
|
1907
|
+
rows: work.rows,
|
|
1908
|
+
shell: work.shell,
|
|
1909
|
+
cwd: work.cwd,
|
|
1910
|
+
onClosed
|
|
1911
|
+
}).then((handle) => {
|
|
1912
|
+
realHandle = handle;
|
|
1913
|
+
if (!activeTerminalSessions.has(sessionId)) {
|
|
1914
|
+
handle.close();
|
|
1915
|
+
}
|
|
1916
|
+
}).catch((err) => {
|
|
1917
|
+
logError(`Failed to open terminal session ${sessionId}`, err);
|
|
1918
|
+
activeTerminalSessions.delete(sessionId);
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
function closeAllTerminalSessions() {
|
|
1922
|
+
if (activeTerminalSessions.size === 0) return;
|
|
1923
|
+
log(`Closing ${activeTerminalSessions.size} terminal session(s)`);
|
|
1924
|
+
for (const handle of activeTerminalSessions.values()) {
|
|
1925
|
+
try {
|
|
1926
|
+
handle.close();
|
|
1927
|
+
} catch {
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
activeTerminalSessions.clear();
|
|
1931
|
+
}
|
|
1932
|
+
function buildEventPayload(opts) {
|
|
1933
|
+
const details = {
|
|
1934
|
+
daemonVersion: opts.daemonVersion,
|
|
1935
|
+
...opts.osInfo ? { osInfo: opts.osInfo } : {},
|
|
1936
|
+
...opts.extra ?? {}
|
|
1937
|
+
};
|
|
1938
|
+
const payload = {
|
|
1939
|
+
kind: opts.kind,
|
|
1940
|
+
message: opts.message,
|
|
1941
|
+
details
|
|
1942
|
+
};
|
|
1943
|
+
if (opts.agentId !== void 0) payload.agentId = opts.agentId;
|
|
1944
|
+
return payload;
|
|
1945
|
+
}
|
|
1946
|
+
function buildHeartbeatPayload(opts) {
|
|
1947
|
+
const agents = opts.agentIds.map((agentId) => {
|
|
1948
|
+
if (agentId === opts.activeAgentId) {
|
|
1949
|
+
const entry = { agentId, status: "running" };
|
|
1950
|
+
if (opts.activeTask) entry.currentTask = opts.activeTask;
|
|
1951
|
+
return entry;
|
|
1952
|
+
}
|
|
1953
|
+
return { agentId, status: "idle" };
|
|
1954
|
+
});
|
|
1955
|
+
const payload = {
|
|
1956
|
+
daemonVersion: opts.daemonVersion,
|
|
1957
|
+
osInfo: opts.osInfo,
|
|
1958
|
+
agents
|
|
1959
|
+
};
|
|
1960
|
+
if (opts.quota !== void 0) payload.quota = opts.quota;
|
|
1961
|
+
if (opts.runtimes !== void 0) payload.capabilities = opts.runtimes;
|
|
1962
|
+
if (opts.hostMetrics !== void 0 && Object.keys(opts.hostMetrics).length > 0) {
|
|
1963
|
+
payload.hostMetrics = opts.hostMetrics;
|
|
1964
|
+
}
|
|
1965
|
+
return payload;
|
|
1966
|
+
}
|
|
907
1967
|
var running = false;
|
|
908
1968
|
async function startDaemon(config) {
|
|
909
1969
|
running = true;
|
|
@@ -912,32 +1972,59 @@ async function startDaemon(config) {
|
|
|
912
1972
|
running = false;
|
|
913
1973
|
log(`Received ${signal} \u2014 shutting down gracefully`);
|
|
914
1974
|
stopAgentRefreshTimer();
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1975
|
+
closeAllTerminalSessions();
|
|
1976
|
+
await postBestEffort(
|
|
1977
|
+
config,
|
|
1978
|
+
"/api/bridge/v1/events",
|
|
1979
|
+
buildEventPayload({
|
|
1980
|
+
kind: "daemon_stopping",
|
|
1981
|
+
message: `Daemon stopping (${signal})`,
|
|
1982
|
+
daemonVersion: VERSION
|
|
1983
|
+
})
|
|
1984
|
+
);
|
|
920
1985
|
process.exit(0);
|
|
921
1986
|
};
|
|
922
1987
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
923
1988
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
924
|
-
await postBestEffort(
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1989
|
+
await postBestEffort(
|
|
1990
|
+
config,
|
|
1991
|
+
"/api/bridge/v1/events",
|
|
1992
|
+
buildEventPayload({
|
|
1993
|
+
kind: "daemon_started",
|
|
1994
|
+
message: "Daemon started",
|
|
1995
|
+
daemonVersion: VERSION,
|
|
1996
|
+
osInfo: { platform: platform(), release: release() }
|
|
1997
|
+
})
|
|
1998
|
+
);
|
|
930
1999
|
log(`Daemon started (v${VERSION}, bridge ${config.bridgeId})`);
|
|
931
2000
|
await refreshAgents(config);
|
|
932
2001
|
startAgentRefreshTimer(config);
|
|
933
2002
|
const sendHeartbeat = async () => {
|
|
934
|
-
const
|
|
2003
|
+
const cachedAgents2 = getCachedAgents();
|
|
2004
|
+
const agentIds = cachedAgents2.map((a) => a.id);
|
|
2005
|
+
const quota = await getQuotaSnapshot();
|
|
2006
|
+
let runtimes;
|
|
2007
|
+
try {
|
|
2008
|
+
const all = await detectRuntimes();
|
|
2009
|
+
const present = all.filter((r) => r.version.length > 0);
|
|
2010
|
+
runtimes = present.length > 0 ? present : void 0;
|
|
2011
|
+
} catch {
|
|
2012
|
+
}
|
|
2013
|
+
let hostMetrics;
|
|
2014
|
+
try {
|
|
2015
|
+
hostMetrics = await collectHostMetrics();
|
|
2016
|
+
} catch {
|
|
2017
|
+
}
|
|
2018
|
+
const payload = buildHeartbeatPayload({
|
|
935
2019
|
daemonVersion: VERSION,
|
|
936
2020
|
osInfo: { platform: platform(), release: release() },
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
2021
|
+
agentIds,
|
|
2022
|
+
activeAgentId: activeRunAgentId,
|
|
2023
|
+
activeTask: activeRunTask,
|
|
2024
|
+
quota: quota ?? void 0,
|
|
2025
|
+
runtimes,
|
|
2026
|
+
hostMetrics
|
|
2027
|
+
});
|
|
941
2028
|
try {
|
|
942
2029
|
await post(config, "/api/bridge/v1/heartbeat", payload);
|
|
943
2030
|
} catch (err) {
|
|
@@ -957,11 +2044,30 @@ async function startDaemon(config) {
|
|
|
957
2044
|
const data = await post(config, "/api/bridge/v1/work", {});
|
|
958
2045
|
backoffIndex = 0;
|
|
959
2046
|
if (data.work) {
|
|
960
|
-
|
|
2047
|
+
const work = data.work;
|
|
2048
|
+
const kind = classifyWorkItem(work);
|
|
2049
|
+
log(`Received work item: run ${work.runId} (kind ${kind})`);
|
|
2050
|
+
if (kind === "terminal_open") {
|
|
2051
|
+
dispatchTerminalWork(work);
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (kind === "exec") {
|
|
2055
|
+
try {
|
|
2056
|
+
await handleExecWork(config, work);
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
logError(`Unhandled error in handleExecWork for run ${work.runId}`, err);
|
|
2059
|
+
}
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
activeRunAgentId = work.agent?.id ?? null;
|
|
2063
|
+
activeRunTask = work.prompt.slice(0, 200);
|
|
961
2064
|
try {
|
|
962
|
-
await runWorkItem(config,
|
|
2065
|
+
await runWorkItem(config, work);
|
|
963
2066
|
} catch (err) {
|
|
964
|
-
logError(`Unhandled error in runWorkItem for run ${
|
|
2067
|
+
logError(`Unhandled error in runWorkItem for run ${work.runId}`, err);
|
|
2068
|
+
} finally {
|
|
2069
|
+
activeRunAgentId = null;
|
|
2070
|
+
activeRunTask = void 0;
|
|
965
2071
|
}
|
|
966
2072
|
continue;
|
|
967
2073
|
}
|
|
@@ -1032,7 +2138,7 @@ program.command("status").description("Print current configuration and server co
|
|
|
1032
2138
|
console.log();
|
|
1033
2139
|
});
|
|
1034
2140
|
program.command("autostart <action>").description("Manage start-at-logon: install | remove | status (Windows Task Scheduler)").action(async (action) => {
|
|
1035
|
-
const { autostartInstall, autostartRemove, autostartStatus } = await import('./autostart-
|
|
2141
|
+
const { autostartInstall, autostartRemove, autostartStatus } = await import('./autostart-ASW5MDQS.js');
|
|
1036
2142
|
if (action === "install") autostartInstall();
|
|
1037
2143
|
else if (action === "remove") autostartRemove();
|
|
1038
2144
|
else if (action === "status") autostartStatus();
|