@bivy/bivy 0.7.0-staging.104 → 0.7.0-staging.106

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.
@@ -116,14 +116,27 @@ function claudeCodeInfo() {
116
116
  },
117
117
  };
118
118
  }
119
+ // Memoized CLI probes. `commandAvailable`/`resolveCommandPath`/`probeHelpText`
120
+ // each shell out with a BLOCKING spawnSync, and the runtime catalog that calls
121
+ // them (cliAgentInfo → prefersAcp/acpSupportedByBinary) is rebuilt often — on
122
+ // every advertise and every runtimes.list send. Re-probing per build stalled the
123
+ // event loop for seconds at a time (a synchronous spawn storm). A CLI's presence
124
+ // is effectively constant for the daemon's run, so cache per command for the
125
+ // process lifetime and clear on install (invalidateCliProbeCache).
126
+ const COMMAND_AVAILABLE_CACHE = new Map();
119
127
  function commandAvailable(command) {
120
128
  if (!command.trim())
121
129
  return false;
130
+ const cached = COMMAND_AVAILABLE_CACHE.get(command);
131
+ if (cached !== undefined)
132
+ return cached;
122
133
  const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
123
134
  shell: process.platform !== "win32",
124
135
  stdio: "ignore",
125
136
  });
126
- return result.status === 0;
137
+ const available = result.status === 0;
138
+ COMMAND_AVAILABLE_CACHE.set(command, available);
139
+ return available;
127
140
  }
128
141
  function genericCliInfo() {
129
142
  const options = processRuntimeFromEnv();
@@ -869,17 +882,23 @@ function cliThinkingConfig(id) {
869
882
  * Used to key the help-probe cache: caching by the bare NAME would keep serving a
870
883
  * stale answer after the binary behind that name changed (a CLI upgraded or
871
884
  * installed while the daemon is running, or a different PATH entry winning).
885
+ * Memoized per command (see COMMAND_AVAILABLE_CACHE) — it spawnSyncs, and is hit
886
+ * on every catalog build.
872
887
  */
888
+ const COMMAND_PATH_CACHE = new Map();
873
889
  function resolveCommandPath(command) {
874
890
  if (!command.trim())
875
891
  return null;
892
+ const cached = COMMAND_PATH_CACHE.get(command);
893
+ if (cached !== undefined)
894
+ return cached;
876
895
  const res = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
877
896
  shell: process.platform !== "win32",
878
897
  encoding: "utf8",
879
898
  });
880
- if (res.status !== 0)
881
- return null;
882
- return (res.stdout ?? "").split(/\r?\n/)[0]?.trim() || null;
899
+ const resolved = res.status !== 0 ? null : ((res.stdout ?? "").split(/\r?\n/)[0]?.trim() || null);
900
+ COMMAND_PATH_CACHE.set(command, resolved);
901
+ return resolved;
883
902
  }
884
903
  const HELP_PROBE_CACHE = new Map();
885
904
  function probeHelpText(command) {
@@ -898,6 +917,19 @@ function probeHelpText(command) {
898
917
  HELP_PROBE_CACHE.set(key, text);
899
918
  return text;
900
919
  }
920
+ /**
921
+ * Drop every memoized CLI probe (availability, resolved path, --help text). These
922
+ * probes shell out with a blocking spawnSync and are cached for the process
923
+ * lifetime to keep the frequently-rebuilt runtime catalog off the event loop, so a
924
+ * CLI installed/updated mid-run wouldn't otherwise be noticed until a restart.
925
+ * Call this right after Bivy installs a runtime so the next catalog build re-probes
926
+ * and reflects the new binary immediately.
927
+ */
928
+ export function invalidateCliProbeCache() {
929
+ COMMAND_AVAILABLE_CACHE.clear();
930
+ COMMAND_PATH_CACHE.clear();
931
+ HELP_PROBE_CACHE.clear();
932
+ }
901
933
  // A resume template mixes launch flags (`-p`, `--force`) with the resume-specific
902
934
  // token(s) (`--resume`, `threads continue`, `-s`, `--restore`, …). Only the latter
903
935
  // evidence resume support, so we match on those — otherwise a shared launch flag
package/dist/server.js CHANGED
@@ -8,7 +8,7 @@ import { randomUUID, randomBytes, timingSafeEqual, createHash } from "node:crypt
8
8
  import { fileURLToPath } from "node:url";
9
9
  import express from "express";
10
10
  import { WebSocketServer, WebSocket } from "ws";
11
- import { listRuntimes, catalogRuntimes, cliInstallSpec, isCliAgentId } from "./runtime/index.js";
11
+ import { listRuntimes, catalogRuntimes, cliInstallSpec, invalidateCliProbeCache, isCliAgentId } from "./runtime/index.js";
12
12
  import { createRunPolicy } from "./policy/run-policy.js";
13
13
  import { DEFAULT_BACKOFF } from "./policy/ruleset.js";
14
14
  import { SessionRerouteController } from "./policy/session-reroute.js";
@@ -3217,6 +3217,9 @@ const RELAY_COMMANDS = {
3217
3217
  const before = runtimeList().find((runtime) => runtime.id === spec.id);
3218
3218
  if (before?.status !== "available")
3219
3219
  await runInstallCommand(spec);
3220
+ // The just-installed binary changes what the CLI probes would report, so drop
3221
+ // their (process-lifetime) cache and let the catalog below re-probe it.
3222
+ invalidateCliProbeCache();
3220
3223
  const activeAgent = active?.runtimeId ?? defaultRuntimeId;
3221
3224
  const runtimes = runtimeList(activeAgent);
3222
3225
  relay?.sendEvent({ type: "runtime.install.done", id: spec.id, runtimes });
@@ -4345,6 +4348,12 @@ function startModelAuthWatcher() {
4345
4348
  let sessionAdvertiseTarget;
4346
4349
  let advertiseTimer;
4347
4350
  let advertiseResyncTimer;
4351
+ // Only one replace-all session advert may be in flight. If an older snapshot
4352
+ // (still containing a just-deleted/pruned session) completes after a newer one,
4353
+ // the control plane resurrects that row. Changes arriving during a request set
4354
+ // this flag and are sent immediately after it completes, in order.
4355
+ let advertiseRunning = false;
4356
+ let advertiseAgain = false;
4348
4357
  // How often the node re-affirms it's online to the control plane. Kept well
4349
4358
  // under the control plane's NODE_ONLINE_TTL_MS (90s) so a missed beat or two
4350
4359
  // doesn't flap a healthy node's status.
@@ -4575,11 +4584,17 @@ async function advertiseSessions() {
4575
4584
  : [];
4576
4585
  return {
4577
4586
  sessionId: s.id,
4578
- status: pendingApproval || failureAttention.length ? "needs_action" : (record ? (sessionBusy(record) ? "working" : "idle") : "saved"),
4579
- needsAction: pendingApproval || failureAttention.length > 0,
4587
+ // Failures (including exhausted credits/rate limits) are outcomes to
4588
+ // review, not blocking questions that keep saying "Needs your response".
4589
+ // Only a still-pending approval/question owns that status.
4590
+ status: pendingApproval ? "needs_action" : (record ? (sessionBusy(record) ? "working" : "idle") : "saved"),
4591
+ needsAction: pendingApproval,
4580
4592
  source: record?.source || meta?.source,
4581
4593
  titleEnc: name ? relay.sealString(name) : undefined,
4582
4594
  branch: record?.worktree?.branch || meta?.branch,
4595
+ // This is activity time, not advert receive time. A daemon restart/full
4596
+ // resync must not make every historical row appear freshly updated.
4597
+ updatedAt: isoFrom(record?.lastTouchedAt ?? meta?.lastActivityAt ?? meta?.updatedAt ?? s.modified),
4583
4598
  agentServiceAddress,
4584
4599
  githubIssueUrl: record?.githubIssueUrl,
4585
4600
  prUrl: record?.prUrl,
@@ -4597,16 +4612,39 @@ async function advertiseSessions() {
4597
4612
  // best effort; the periodic resync and the next change will retry
4598
4613
  }
4599
4614
  }
4600
- /** Debounced advertise — many session events collapse into one POST. */
4615
+ /** Debounced, serialized advertise — many session events collapse into one
4616
+ * POST, and replace-all snapshots can never complete out of order. */
4601
4617
  function scheduleAdvertise() {
4602
- if (!sessionAdvertiseTarget || advertiseTimer)
4618
+ if (!sessionAdvertiseTarget)
4619
+ return;
4620
+ if (advertiseRunning) {
4621
+ advertiseAgain = true;
4622
+ return;
4623
+ }
4624
+ if (advertiseTimer)
4603
4625
  return;
4604
4626
  advertiseTimer = setTimeout(() => {
4605
4627
  advertiseTimer = undefined;
4606
- void advertiseSessions();
4628
+ void drainSessionAdverts();
4607
4629
  }, 1000);
4608
4630
  advertiseTimer.unref?.();
4609
4631
  }
4632
+ async function drainSessionAdverts() {
4633
+ if (advertiseRunning) {
4634
+ advertiseAgain = true;
4635
+ return;
4636
+ }
4637
+ advertiseRunning = true;
4638
+ try {
4639
+ do {
4640
+ advertiseAgain = false;
4641
+ await advertiseSessions();
4642
+ } while (advertiseAgain);
4643
+ }
4644
+ finally {
4645
+ advertiseRunning = false;
4646
+ }
4647
+ }
4610
4648
  let githubPoller;
4611
4649
  /**
4612
4650
  * Serialize all work for one issue (keyed by its `issue:owner/repo#N` source).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.7.0-staging.104",
3
+ "version": "0.7.0-staging.106",
4
4
  "type": "module",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "description": "Run coding agents on machines you own. Source-available, self-hostable agent workspace.",