@strideops/bridge 0.1.2 → 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 +1403 -156
- 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;
|
|
@@ -365,6 +396,188 @@ console.log(res.ok ? "identity-sync: onboarding recorded in Stride" : \`identity
|
|
|
365
396
|
mkdirSync(claudeDir, { recursive: true });
|
|
366
397
|
writeFileSync(join(claudeDir, "identity-sync.mjs"), script, { encoding: "utf-8", mode: 448 });
|
|
367
398
|
}
|
|
399
|
+
function writeMemorySearchScript(opts) {
|
|
400
|
+
const { agentId, hookSecret, apiBaseUrl, workspaceDir } = opts;
|
|
401
|
+
const base = apiBaseUrl.replace(/\/$/, "");
|
|
402
|
+
const searchUrl = `${base}/api/internal/build/agents/${agentId}/memory/search`;
|
|
403
|
+
const script = `#!/usr/bin/env node
|
|
404
|
+
// Auto-generated by stride-bridge. Do not edit.
|
|
405
|
+
// Semantically searches this agent's synced memory in Stride.
|
|
406
|
+
|
|
407
|
+
import { createHmac } from "node:crypto";
|
|
408
|
+
|
|
409
|
+
const HOOK_SECRET = ${JSON.stringify(hookSecret)};
|
|
410
|
+
const SEARCH_URL = ${JSON.stringify(searchUrl)};
|
|
411
|
+
|
|
412
|
+
const query = process.argv.slice(2).join(" ").trim();
|
|
413
|
+
if (!query) {
|
|
414
|
+
console.log("usage: node memory-search.mjs <query>");
|
|
415
|
+
process.exit(0);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const body = JSON.stringify({ query, limit: 5 });
|
|
419
|
+
const ts = Math.floor(Date.now() / 1000).toString();
|
|
420
|
+
const sig = createHmac("sha256", HOOK_SECRET).update(ts + "\\n" + body).digest("hex");
|
|
421
|
+
|
|
422
|
+
const res = await fetch(SEARCH_URL, {
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers: {
|
|
425
|
+
"Content-Type": "application/json",
|
|
426
|
+
"x-stride-hook-ts": ts,
|
|
427
|
+
"x-stride-hook-sig": sig,
|
|
428
|
+
},
|
|
429
|
+
body,
|
|
430
|
+
}).catch(() => null);
|
|
431
|
+
|
|
432
|
+
if (!res || !res.ok) {
|
|
433
|
+
console.log("memory-search: unavailable");
|
|
434
|
+
process.exit(0);
|
|
435
|
+
}
|
|
436
|
+
const data = await res.json();
|
|
437
|
+
const results = data?.data?.results ?? [];
|
|
438
|
+
if (results.length === 0) {
|
|
439
|
+
console.log("memory-search: no matches");
|
|
440
|
+
} else {
|
|
441
|
+
for (const r of results) {
|
|
442
|
+
console.log("--- " + (r.label ?? "memory") + " ---");
|
|
443
|
+
console.log(String(r.content ?? "").slice(0, 600));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
`;
|
|
447
|
+
const claudeDir = join(workspaceDir, ".claude");
|
|
448
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
449
|
+
writeFileSync(join(claudeDir, "memory-search.mjs"), script, { encoding: "utf-8", mode: 448 });
|
|
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
|
+
}
|
|
368
581
|
|
|
369
582
|
// src/agents.ts
|
|
370
583
|
var cachedAgents = [];
|
|
@@ -378,10 +591,14 @@ async function refreshAgents(config) {
|
|
|
378
591
|
for (const agent of cachedAgents) {
|
|
379
592
|
await ensureAgentWorkspace(config, agent);
|
|
380
593
|
}
|
|
594
|
+
invalidateRuntimeCache();
|
|
381
595
|
} catch (err) {
|
|
382
596
|
logError("Failed to refresh agents", err);
|
|
383
597
|
}
|
|
384
598
|
}
|
|
599
|
+
function getCachedAgents() {
|
|
600
|
+
return cachedAgents;
|
|
601
|
+
}
|
|
385
602
|
function findAgent(agentId) {
|
|
386
603
|
return cachedAgents.find((a) => a.id === agentId);
|
|
387
604
|
}
|
|
@@ -394,17 +611,32 @@ async function ensureAgentWorkspace(config, agent) {
|
|
|
394
611
|
sections.push(agent.soulMd ?? agent.defaultSoulMd ?? "");
|
|
395
612
|
if (agent.memoryProtocolMd) sections.push(agent.memoryProtocolMd);
|
|
396
613
|
sections.push(
|
|
397
|
-
|
|
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'
|
|
398
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
|
+
}
|
|
399
627
|
if (!agent.onboardedAt && agent.onboardingMd) {
|
|
400
628
|
sections.push(
|
|
401
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"
|
|
402
630
|
);
|
|
403
631
|
}
|
|
404
|
-
|
|
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");
|
|
405
635
|
if (!agent.onboardedAt && agent.onboardingMd) {
|
|
406
636
|
writeFileSync(join(workspaceDir, "ONBOARDING.md"), agent.onboardingMd, "utf-8");
|
|
407
637
|
}
|
|
638
|
+
const soulContent = agent.soulMd ?? agent.defaultSoulMd;
|
|
639
|
+
if (soulContent) writeFileSync(join(workspaceDir, "SOUL.md"), soulContent, "utf-8");
|
|
408
640
|
if (agent.identityMd) writeFileSync(join(workspaceDir, "IDENTITY.md"), agent.identityMd, "utf-8");
|
|
409
641
|
if (agent.userMd) writeFileSync(join(workspaceDir, "USER.md"), agent.userMd, "utf-8");
|
|
410
642
|
if (agent.heartbeatChecklist) {
|
|
@@ -436,6 +668,18 @@ async function ensureAgentWorkspace(config, agent) {
|
|
|
436
668
|
apiBaseUrl: config.apiBaseUrl,
|
|
437
669
|
workspaceDir
|
|
438
670
|
});
|
|
671
|
+
writeMemorySearchScript({
|
|
672
|
+
agentId: agent.id,
|
|
673
|
+
hookSecret: agent.hookSecret,
|
|
674
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
675
|
+
workspaceDir
|
|
676
|
+
});
|
|
677
|
+
writeApprovalRequestScript({
|
|
678
|
+
agentId: agent.id,
|
|
679
|
+
hookSecret: agent.hookSecret,
|
|
680
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
681
|
+
workspaceDir
|
|
682
|
+
});
|
|
439
683
|
log(`Provisioned workspace for agent "${agent.name}" (${agent.id})`);
|
|
440
684
|
}
|
|
441
685
|
function agentWorkspaceDir(agentId) {
|
|
@@ -491,6 +735,151 @@ function recordRunSession(agentId, newSessionId, previous, nowMs) {
|
|
|
491
735
|
return next;
|
|
492
736
|
}
|
|
493
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
|
+
|
|
494
883
|
// src/runner.ts
|
|
495
884
|
var RUN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
496
885
|
var STDOUT_CAP = 1e4;
|
|
@@ -503,6 +892,12 @@ function resolveClaudeCommand() {
|
|
|
503
892
|
if (existsSync(nativeExe)) return { cmd: nativeExe, shell: false };
|
|
504
893
|
return { cmd: "claude", shell: true };
|
|
505
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
|
+
}
|
|
506
901
|
function parseClaudeOutput(raw) {
|
|
507
902
|
const trimmed = raw.trim();
|
|
508
903
|
const lastBrace = trimmed.lastIndexOf("}");
|
|
@@ -521,148 +916,526 @@ function parseClaudeOutput(raw) {
|
|
|
521
916
|
}
|
|
522
917
|
if (start === -1) return {};
|
|
523
918
|
try {
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
if (typeof
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (typeof
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (typeof
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
result.resultText = parsed["result"];
|
|
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"];
|
|
537
931
|
}
|
|
538
|
-
|
|
539
|
-
if (usage && typeof usage === "object") {
|
|
540
|
-
const u = usage;
|
|
541
|
-
if (typeof u["input_tokens"] === "number") result.tokensInput = u["input_tokens"];
|
|
542
|
-
if (typeof u["output_tokens"] === "number") result.tokensOutput = u["output_tokens"];
|
|
543
|
-
}
|
|
544
|
-
return result;
|
|
932
|
+
return r;
|
|
545
933
|
} catch {
|
|
546
934
|
return {};
|
|
547
935
|
}
|
|
548
936
|
}
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
937
|
+
function classifyFailure(stderr, resultText, apiErrorStatus) {
|
|
938
|
+
const h = `${stderr}
|
|
939
|
+
${resultText ?? ""}`.toLowerCase();
|
|
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))
|
|
942
|
+
return "environment";
|
|
943
|
+
return "error";
|
|
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";
|
|
560
953
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
const
|
|
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
|
+
}
|
|
984
|
+
var HANDOFF_TIMEOUT_MS = 4 * 60 * 1e3;
|
|
985
|
+
async function runHandoffTurn(claude, agentWorkspace, settingsPath, model, oldSessionId) {
|
|
565
986
|
const rawArgs = [
|
|
566
987
|
"-p",
|
|
567
988
|
"--output-format",
|
|
568
989
|
"json",
|
|
569
990
|
"--max-turns",
|
|
570
|
-
"
|
|
991
|
+
"6",
|
|
571
992
|
"--model",
|
|
572
993
|
model,
|
|
573
994
|
"--settings",
|
|
574
|
-
settingsPath
|
|
995
|
+
settingsPath,
|
|
996
|
+
"--resume",
|
|
997
|
+
oldSessionId
|
|
575
998
|
];
|
|
576
|
-
const
|
|
577
|
-
let sessionState = loadSessionState(work.agent.id);
|
|
578
|
-
if (persistent) {
|
|
579
|
-
if (shouldRotateSession(sessionState, Date.now())) {
|
|
580
|
-
if (sessionState.sessionId) {
|
|
581
|
-
log(`Rotating session for agent "${work.agent.name}" (age/run limit reached)`);
|
|
582
|
-
}
|
|
583
|
-
sessionState = { sessionId: null, sessionStartedAt: null, runCount: 0 };
|
|
584
|
-
} else if (sessionState.sessionId) {
|
|
585
|
-
rawArgs.push("--resume", sessionState.sessionId);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
const args = claude.shell ? rawArgs.map((a) => /[\s"^&|<>%]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a) : rawArgs;
|
|
589
|
-
const env = { ...process.env };
|
|
590
|
-
let stdoutBuf = "";
|
|
591
|
-
let stderrBuf = "";
|
|
592
|
-
let exitCode = null;
|
|
593
|
-
let spawnError;
|
|
999
|
+
const args = claude.shell ? shellEscapeArgs(rawArgs) : rawArgs;
|
|
594
1000
|
await new Promise((resolve) => {
|
|
595
1001
|
let child;
|
|
596
1002
|
try {
|
|
597
1003
|
child = spawn(claude.cmd, args, {
|
|
598
|
-
cwd,
|
|
599
|
-
env,
|
|
600
|
-
// shell only as a last resort (Windows .cmd shim). Safe because argv
|
|
601
|
-
// contains only our fixed, pre-quoted tokens.
|
|
1004
|
+
cwd: agentWorkspace,
|
|
1005
|
+
env: { ...process.env },
|
|
602
1006
|
shell: claude.shell,
|
|
603
|
-
|
|
604
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1007
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
605
1008
|
});
|
|
606
|
-
child.stdin?.write(
|
|
1009
|
+
child.stdin?.write(
|
|
1010
|
+
"Your session is about to be rotated (context limit). Write or overwrite HANDOFF.md in your workspace root RIGHT NOW: what you were working on, current state, decisions in flight, and the immediate next step. Max 60 lines. Do nothing else."
|
|
1011
|
+
);
|
|
607
1012
|
child.stdin?.end();
|
|
608
1013
|
} catch (err) {
|
|
609
|
-
|
|
1014
|
+
logWarn(`Handoff turn failed to spawn: ${err instanceof Error ? err.message : String(err)}`);
|
|
610
1015
|
resolve();
|
|
611
1016
|
return;
|
|
612
1017
|
}
|
|
613
|
-
const
|
|
614
|
-
logWarn(
|
|
1018
|
+
const timer = setTimeout(() => {
|
|
1019
|
+
logWarn("Handoff turn timed out \u2014 rotating without handoff doc");
|
|
615
1020
|
try {
|
|
616
1021
|
child.kill("SIGTERM");
|
|
617
1022
|
} catch {
|
|
618
1023
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
1024
|
+
}, HANDOFF_TIMEOUT_MS);
|
|
1025
|
+
child.on("close", () => {
|
|
1026
|
+
clearTimeout(timer);
|
|
1027
|
+
log("Handoff turn complete");
|
|
1028
|
+
resolve();
|
|
1029
|
+
});
|
|
1030
|
+
child.on("error", () => {
|
|
1031
|
+
clearTimeout(timer);
|
|
1032
|
+
resolve();
|
|
628
1033
|
});
|
|
629
|
-
|
|
630
|
-
|
|
1034
|
+
});
|
|
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");
|
|
631
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);
|
|
632
1064
|
child.on("close", (code) => {
|
|
633
|
-
clearTimeout(
|
|
634
|
-
|
|
1065
|
+
clearTimeout(timer);
|
|
1066
|
+
if (code !== 0) logWarn(`Memory sync exited ${String(code)}${stderrBuf ? `: ${stderrBuf.slice(-500)}` : ""}`);
|
|
635
1067
|
resolve();
|
|
636
1068
|
});
|
|
637
1069
|
child.on("error", (err) => {
|
|
638
|
-
clearTimeout(
|
|
639
|
-
|
|
1070
|
+
clearTimeout(timer);
|
|
1071
|
+
logWarn(`Memory sync error: ${err.message}`);
|
|
640
1072
|
resolve();
|
|
641
1073
|
});
|
|
642
1074
|
});
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
+
}
|
|
1107
|
+
async function runWorkItem(config, work) {
|
|
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
|
|
661
1128
|
);
|
|
662
|
-
|
|
663
|
-
|
|
1129
|
+
let cwd = agentWorkspace;
|
|
1130
|
+
if (work.workingDir && existsSync(work.workingDir)) {
|
|
1131
|
+
cwd = work.workingDir;
|
|
1132
|
+
} else if (work.workingDir) {
|
|
1133
|
+
logWarn(`workingDir "${work.workingDir}" does not exist; falling back to agent workspace`);
|
|
1134
|
+
}
|
|
1135
|
+
log(`Starting run ${work.runId} (agent "${workAgent.name}", runtime "${runtime}", model "${model}")`);
|
|
1136
|
+
log(` cwd: ${cwd}`);
|
|
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;
|
|
1298
|
+
}
|
|
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);
|
|
1437
|
+
}
|
|
664
1438
|
}
|
|
665
|
-
await sendReport(config, report);
|
|
666
1439
|
}
|
|
667
1440
|
async function sendReport(config, report) {
|
|
668
1441
|
const path = `/api/bridge/v1/runs/${report.runId}`;
|
|
@@ -674,46 +1447,251 @@ async function sendReport(config, report) {
|
|
|
674
1447
|
return;
|
|
675
1448
|
} catch (err) {
|
|
676
1449
|
logError(`Failed to report run ${report.runId} (attempt ${attempt})`, err);
|
|
677
|
-
if (attempt < REPORT_RETRY_COUNT)
|
|
678
|
-
const delay = REPORT_RETRY_BASE_MS * Math.pow(2, attempt - 1);
|
|
679
|
-
await sleep(delay);
|
|
680
|
-
}
|
|
1450
|
+
if (attempt < REPORT_RETRY_COUNT) await sleep(REPORT_RETRY_BASE_MS * Math.pow(2, attempt - 1));
|
|
681
1451
|
}
|
|
682
1452
|
}
|
|
683
1453
|
logWarn(`Persisting failed report for run ${report.runId} to pending-reports.json`);
|
|
684
|
-
|
|
1454
|
+
enqueue({ runId: report.runId, report, attempts: REPORT_RETRY_COUNT, firstFailedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
685
1455
|
}
|
|
686
1456
|
async function retryPendingReports(config) {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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;
|
|
692
1474
|
}
|
|
1475
|
+
return Math.min(timeoutMs, MAX_EXEC_TIMEOUT_MS);
|
|
693
1476
|
}
|
|
694
|
-
function
|
|
695
|
-
|
|
696
|
-
const deduped = existing.filter((r) => r.runId !== report.runId);
|
|
697
|
-
deduped.push(report);
|
|
698
|
-
writeJsonAtomic(PENDING_REPORTS_PATH, deduped);
|
|
1477
|
+
function capExcerpt(buf, cap) {
|
|
1478
|
+
return buf.length > cap ? buf.slice(-cap) : buf;
|
|
699
1479
|
}
|
|
700
|
-
function
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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 };
|
|
706
1489
|
}
|
|
1490
|
+
const sh = process.env["SHELL"] ?? "/bin/sh";
|
|
1491
|
+
return { cmd: sh, args: ["-c", command], shell: false };
|
|
707
1492
|
}
|
|
708
|
-
function
|
|
709
|
-
|
|
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;
|
|
1497
|
+
let stdoutBuf = "";
|
|
1498
|
+
let stderrBuf = "";
|
|
1499
|
+
let exitCode = null;
|
|
1500
|
+
let spawnError;
|
|
1501
|
+
let timedOut = false;
|
|
1502
|
+
await new Promise((resolve) => {
|
|
1503
|
+
let child;
|
|
1504
|
+
try {
|
|
1505
|
+
child = spawn(cmd, args, {
|
|
1506
|
+
cwd: spawnCwd,
|
|
1507
|
+
env: { ...process.env },
|
|
1508
|
+
shell,
|
|
1509
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1510
|
+
windowsHide: true
|
|
1511
|
+
});
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
spawnError = err instanceof Error ? err.message : String(err);
|
|
1514
|
+
resolve();
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const timeoutHandle = setTimeout(() => {
|
|
1518
|
+
timedOut = true;
|
|
1519
|
+
try {
|
|
1520
|
+
child.kill("SIGTERM");
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
setTimeout(() => {
|
|
1524
|
+
try {
|
|
1525
|
+
child.kill("SIGKILL");
|
|
1526
|
+
} catch {
|
|
1527
|
+
}
|
|
1528
|
+
}, KILL_GRACE_MS);
|
|
1529
|
+
}, timeoutMs);
|
|
1530
|
+
child.stdout?.on("data", (c) => {
|
|
1531
|
+
stdoutBuf += c.toString("utf-8");
|
|
1532
|
+
});
|
|
1533
|
+
child.stderr?.on("data", (c) => {
|
|
1534
|
+
stderrBuf += c.toString("utf-8");
|
|
1535
|
+
});
|
|
1536
|
+
child.on("close", (code) => {
|
|
1537
|
+
clearTimeout(timeoutHandle);
|
|
1538
|
+
exitCode = code;
|
|
1539
|
+
resolve();
|
|
1540
|
+
});
|
|
1541
|
+
child.on("error", (err) => {
|
|
1542
|
+
clearTimeout(timeoutHandle);
|
|
1543
|
+
spawnError = err.message;
|
|
1544
|
+
resolve();
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
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;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
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
|
+
}
|
|
1605
|
+
try {
|
|
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");
|
|
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
|
+
});
|
|
1642
|
+
} catch (err) {
|
|
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
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
} else if (frame.type === "ping") {
|
|
1679
|
+
send({ type: "pong" });
|
|
1680
|
+
}
|
|
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;
|
|
710
1688
|
}
|
|
711
1689
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
712
|
-
var
|
|
1690
|
+
var CACHE_TTL_MS2 = 6e4;
|
|
713
1691
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
714
1692
|
function remainingPct(utilization) {
|
|
715
1693
|
if (typeof utilization !== "number" || !Number.isFinite(utilization)) return null;
|
|
716
|
-
return Math.max(0, Math.min(100, Math.round(
|
|
1694
|
+
return Math.max(0, Math.min(100, Math.round(100 - utilization)));
|
|
717
1695
|
}
|
|
718
1696
|
function parseUsageResponse(body, nowIso) {
|
|
719
1697
|
return {
|
|
@@ -729,10 +1707,10 @@ function readOauthToken() {
|
|
|
729
1707
|
);
|
|
730
1708
|
return creds?.claudeAiOauth?.accessToken ?? null;
|
|
731
1709
|
}
|
|
732
|
-
var
|
|
1710
|
+
var cached2 = null;
|
|
733
1711
|
var warnedNoToken = false;
|
|
734
1712
|
async function getQuotaSnapshot() {
|
|
735
|
-
if (
|
|
1713
|
+
if (cached2 && Date.now() - cached2.at < CACHE_TTL_MS2) return cached2.snapshot;
|
|
736
1714
|
const token = readOauthToken();
|
|
737
1715
|
if (!token) {
|
|
738
1716
|
if (!warnedNoToken) {
|
|
@@ -749,20 +1727,243 @@ async function getQuotaSnapshot() {
|
|
|
749
1727
|
signal: controller.signal
|
|
750
1728
|
});
|
|
751
1729
|
clearTimeout(timer);
|
|
752
|
-
if (!res.ok) return
|
|
1730
|
+
if (!res.ok) return cached2?.snapshot ?? null;
|
|
753
1731
|
const body = await res.json();
|
|
754
1732
|
const snapshot = parseUsageResponse(body, (/* @__PURE__ */ new Date()).toISOString());
|
|
755
|
-
|
|
1733
|
+
cached2 = { snapshot, at: Date.now() };
|
|
756
1734
|
return snapshot;
|
|
757
1735
|
} catch {
|
|
758
|
-
return
|
|
1736
|
+
return cached2?.snapshot ?? null;
|
|
1737
|
+
}
|
|
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 {
|
|
759
1826
|
}
|
|
1827
|
+
return metrics;
|
|
760
1828
|
}
|
|
761
1829
|
|
|
762
1830
|
// src/poller.ts
|
|
763
1831
|
var POLL_INTERVAL_MS = 2500;
|
|
764
1832
|
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
765
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
|
+
}
|
|
766
1967
|
var running = false;
|
|
767
1968
|
async function startDaemon(config) {
|
|
768
1969
|
running = true;
|
|
@@ -771,32 +1972,59 @@ async function startDaemon(config) {
|
|
|
771
1972
|
running = false;
|
|
772
1973
|
log(`Received ${signal} \u2014 shutting down gracefully`);
|
|
773
1974
|
stopAgentRefreshTimer();
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
+
);
|
|
779
1985
|
process.exit(0);
|
|
780
1986
|
};
|
|
781
1987
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
782
1988
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
783
|
-
await postBestEffort(
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
+
);
|
|
789
1999
|
log(`Daemon started (v${VERSION}, bridge ${config.bridgeId})`);
|
|
790
2000
|
await refreshAgents(config);
|
|
791
2001
|
startAgentRefreshTimer(config);
|
|
792
2002
|
const sendHeartbeat = async () => {
|
|
793
|
-
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({
|
|
794
2019
|
daemonVersion: VERSION,
|
|
795
2020
|
osInfo: { platform: platform(), release: release() },
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
2021
|
+
agentIds,
|
|
2022
|
+
activeAgentId: activeRunAgentId,
|
|
2023
|
+
activeTask: activeRunTask,
|
|
2024
|
+
quota: quota ?? void 0,
|
|
2025
|
+
runtimes,
|
|
2026
|
+
hostMetrics
|
|
2027
|
+
});
|
|
800
2028
|
try {
|
|
801
2029
|
await post(config, "/api/bridge/v1/heartbeat", payload);
|
|
802
2030
|
} catch (err) {
|
|
@@ -816,11 +2044,30 @@ async function startDaemon(config) {
|
|
|
816
2044
|
const data = await post(config, "/api/bridge/v1/work", {});
|
|
817
2045
|
backoffIndex = 0;
|
|
818
2046
|
if (data.work) {
|
|
819
|
-
|
|
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);
|
|
820
2064
|
try {
|
|
821
|
-
await runWorkItem(config,
|
|
2065
|
+
await runWorkItem(config, work);
|
|
822
2066
|
} catch (err) {
|
|
823
|
-
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;
|
|
824
2071
|
}
|
|
825
2072
|
continue;
|
|
826
2073
|
}
|
|
@@ -891,7 +2138,7 @@ program.command("status").description("Print current configuration and server co
|
|
|
891
2138
|
console.log();
|
|
892
2139
|
});
|
|
893
2140
|
program.command("autostart <action>").description("Manage start-at-logon: install | remove | status (Windows Task Scheduler)").action(async (action) => {
|
|
894
|
-
const { autostartInstall, autostartRemove, autostartStatus } = await import('./autostart-
|
|
2141
|
+
const { autostartInstall, autostartRemove, autostartStatus } = await import('./autostart-ASW5MDQS.js');
|
|
895
2142
|
if (action === "install") autostartInstall();
|
|
896
2143
|
else if (action === "remove") autostartRemove();
|
|
897
2144
|
else if (action === "status") autostartStatus();
|