@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.
- package/dist/server.js +101 -27
- 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
|
-
|
|
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
|
-
//
|
|
8059
|
-
//
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
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
|
|
8066
|
-
if (
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
//
|
|
8074
|
-
//
|
|
8075
|
-
//
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
.
|
|
8082
|
-
|
|
8083
|
-
return
|
|
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
|
|
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