@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/cli.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { requireConfig, log, readConfig, CONFIG_PATH, isConfigured, writeConfig, logError, logWarn, readJsonSafe, PENDING_REPORTS_PATH, AGENTS_DIR, writeJsonAtomic } from './chunk-GBLMB3XB.js';
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.3";
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
- writeFileSync(join(workspaceDir, "CLAUDE.md"), sections.filter(Boolean).join("\n\n"), "utf-8");
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 parsed = JSON.parse(trimmed.slice(start, lastBrace + 1));
584
- const result = {};
585
- if (typeof parsed["total_cost_usd"] === "number") {
586
- result.costUsd = parsed["total_cost_usd"];
587
- }
588
- if (typeof parsed["session_id"] === "string") {
589
- result.sessionId = parsed["session_id"];
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
- if (typeof parsed["is_error"] === "boolean") {
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 haystack = `${stderr}
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(haystack)) {
615
- return "rate_limited";
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.map((a) => /[\s"^&|<>%]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a) : 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
- const agent = findAgent(work.agent.id);
676
- const model = agent?.model ?? work.agent.model;
677
- const agentWorkspace = agentWorkspaceDir(work.agent.id);
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 "${work.agent.name}", model "${model}")`);
1135
+ log(`Starting run ${work.runId} (agent "${workAgent.name}", runtime "${runtime}", model "${model}")`);
687
1136
  log(` cwd: ${cwd}`);
688
- const claude = resolveClaudeCommand();
689
- const settingsPath = join(agentWorkspace, ".claude", "settings.json").replace(/\\/g, "/");
690
- const rawArgs = [
691
- "-p",
692
- "--output-format",
693
- "json",
694
- "--max-turns",
695
- "25",
696
- "--model",
697
- model,
698
- "--settings",
699
- settingsPath
700
- ];
701
- const persistent = work.agent.localSessionMode === "persistent";
702
- let sessionState = loadSessionState(work.agent.id);
703
- let rotated = false;
704
- if (persistent) {
705
- if (shouldRotateSession(sessionState, Date.now())) {
706
- if (sessionState.sessionId) {
707
- log(`Rotating session for agent "${work.agent.name}" (age/run limit reached)`);
708
- await runHandoffTurn(claude, agentWorkspace, settingsPath, model, sessionState.sessionId);
709
- rotated = true;
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
- sessionState = { sessionId: null, sessionStartedAt: null, runCount: 0 };
712
- } else if (sessionState.sessionId) {
713
- rawArgs.push("--resume", sessionState.sessionId);
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
- const args = claude.shell ? rawArgs.map((a) => /[\s"^&|<>%]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a) : rawArgs;
717
- const promptText = rotated ? `## Session rotated
718
- Your previous session ended (context rotation). Read HANDOFF.md and your recent memory/ journal entries before starting.
719
-
720
- ---
721
-
722
- ${work.prompt}` : work.prompt;
723
- const env = { ...process.env };
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(claude.cmd, args, {
732
- cwd,
733
- env,
734
- // shell only as a last resort (Windows .cmd shim). Safe because argv
735
- // contains only our fixed, pre-quoted tokens.
736
- shell: claude.shell,
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
- logWarn(`Run ${work.runId} exceeded timeout \u2014 sending SIGTERM`);
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
- }, 5e3);
759
- }, RUN_TIMEOUT_MS);
760
- child.stdout?.on("data", (chunk) => {
761
- stdoutBuf += chunk.toString("utf-8");
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", (chunk) => {
764
- stderrBuf += chunk.toString("utf-8");
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.length > STDOUT_CAP ? stdoutBuf.slice(-STDOUT_CAP) : stdoutBuf;
778
- const stderrExcerpt = stderrBuf.length > STDERR_CAP ? stderrBuf.slice(-STDERR_CAP) : stderrBuf;
779
- const parsed = parseClaudeOutput(stdoutBuf);
780
- const succeeded = spawnError === void 0 && exitCode === 0 && parsed.isError !== true;
781
- 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"}`;
782
- const failureKind = succeeded ? void 0 : classifyFailure(stderrExcerpt, parsed.resultText, parsed.apiErrorStatus);
783
- const {
784
- isError: _isError,
785
- resultText: _resultText,
786
- apiErrorStatus: _apiErrorStatus,
787
- ...reportFields
788
- } = parsed;
789
- const report = {
790
- runId: work.runId,
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
- async function sendReport(config, report) {
809
- const path = `/api/bridge/v1/runs/${report.runId}`;
810
- for (let attempt = 1; attempt <= REPORT_RETRY_COUNT; attempt++) {
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
- await post(config, path, report);
813
- log(`Reported run ${report.runId} (attempt ${attempt})`);
814
- removePendingReport(report.runId);
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
- logError(`Failed to report run ${report.runId} (attempt ${attempt})`, err);
818
- if (attempt < REPORT_RETRY_COUNT) {
819
- const delay = REPORT_RETRY_BASE_MS * Math.pow(2, attempt - 1);
820
- await sleep(delay);
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
- logWarn(`Persisting failed report for run ${report.runId} to pending-reports.json`);
825
- appendPendingReport(report);
826
- }
827
- async function retryPendingReports(config) {
828
- const pending = readJsonSafe(PENDING_REPORTS_PATH);
829
- if (!pending || pending.length === 0) return;
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 CACHE_TTL_MS = 6e4;
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((1 - utilization) * 100)));
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 cached = null;
1710
+ var cached2 = null;
874
1711
  var warnedNoToken = false;
875
1712
  async function getQuotaSnapshot() {
876
- if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.snapshot;
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 cached?.snapshot ?? null;
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
- cached = { snapshot, at: Date.now() };
1733
+ cached2 = { snapshot, at: Date.now() };
897
1734
  return snapshot;
898
1735
  } catch {
899
- return cached?.snapshot ?? null;
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
- await postBestEffort(config, "/api/bridge/v1/events", {
916
- kind: "daemon_stopping",
917
- message: `Daemon stopping (${signal})`,
918
- daemonVersion: VERSION
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(config, "/api/bridge/v1/events", {
925
- kind: "daemon_started",
926
- message: "Daemon started",
927
- daemonVersion: VERSION,
928
- osInfo: { platform: platform(), release: release() }
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 payload = {
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
- agents: []
938
- };
939
- const quota = await getQuotaSnapshot();
940
- if (quota) payload.quota = quota;
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
- log(`Received work item: run ${data.work.runId}`);
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, data.work);
2065
+ await runWorkItem(config, work);
963
2066
  } catch (err) {
964
- logError(`Unhandled error in runWorkItem for run ${data.work.runId}`, err);
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-77BPPTEG.js');
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();