@bivy/bivy 0.6.0-staging.84 → 0.6.0-staging.85

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.
Files changed (2) hide show
  1. package/dist/server.js +101 -27
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -3084,6 +3084,7 @@ const RELAY_COMMANDS = {
3084
3084
  },
3085
3085
  async "models.list"(msg) {
3086
3086
  const requestedSessionId = typeof msg.sessionId === "string" && msg.sessionId ? msg.sessionId : undefined;
3087
+ const wantedRuntimeId = typeof msg.runtimeId === "string" && msg.runtimeId ? msg.runtimeId : undefined;
3087
3088
  let record;
3088
3089
  try {
3089
3090
  record = requestedSessionId ? await resolveOrResumeSession(requestedSessionId, msg.path) : active;
@@ -3096,7 +3097,12 @@ const RELAY_COMMANDS = {
3096
3097
  relay?.sendEvent({ type: "session.error", sessionId: requestedSessionId, error: "Session not found" });
3097
3098
  return;
3098
3099
  }
3099
- record ??= await sessionForModelQuery();
3100
+ // On a draft (no session id), a runtime hint from the composer takes
3101
+ // precedence so an agent switch previews *that* agent's models even if a
3102
+ // stale `active` on another runtime lingers on the node.
3103
+ if (!requestedSessionId && wantedRuntimeId && record?.runtimeId !== wantedRuntimeId)
3104
+ record = null;
3105
+ record ??= await sessionForModelQuery(wantedRuntimeId);
3100
3106
  const session = record.session;
3101
3107
  const current = session.getCurrentModel();
3102
3108
  const models = await publicModelsList(session, current);
@@ -3108,6 +3114,17 @@ const RELAY_COMMANDS = {
3108
3114
  // (e.g. Claude) — the "Claude shows Codex models" bug.
3109
3115
  relay?.sendEvent({ type: "models.list", sessionId: record.id, runtimeId: record.runtimeId, current: current ? publicModel(current, current) : null, models, thinking });
3110
3116
  },
3117
+ "models.prefetch"(msg) {
3118
+ // The composer's agent picker opened: warm the scratch session for each
3119
+ // offered agent in the background so the first switch to any of them answers
3120
+ // instantly. Fire-and-forget — no reply; the follow-up models.list carries
3121
+ // the result. Ignore anything but a bounded string[] of runtime ids.
3122
+ const ids = Array.isArray(msg.runtimeIds)
3123
+ ? msg.runtimeIds.filter((id) => typeof id === "string" && !!id).slice(0, 16)
3124
+ : [];
3125
+ if (ids.length)
3126
+ prefetchModels(ids);
3127
+ },
3111
3128
  async "model.select"(msg) {
3112
3129
  const requestedSessionId = typeof msg.sessionId === "string" && msg.sessionId ? msg.sessionId : undefined;
3113
3130
  let record;
@@ -8055,32 +8072,75 @@ async function resolveOrResumeSession(sessionId, sessionPath) {
8055
8072
  // races the runtime.select that switches the default agent, pin the pill to the
8056
8073
  // *previous* runtime (the reported agent-switching bug). Mirror how session.new/
8057
8074
  // session.open already refuse to touch `active` for remote clients: reuse a
8058
- // single non-active scratch session on the current default runtime instead of
8059
- // spawning a fresh runtime process on every picker read.
8060
- let modelQueryScratch;
8061
- let modelQueryScratchPending;
8062
- async function sessionForModelQuery() {
8063
- if (active)
8075
+ // non-active scratch session per runtime instead of spawning a fresh runtime
8076
+ // process on every picker read.
8077
+ //
8078
+ // Keyed by runtime id, not a single slot: switching agents (Claude → Codex →
8079
+ // Claude) used to evict and re-spawn the one scratch on every switch — the
8080
+ // "switching agent takes a long time before models appear" bug. A map keeps one
8081
+ // warm scratch per runtime so a switch back to an agent already viewed this
8082
+ // session answers from the live session with no re-spawn, and `prefetchModels`
8083
+ // can warm several ahead of the first pick.
8084
+ const modelQueryScratch = new Map();
8085
+ const modelQueryScratchPending = new Map();
8086
+ async function sessionForModelQuery(runtimeId) {
8087
+ const wanted = resolveRuntimeId(runtimeId);
8088
+ // A live active session answers for itself — but only when it IS the runtime
8089
+ // being queried, so a prefetch/draft read for a *different* agent doesn't get
8090
+ // the active session's (wrong-runtime) model list.
8091
+ if (active && active.runtimeId === wanted)
8064
8092
  return active;
8065
- const wanted = resolveRuntimeId();
8066
- if (modelQueryScratch &&
8067
- openSessions.has(modelQueryScratch.id) &&
8068
- modelQueryScratch.runtimeId === wanted &&
8069
- !sessionBusy(modelQueryScratch)) {
8070
- touchSession(modelQueryScratch);
8071
- return modelQueryScratch;
8072
- }
8073
- // De-dupe concurrent picker reads. Without this, a WS models.list and an HTTP
8074
- // GET /api/models fired together on page load both miss the reuse guard above
8075
- // (the scratch assignment only lands after createSession resolves ~0.3s later)
8076
- // and each stand up a session, leaving two empty rows a fraction of a second
8077
- // apart. Collapse concurrent builds onto one promise, mirroring resumingSessions.
8078
- if (modelQueryScratchPending)
8079
- return modelQueryScratchPending;
8080
- modelQueryScratchPending = createSession(defaultWorkspace, undefined, { makeActive: false, ephemeral: true })
8081
- .then((rec) => { modelQueryScratch = rec; return rec; })
8082
- .finally(() => { modelQueryScratchPending = undefined; });
8083
- return modelQueryScratchPending;
8093
+ const cached = modelQueryScratch.get(wanted);
8094
+ if (cached && openSessions.has(cached.id) && cached.runtimeId === wanted && !sessionBusy(cached)) {
8095
+ touchSession(cached);
8096
+ return cached;
8097
+ }
8098
+ // De-dupe concurrent picker reads per runtime. Without this, a WS models.list
8099
+ // and an HTTP GET /api/models fired together on page load both miss the reuse
8100
+ // guard above (the scratch assignment only lands after createSession resolves
8101
+ // ~0.3s later) and each stand up a session, leaving two empty rows a fraction
8102
+ // of a second apart. Collapse concurrent builds onto one promise per runtime,
8103
+ // mirroring resumingSessions.
8104
+ const inflight = modelQueryScratchPending.get(wanted);
8105
+ if (inflight)
8106
+ return inflight;
8107
+ const build = createSession(defaultWorkspace, undefined, { makeActive: false, ephemeral: true, runtimeId: wanted })
8108
+ .then((rec) => { modelQueryScratch.set(wanted, rec); return rec; })
8109
+ .finally(() => { modelQueryScratchPending.delete(wanted); });
8110
+ modelQueryScratchPending.set(wanted, build);
8111
+ return build;
8112
+ }
8113
+ /**
8114
+ * Warm the model-query scratch for one or more runtimes in the background so the
8115
+ * first agent switch to any of them answers instantly instead of paying the
8116
+ * runtime spin-up on the critical path. Fired when the agent picker opens (see
8117
+ * the `models.prefetch` command). Best-effort and de-duped: a runtime already
8118
+ * warm (or being warmed) is a no-op, and a spin-up failure is swallowed — the
8119
+ * normal models.list path will surface any real error when the user picks it.
8120
+ */
8121
+ function prefetchModels(runtimeIds) {
8122
+ const wanted = [];
8123
+ for (const id of runtimeIds) {
8124
+ let resolved;
8125
+ try {
8126
+ resolved = resolveRuntimeId(id);
8127
+ }
8128
+ catch {
8129
+ continue; // unknown/uninstalled agent — nothing to warm
8130
+ }
8131
+ if (wanted.includes(resolved))
8132
+ continue;
8133
+ const cached = modelQueryScratch.get(resolved);
8134
+ if (cached && openSessions.has(cached.id) && !sessionBusy(cached))
8135
+ continue;
8136
+ if (modelQueryScratchPending.has(resolved))
8137
+ continue;
8138
+ wanted.push(resolved);
8139
+ }
8140
+ // Warm serially, not in a burst: spinning up every agent subprocess at once
8141
+ // would spike a small node's memory/CPU right as the user is interacting. Each
8142
+ // build is cached (and de-duped) so this cost is paid at most once per runtime.
8143
+ void wanted.reduce((chain, id) => chain.then(() => sessionForModelQuery(id).then(() => undefined, () => undefined)), Promise.resolve());
8084
8144
  }
8085
8145
  async function createRepoSession(parsed, opts = {}) {
8086
8146
  const token = await resolveTokenForRepo(parsed.owner, parsed.repo);
@@ -8971,10 +9031,13 @@ app.get("/api/models", async (req, res, next) => {
8971
9031
  try {
8972
9032
  const requestedSessionId = typeof req.query.sessionId === "string" && req.query.sessionId ? req.query.sessionId : undefined;
8973
9033
  const requestedPath = typeof req.query.path === "string" ? req.query.path : undefined;
9034
+ const wantedRuntimeId = typeof req.query.runtimeId === "string" && req.query.runtimeId ? req.query.runtimeId : undefined;
8974
9035
  let record = requestedSessionId ? await resolveOrResumeSession(requestedSessionId, requestedPath) : active;
8975
9036
  if (requestedSessionId && !record)
8976
9037
  return res.status(404).json({ error: "Session not found" });
8977
- record ??= await sessionForModelQuery();
9038
+ if (!requestedSessionId && wantedRuntimeId && record?.runtimeId !== wantedRuntimeId)
9039
+ record = undefined;
9040
+ record ??= await sessionForModelQuery(wantedRuntimeId);
8978
9041
  const session = record.session;
8979
9042
  const current = session.getCurrentModel();
8980
9043
  const models = await publicModelsList(session, current);
@@ -8985,6 +9048,17 @@ app.get("/api/models", async (req, res, next) => {
8985
9048
  next(error);
8986
9049
  }
8987
9050
  });
9051
+ // Warm the per-runtime model-query scratch ahead of the first agent switch (see
9052
+ // prefetchModels). Fire-and-forget: returns immediately while the runtimes spin
9053
+ // up in the background, so the picker never blocks on it.
9054
+ app.post("/api/models/prefetch", (req, res) => {
9055
+ const ids = Array.isArray(req.body?.runtimeIds)
9056
+ ? req.body.runtimeIds.filter((id) => typeof id === "string" && !!id).slice(0, 16)
9057
+ : [];
9058
+ if (ids.length)
9059
+ prefetchModels(ids);
9060
+ res.json({ ok: true });
9061
+ });
8988
9062
  app.post("/api/models/select", async (req, res, next) => {
8989
9063
  try {
8990
9064
  const requestedSessionId = typeof req.body?.sessionId === "string" && req.body.sessionId ? req.body.sessionId : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.6.0-staging.84",
3
+ "version": "0.6.0-staging.85",
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.",