@bivy/bivy 0.18.11-staging.16 → 0.18.11-staging.2
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/bin/acp-shim.mjs +16 -142
- package/bin/agent-manifest.json +2 -2
- package/bin/bivy.mjs +27 -189
- package/bin/prune-sessions.mjs +0 -38
- package/dist/agent-instructions.js +0 -4
- package/dist/agents/claude-code/integration.js +1 -1
- package/dist/agents/claude-code/runtime.js +0 -15
- package/dist/agents/codex/integration.js +1 -1
- package/dist/agents/profiles.js +3 -3
- package/dist/apps/gateway.js +17 -53
- package/dist/apps/preview-shell.js +0 -2
- package/dist/apps/remote-preview.js +2 -4
- package/dist/apps/service.js +3 -18
- package/dist/apps/styles.css +1 -29
- package/dist/apps/types.js +0 -8
- package/dist/certification/generated.js +4 -4
- package/dist/control-plane-tasks.js +1 -6
- package/dist/controllers/app-commands.js +3 -7
- package/dist/controllers/fork-commands.js +20 -20
- package/dist/exec.js +4 -41
- package/dist/protocol/client-command-routes.js +1 -1
- package/dist/protocol/client-command-schemas.js +1 -4
- package/dist/run-tools.js +2 -15
- package/dist/runtime/protocol.js +4 -60
- package/dist/runtime/tool-call-map.js +0 -24
- package/dist/server.js +25 -289
- package/dist/session/event-log.js +3 -61
- package/dist/session/fork-standup.js +0 -5
- package/dist/session/fork.js +1 -5
- package/dist/session/sibling-client.js +3 -40
- package/dist/session/transcript-merge.js +1 -29
- package/dist/session/transcript-persistence.js +0 -4
- package/package.json +2 -2
- package/dist/runtime/model-ref.js +0 -33
- package/dist/session/delegation-cards.js +0 -28
- package/dist/session/delegations.js +0 -21
package/bin/acp-shim.mjs
CHANGED
|
@@ -136,31 +136,9 @@ let initialized = false;
|
|
|
136
136
|
let modelConfigId = "model";
|
|
137
137
|
// A model chosen before the ACP session existed, applied once it does.
|
|
138
138
|
let pendingModel = null;
|
|
139
|
-
// True while session/load replays history (see session.resume).
|
|
140
|
-
let replayingHistory = false;
|
|
141
|
-
// The config-option id carrying reasoning effort (ACP category "thought_level"),
|
|
142
|
-
// when the agent advertises one.
|
|
143
|
-
let thoughtConfigId = null;
|
|
144
139
|
// toolCallId -> { requestId, options } so a later bivy tool.decision answers the
|
|
145
140
|
// right ACP permission request with a concrete optionId.
|
|
146
141
|
const permissionRequests = new Map();
|
|
147
|
-
// Calls the human (or policy) already approved. An agent that performs an
|
|
148
|
-
// approved edit through fs/write_text_file (Grok does) must not be gated — and
|
|
149
|
-
// carded — a second time for the same file.
|
|
150
|
-
const approvedCalls = new Set();
|
|
151
|
-
|
|
152
|
-
/** The approved, still-running call whose inputs name `file`, if any. */
|
|
153
|
-
function approvedCallWriting(file) {
|
|
154
|
-
for (const id of approvedCalls) {
|
|
155
|
-
const state = toolCallState.get(id);
|
|
156
|
-
if (!state) { approvedCalls.delete(id); continue; }
|
|
157
|
-
const input = toolCallInput(state);
|
|
158
|
-
const paths = [input.path, input.file_path, input.filePath, ...(state.locations || []).map((l) => l?.path)].filter(Boolean);
|
|
159
|
-
const root = fs.realpathSync(cwd);
|
|
160
|
-
if (paths.some((p) => path.resolve(root, String(p)) === file || path.resolve(cwd, String(p)) === file)) return id;
|
|
161
|
-
}
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
142
|
const sandboxTier = process.env.BIVY_ACP_SANDBOX || "workspace-write";
|
|
165
143
|
|
|
166
144
|
function isWithin(root, candidate) {
|
|
@@ -212,9 +190,6 @@ function finishTurnDone() {
|
|
|
212
190
|
turnDrainTimer = null;
|
|
213
191
|
if (!turnDraining) return;
|
|
214
192
|
turnDraining = false;
|
|
215
|
-
ownCalls.clear();
|
|
216
|
-
childParent.clear();
|
|
217
|
-
approvedCalls.clear();
|
|
218
193
|
bivy({ type: "session.status", status: "idle" });
|
|
219
194
|
bivy({ type: "session.done" });
|
|
220
195
|
}
|
|
@@ -247,11 +222,6 @@ function mergeToolCallState(toolCallId, u) {
|
|
|
247
222
|
const next = {
|
|
248
223
|
kind: u.kind ?? prev.kind,
|
|
249
224
|
title: u.title ?? prev.title,
|
|
250
|
-
// Agents open a call titled with the tool's own name ("task",
|
|
251
|
-
// "spawn_subagent", "bash") and later retitle it with prose ("Count README
|
|
252
|
-
// lines"). Keep the first identifier-like title as the stable name, so a
|
|
253
|
-
// delegation still classifies as one after the retitle.
|
|
254
|
-
name: prev.name ?? (typeof u.title === "string" && /^[A-Za-z_][\w.:-]*$/.test(u.title) ? u.title : undefined),
|
|
255
225
|
rawInput: u.rawInput && typeof u.rawInput === "object" && Object.keys(u.rawInput).length ? u.rawInput : prev.rawInput,
|
|
256
226
|
locations: Array.isArray(u.locations) && u.locations.length ? u.locations : prev.locations,
|
|
257
227
|
content: content && content.length ? content : prev.content,
|
|
@@ -289,60 +259,7 @@ function toolCallInput(state) {
|
|
|
289
259
|
* both the node's bucket classifier and the client's own name-based heuristic,
|
|
290
260
|
* which both expect short tool-name-like tokens, not prose. */
|
|
291
261
|
function toolCallName(state) {
|
|
292
|
-
return (state.kind && KIND_TOOL_NAME[state.kind]) || state.
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// --- sub-agent sessions ------------------------------------------------------
|
|
296
|
-
// Agents that run sub-agents as ACP sessions of their own (Grok's
|
|
297
|
-
// spawn_subagent) stream the child's updates over this same connection, tagged
|
|
298
|
-
// with the CHILD's sessionId. Folding those into the parent turn glued the
|
|
299
|
-
// sub-agent's prose onto the parent's answer and listed its tools as the
|
|
300
|
-
// parent's. Route them instead: child tool calls carry `parentToolCallId` (the
|
|
301
|
-
// delegating call, so the transcript nests them) and child prose stays out of
|
|
302
|
-
// the parent reply — the delegation's own tool result carries its answer.
|
|
303
|
-
const childParent = new Map(); // child sessionId -> delegating toolCallId ("" if unknown)
|
|
304
|
-
// This turn's own (non-child) calls, kept after they complete: a background
|
|
305
|
-
// sub-agent's spawn call often finishes before the child's first update lands.
|
|
306
|
-
const ownCalls = new Map(); // toolCallId -> accumulated state
|
|
307
|
-
const DELEGATION_NAME = /agent|task|delegat/i;
|
|
308
|
-
|
|
309
|
-
function isDelegation(state) {
|
|
310
|
-
const input = state?.rawInput || {};
|
|
311
|
-
return DELEGATION_NAME.test(String(state?.name || "")) || typeof input.subagent_type === "string" || typeof input.prompt === "string";
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** The delegating call for a child session: an explicit link when the agent
|
|
315
|
-
* sent one (see linkChildSession), else the newest delegation-shaped call of
|
|
316
|
-
* this turn that no other child has claimed, else the newest open call. */
|
|
317
|
-
function parentCallFor(childSessionId) {
|
|
318
|
-
if (childParent.has(childSessionId)) return childParent.get(childSessionId);
|
|
319
|
-
const claimed = new Set(childParent.values());
|
|
320
|
-
const calls = [...ownCalls].reverse().filter(([id]) => !claimed.has(id));
|
|
321
|
-
const pick = calls.find(([, st]) => isDelegation(st)) ?? calls.find(([id]) => toolCallState.has(id));
|
|
322
|
-
const parent = pick?.[0] ?? "";
|
|
323
|
-
childParent.set(childSessionId, parent);
|
|
324
|
-
return parent;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** An agent-specific notification that names a child session and its parent
|
|
328
|
-
* (`child_session_id` / `parent_session_id`, plus the tool call id or the
|
|
329
|
-
* delegation's description) pins the link instead of the heuristic above. */
|
|
330
|
-
function linkChildSession(update) {
|
|
331
|
-
const child = update?.child_session_id ?? update?.childSessionId;
|
|
332
|
-
const parentSession = update?.parent_session_id ?? update?.parentSessionId;
|
|
333
|
-
if (!child || (parentSession && parentSession !== sessionId) || childParent.get(child)) return;
|
|
334
|
-
const byId = update.tool_call_id ?? update.toolCallId;
|
|
335
|
-
const match = byId && ownCalls.has(byId)
|
|
336
|
-
? byId
|
|
337
|
-
: [...ownCalls].reverse().find(([, st]) => update.description && (st.title === update.description || st.rawInput?.description === update.description))?.[0];
|
|
338
|
-
if (match) childParent.set(child, match);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/** Routing for an update from `sid`: null for this session, else the parent
|
|
342
|
-
* call id ("" when it can't be placed). */
|
|
343
|
-
function childRoute(sid) {
|
|
344
|
-
if (!sid || !sessionId || sid === sessionId) return null;
|
|
345
|
-
return parentCallFor(sid);
|
|
262
|
+
return (state.kind && KIND_TOOL_NAME[state.kind]) || state.title || state.kind || "tool";
|
|
346
263
|
}
|
|
347
264
|
|
|
348
265
|
async function ensureInitialized() {
|
|
@@ -374,33 +291,24 @@ function publishModels(result) {
|
|
|
374
291
|
// ACP model ids are `provider/model`; split the provider so Bivy can group and
|
|
375
292
|
// scope provider-specific settings the same way it does for other runtimes.
|
|
376
293
|
.map((m) => ({ ...m, provider: m.id.includes("/") ? m.id.split("/")[0] : "agent" }));
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const thoughtOption = options.find((o) => String(o?.category ?? "") === "thought_level");
|
|
380
|
-
const levels = (Array.isArray(thoughtOption?.options) ? thoughtOption.options : []).map((o) => String(o?.value ?? "")).filter(Boolean);
|
|
381
|
-
if (!models.length && !levels.length) return;
|
|
382
|
-
if (models.length) modelConfigId = String(modelOption?.id ?? "model");
|
|
383
|
-
if (levels.length) thoughtConfigId = String(thoughtOption.id);
|
|
294
|
+
if (!models.length) return;
|
|
295
|
+
modelConfigId = String(modelOption?.id ?? "model");
|
|
384
296
|
bivy({
|
|
385
297
|
type: "runtime.models",
|
|
386
298
|
models,
|
|
387
299
|
...(modelOption?.currentValue ? { currentModel: String(modelOption.currentValue) } : {}),
|
|
388
|
-
...(levels.length ? { thinking: { levels, ...(thoughtOption?.currentValue ? { current: String(thoughtOption.currentValue) } : {}) } } : {}),
|
|
389
300
|
});
|
|
390
301
|
}
|
|
391
302
|
|
|
392
303
|
// --- ACP → bivy: streamed session/update notifications ----------------------
|
|
393
304
|
function onSessionUpdate(params) {
|
|
394
305
|
const u = params?.update;
|
|
395
|
-
if (!u || typeof u !== "object"
|
|
306
|
+
if (!u || typeof u !== "object") return;
|
|
396
307
|
// Any update arriving while the turn is draining means the agent is still
|
|
397
308
|
// emitting the tail of this turn (see the drain note above) — reset the quiet
|
|
398
309
|
// window so session.done waits for it instead of sealing history short.
|
|
399
310
|
if (turnDraining) scheduleTurnDone();
|
|
400
311
|
const kind = String(u.sessionUpdate || "");
|
|
401
|
-
const parentCall = childRoute(params?.sessionId);
|
|
402
|
-
const child = parentCall !== null;
|
|
403
|
-
const nest = parentCall ? { parentToolCallId: parentCall } : {};
|
|
404
312
|
const textOf = (content) => {
|
|
405
313
|
if (!content) return "";
|
|
406
314
|
if (typeof content === "string") return content;
|
|
@@ -419,12 +327,12 @@ function onSessionUpdate(params) {
|
|
|
419
327
|
switch (kind) {
|
|
420
328
|
case "agent_message_chunk": {
|
|
421
329
|
const t = textOf(u.content);
|
|
422
|
-
if (t
|
|
330
|
+
if (t) bivy({ type: "message.delta", text: t });
|
|
423
331
|
break;
|
|
424
332
|
}
|
|
425
333
|
case "agent_thought_chunk": {
|
|
426
334
|
const t = textOf(u.content);
|
|
427
|
-
if (t
|
|
335
|
+
if (t) bivy({ type: "message.reasoning", text: t });
|
|
428
336
|
break;
|
|
429
337
|
}
|
|
430
338
|
case "tool_call": {
|
|
@@ -432,14 +340,12 @@ function onSessionUpdate(params) {
|
|
|
432
340
|
// shows the action; the result arrives via tool_call_update.
|
|
433
341
|
const toolCallId = String(u.toolCallId ?? u.id ?? "");
|
|
434
342
|
const state = mergeToolCallState(toolCallId, u);
|
|
435
|
-
|
|
436
|
-
bivy({ type: "tool.observe", toolCallId, name: toolCallName(state), input: toolCallInput(state), ...nest });
|
|
343
|
+
bivy({ type: "tool.observe", toolCallId, name: toolCallName(state), input: toolCallInput(state) });
|
|
437
344
|
break;
|
|
438
345
|
}
|
|
439
346
|
case "tool_call_update": {
|
|
440
347
|
const toolCallId = String(u.toolCallId ?? u.id ?? "");
|
|
441
348
|
const state = mergeToolCallState(toolCallId, u);
|
|
442
|
-
if (!child && ownCalls.has(toolCallId)) ownCalls.set(toolCallId, state);
|
|
443
349
|
const status = String(u.status || "");
|
|
444
350
|
if (status === "completed" || status === "failed") {
|
|
445
351
|
toolCallState.delete(toolCallId);
|
|
@@ -456,18 +362,17 @@ function onSessionUpdate(params) {
|
|
|
456
362
|
// the terminal frame's content when it has some, else the accumulated.
|
|
457
363
|
result: textOf(u.content) || textOf(state.content) || status,
|
|
458
364
|
isError: status === "failed",
|
|
459
|
-
...nest,
|
|
460
365
|
});
|
|
461
366
|
} else {
|
|
462
367
|
// Still running: forward the fuller name/input as it fills in so a live
|
|
463
368
|
// tool card isn't stuck with the sparse initial notification.
|
|
464
|
-
bivy({ type: "tool.update", toolCallId, name: toolCallName(state), input: toolCallInput(state)
|
|
369
|
+
bivy({ type: "tool.update", toolCallId, name: toolCallName(state), input: toolCallInput(state) });
|
|
465
370
|
}
|
|
466
371
|
break;
|
|
467
372
|
}
|
|
468
373
|
case "plan":
|
|
469
374
|
// Optional planning stream — fold into reasoning so nothing is lost.
|
|
470
|
-
if (
|
|
375
|
+
if (Array.isArray(u.entries)) bivy({ type: "message.reasoning", text: u.entries.map((e) => `• ${e.content ?? ""}`).join("\n") });
|
|
471
376
|
break;
|
|
472
377
|
default:
|
|
473
378
|
break;
|
|
@@ -486,9 +391,7 @@ async function onAgentRequest(id, method, params) {
|
|
|
486
391
|
const options = Array.isArray(params?.options) ? params.options : [];
|
|
487
392
|
permissionRequests.set(toolCallId, { requestId: id, options });
|
|
488
393
|
const state = mergeToolCallState(toolCallId, tc);
|
|
489
|
-
|
|
490
|
-
if (parentCall === null) ownCalls.set(toolCallId, state);
|
|
491
|
-
bivy({ type: "tool.call", toolCallId, name: toolCallName(state), input: toolCallInput(state), ...(parentCall ? { parentToolCallId: parentCall } : {}) });
|
|
394
|
+
bivy({ type: "tool.call", toolCallId, name: toolCallName(state), input: toolCallInput(state) });
|
|
492
395
|
return;
|
|
493
396
|
}
|
|
494
397
|
case "fs/read_text_file": {
|
|
@@ -510,15 +413,9 @@ async function onAgentRequest(id, method, params) {
|
|
|
510
413
|
try {
|
|
511
414
|
if (sandboxTier === "read-only") throw new Error("writes are disabled by the read-only sandbox");
|
|
512
415
|
const file = workspacePath(params?.path, { write: true });
|
|
513
|
-
if (approvedCallWriting(file)) {
|
|
514
|
-
fs.writeFileSync(file, String(params?.content ?? ""));
|
|
515
|
-
agentReply(id, {});
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
416
|
const toolCallId = `acp-fs-write-${id}`;
|
|
519
417
|
permissionRequests.set(toolCallId, { kind: "fs-write", requestId: id, file, content: String(params?.content ?? "") });
|
|
520
|
-
|
|
521
|
-
bivy({ type: "tool.call", toolCallId, name: "write", input: { path: file }, ...(parentCall ? { parentToolCallId: parentCall } : {}) });
|
|
418
|
+
bivy({ type: "tool.call", toolCallId, name: "write", input: { path: file } });
|
|
522
419
|
} catch (e) {
|
|
523
420
|
agentReplyError(id, -32000, `write failed: ${e.message}`);
|
|
524
421
|
}
|
|
@@ -552,7 +449,6 @@ createInterface({ input: agent.stdout }).on("line", (line) => {
|
|
|
552
449
|
if (msg.id !== undefined && msg.method) { void onAgentRequest(msg.id, msg.method, msg.params); return; }
|
|
553
450
|
// Notification (method, no id).
|
|
554
451
|
if (msg.method === "session/update") onSessionUpdate(msg.params);
|
|
555
|
-
else if (msg.params?.update) linkChildSession(msg.params.update);
|
|
556
452
|
});
|
|
557
453
|
|
|
558
454
|
/**
|
|
@@ -611,14 +507,7 @@ async function onBivyCommand(msg) {
|
|
|
611
507
|
// hanging the reopen with no watchdog to recover it (resume isn't a
|
|
612
508
|
// "working" turn). On timeout/failure we fall back to a fresh session so
|
|
613
509
|
// the chat still opens instead of spinning on "Fetching transcript…".
|
|
614
|
-
|
|
615
|
-
// session/load. Bivy already has that history; forwarding the replay
|
|
616
|
-
// re-recorded past thinking (and could fold old text onto the last
|
|
617
|
-
// reply) as if it were live.
|
|
618
|
-
replayingHistory = true;
|
|
619
|
-
let res;
|
|
620
|
-
try { res = await agentRequest("session/load", { sessionId: ref, cwd, mcpServers }, { timeoutMs: 30_000 }); }
|
|
621
|
-
finally { replayingHistory = false; }
|
|
510
|
+
const res = await agentRequest("session/load", { sessionId: ref, cwd, mcpServers }, { timeoutMs: 30_000 });
|
|
622
511
|
sessionId = res?.sessionId ?? ref;
|
|
623
512
|
publishModels(res);
|
|
624
513
|
} catch (error) {
|
|
@@ -665,22 +554,16 @@ async function onBivyCommand(msg) {
|
|
|
665
554
|
permissionRequests.delete(msg.toolCallId);
|
|
666
555
|
const allow = msg.decision !== "deny";
|
|
667
556
|
if (entry.kind === "fs-write") {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
try { fs.writeFileSync(entry.file, entry.content); }
|
|
673
|
-
catch (e) { error = `write failed: ${e.message}`; }
|
|
557
|
+
if (!allow) agentReplyError(entry.requestId, -32001, String(msg.reason || "write denied by Bivy policy"));
|
|
558
|
+
else {
|
|
559
|
+
try { fs.writeFileSync(entry.file, entry.content); agentReply(entry.requestId, {}); }
|
|
560
|
+
catch (e) { agentReplyError(entry.requestId, -32000, `write failed: ${e.message}`); }
|
|
674
561
|
}
|
|
675
|
-
if (error) agentReplyError(entry.requestId, allow ? -32000 : -32001, error);
|
|
676
|
-
else agentReply(entry.requestId, {});
|
|
677
|
-
bivy({ type: "tool.result", toolCallId: msg.toolCallId, name: "write", result: error || `Wrote ${entry.file}`, isError: Boolean(error) });
|
|
678
562
|
return;
|
|
679
563
|
}
|
|
680
564
|
// Pick an ACP option matching the human's choice by its `kind`
|
|
681
565
|
// (allow_once/allow_always vs reject_once/reject_always); fall back to the
|
|
682
566
|
// first option, or a cancelled outcome when nothing fits.
|
|
683
|
-
if (allow) approvedCalls.add(msg.toolCallId);
|
|
684
567
|
const want = allow ? /^allow/ : /^reject/;
|
|
685
568
|
const opt = entry.options.find((o) => want.test(String(o.kind || ""))) ?? entry.options[0];
|
|
686
569
|
if (opt && opt.optionId !== undefined) agentReply(entry.requestId, { outcome: { outcome: "selected", optionId: opt.optionId } });
|
|
@@ -701,15 +584,6 @@ async function onBivyCommand(msg) {
|
|
|
701
584
|
bivy({ replyTo: id, ok: true });
|
|
702
585
|
return;
|
|
703
586
|
}
|
|
704
|
-
case "thinking.set": {
|
|
705
|
-
const level = String(msg.level ?? "").trim();
|
|
706
|
-
if (sessionId && thoughtConfigId && level) {
|
|
707
|
-
try { await agentRequest("session/set_config_option", { sessionId, configId: thoughtConfigId, value: level }, { timeoutMs: 15_000 }); }
|
|
708
|
-
catch (e) { bivy({ type: "runtime.debug", message: `acp set thought level failed: ${e instanceof Error ? e.message : String(e)}` }); }
|
|
709
|
-
}
|
|
710
|
-
if (id !== undefined) bivy({ replyTo: id, ok: true });
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
587
|
case "session.abort": {
|
|
714
588
|
// A pending drain must not fire session.done after the user cancelled —
|
|
715
589
|
// the turn is being torn down, not finishing on its own.
|
package/bin/agent-manifest.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"hidden": false,
|
|
23
23
|
"supportTier": "supported",
|
|
24
24
|
"certification": "release-tested",
|
|
25
|
-
"testedVersion": "1.18.
|
|
25
|
+
"testedVersion": "1.18.32",
|
|
26
26
|
"headlessFlags": [
|
|
27
27
|
"run",
|
|
28
28
|
"-s"
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"hidden": false,
|
|
192
192
|
"supportTier": "supported",
|
|
193
193
|
"certification": "release-tested",
|
|
194
|
-
"testedVersion": "1.0.
|
|
194
|
+
"testedVersion": "1.0.41",
|
|
195
195
|
"headlessFlags": [
|
|
196
196
|
"--always-approve",
|
|
197
197
|
"-p",
|
package/bin/bivy.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
35
35
|
import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash, verify as cryptoVerify } from "node:crypto";
|
|
36
36
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
37
37
|
import vm from "node:vm";
|
|
38
|
-
import {
|
|
38
|
+
import { selectStaleSessions, sessionActivityMs } from "./prune-sessions.mjs";
|
|
39
39
|
import { nativeResumeRef, resolveSessionsLimit, truncateSavedSessions } from "./sessions-list.mjs";
|
|
40
40
|
import { renderManagedBlock, upsertManagedBlock, removeManagedBlock, rcFileForShell } from "./shim-path.mjs";
|
|
41
41
|
import { removeInstallAndState } from "./uninstall-paths.mjs";
|
|
@@ -1827,7 +1827,7 @@ function cmdCompletions(args = []) {
|
|
|
1827
1827
|
const shell = (args[0] || "").toLowerCase();
|
|
1828
1828
|
const commands = [
|
|
1829
1829
|
"run", "runs", "sessions", "ls", "resume", "promote", "rename", "nodes", "agent", "agents", "agents:install", "shim", "takeover", "token", "exec",
|
|
1830
|
-
"send", "attach", "suggest", "
|
|
1830
|
+
"send", "attach", "suggest", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "capabilities", "logs", "login", "logout", "signout", "provider", "model",
|
|
1831
1831
|
"update", "update:log", "audit", "automation", "config", "plugin", "open", "service", "secrets", "voice", "link", "relay:setup",
|
|
1832
1832
|
"github:connect", "github:app-create", "github:app-connect", "github:app-sync", "prune", "uninstall", "help", "version",
|
|
1833
1833
|
];
|
|
@@ -2462,131 +2462,6 @@ async function cmdSuggest(args = []) {
|
|
|
2462
2462
|
console.log(c.green("Suggested in the chat. The user can start it in one tap."));
|
|
2463
2463
|
}
|
|
2464
2464
|
|
|
2465
|
-
// Cross-agent / cross-machine delegation (docs/agent-delegation.md): hand a
|
|
2466
|
-
// self-contained task from this session to another agent, optionally on another
|
|
2467
|
-
// account machine, and get its answer back. Agent-agnostic — any agent with a
|
|
2468
|
-
// shell can call it — and meant for when the user asks for another agent/machine.
|
|
2469
|
-
const DELEGATE_USAGE = `Usage: bivy delegate "<task>" [--agent <id>] [--machine <name>] [--model <model>] [--repo owner/repo] [--approval never|risky|always|autonomous] [--sandbox <tier>] [--wait [seconds]] [--json]
|
|
2470
|
-
bivy delegate "<task>" --to <agent>[@<machine>],<agent>[@<machine>],… [--wait [seconds]]
|
|
2471
|
-
bivy delegate machines [--json]
|
|
2472
|
-
bivy delegate status <run-id> [--json]
|
|
2473
|
-
bivy delegate wait <run-id> [--timeout <seconds>] [--json]
|
|
2474
|
-
|
|
2475
|
-
Hands a self-contained task to another agent (and/or another machine on your account) as a
|
|
2476
|
-
delegated Run tied to this session ($BIVY_SESSION_ID). With --wait (or 'wait'), prints the
|
|
2477
|
-
child's final answer and any branch/PR it produced once it finishes. --to sends the same task
|
|
2478
|
-
to several agents/machines at once (up to 3) so the results can be compared.
|
|
2479
|
-
'machines' lists your machines and the agents installed on each.`;
|
|
2480
|
-
|
|
2481
|
-
async function cmdDelegate(args = []) {
|
|
2482
|
-
if (!args.length || args.some((a) => a === "-h" || a === "--help")) { console.log(DELEGATE_USAGE); return; }
|
|
2483
|
-
const flagsWithValue = new Set(["--session", "--agent", "--machine", "--model", "--repo", "--timeout", "--to", "--approval", "--sandbox"]);
|
|
2484
|
-
// Children inherit this session's approval/sandbox unless set here.
|
|
2485
|
-
const safetyFlags = () => {
|
|
2486
|
-
const safety = { approval: flag("--approval"), sandbox: flag("--sandbox") };
|
|
2487
|
-
return safety.approval || safety.sandbox ? { safety } : {};
|
|
2488
|
-
};
|
|
2489
|
-
const flag = (name) => {
|
|
2490
|
-
const i = args.indexOf(name);
|
|
2491
|
-
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
2492
|
-
};
|
|
2493
|
-
const json = args.includes("--json");
|
|
2494
|
-
const waitIndex = args.indexOf("--wait");
|
|
2495
|
-
const waitArg = waitIndex >= 0 && /^\d+$/.test(args[waitIndex + 1] ?? "") ? args[waitIndex + 1] : undefined;
|
|
2496
|
-
const positional = args.filter((a, i) => !a.startsWith("--") && !(i > 0 && flagsWithValue.has(args[i - 1])) && !(waitArg && i === waitIndex + 1));
|
|
2497
|
-
const sessionId = resolveAttachSessionId({ sessionFlag: flag("--session"), env: process.env });
|
|
2498
|
-
if (!sessionId) { console.error(c.red("No session id. Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.")); process.exit(1); return; }
|
|
2499
|
-
const config = loadConfig();
|
|
2500
|
-
if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
|
|
2501
|
-
let token;
|
|
2502
|
-
try { token = await localDeviceToken(config); } catch { token = undefined; }
|
|
2503
|
-
const base = `${url(config)}/api/session/${encodeURIComponent(sessionId)}/delegated-runs`;
|
|
2504
|
-
const call = async (path, body) => {
|
|
2505
|
-
const res = await fetch(`${base}${path}`, {
|
|
2506
|
-
method: body ? "POST" : "GET",
|
|
2507
|
-
headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
2508
|
-
...(body ? { body: JSON.stringify(body) } : {}),
|
|
2509
|
-
});
|
|
2510
|
-
const data = await res.json().catch(() => ({}));
|
|
2511
|
-
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
|
2512
|
-
return data;
|
|
2513
|
-
};
|
|
2514
|
-
// needs_attention = parked by the machine's policy; it won't move on its own.
|
|
2515
|
-
const terminal = (run) => ["succeeded", "failed", "cancelled", "needs_attention"].includes(run?.status);
|
|
2516
|
-
// The node caps one wait at 300s; loop until the child ends or our budget does.
|
|
2517
|
-
const waitFor = async (runId, seconds) => {
|
|
2518
|
-
const deadline = Date.now() + seconds * 1000;
|
|
2519
|
-
let run = await call(`/${encodeURIComponent(runId)}`);
|
|
2520
|
-
while (!terminal(run) && Date.now() < deadline) {
|
|
2521
|
-
const slice = Math.max(1, Math.min(300, Math.ceil((deadline - Date.now()) / 1000)));
|
|
2522
|
-
run = await call(`/${encodeURIComponent(runId)}/wait`, { timeoutSeconds: slice });
|
|
2523
|
-
}
|
|
2524
|
-
return run;
|
|
2525
|
-
};
|
|
2526
|
-
const print = (run) => {
|
|
2527
|
-
if (json) { console.log(JSON.stringify(run, null, 2)); return; }
|
|
2528
|
-
console.log(`Run ${run.runId}: ${run.status}${terminal(run) ? "" : " (still running — 'bivy delegate wait " + run.runId + "')"}`);
|
|
2529
|
-
const refs = run.references || {};
|
|
2530
|
-
if (refs.failure) console.log(`Failure: ${refs.failure}`);
|
|
2531
|
-
if (refs.branch) console.log(`Branch: ${refs.branch}`);
|
|
2532
|
-
if (refs.prUrl) console.log(`PR: ${refs.prUrl}`);
|
|
2533
|
-
if (refs.sessionId) console.log(`Child session: ${refs.sessionId}`);
|
|
2534
|
-
if (run.answer) console.log(`\nAnswer:\n${run.answer}`);
|
|
2535
|
-
};
|
|
2536
|
-
try {
|
|
2537
|
-
const [action, runId] = positional;
|
|
2538
|
-
if (action === "machines" && positional.length === 1) {
|
|
2539
|
-
const res = await fetch(`${url(config)}/api/machines`, { headers: token ? { authorization: `Bearer ${token}` } : {} });
|
|
2540
|
-
const data = await res.json().catch(() => ({}));
|
|
2541
|
-
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
|
2542
|
-
if (json) { console.log(JSON.stringify(data.machines, null, 2)); return; }
|
|
2543
|
-
for (const m of data.machines || []) {
|
|
2544
|
-
const agents = m.agents ? m.agents.map((a) => a.id).join(", ") || "none" : m.error ? `unknown (${m.error})` : "unknown while offline";
|
|
2545
|
-
console.log(`${m.name}${m.self ? " (this machine)" : ""} — ${m.online ? "online" : "offline"} — agents: ${agents}`);
|
|
2546
|
-
}
|
|
2547
|
-
return;
|
|
2548
|
-
}
|
|
2549
|
-
if (action === "status" && runId) return print(await call(`/${encodeURIComponent(runId)}`));
|
|
2550
|
-
if (action === "wait" && runId) return print(await waitFor(runId, Number(flag("--timeout") ?? 600)));
|
|
2551
|
-
const instructions = positional.join(" ").trim();
|
|
2552
|
-
if (!instructions) { console.error(c.red(DELEGATE_USAGE)); process.exit(1); return; }
|
|
2553
|
-
// Fan-out: the same task to several agent@machine targets, one group, so the
|
|
2554
|
-
// parent's transcript shows them side by side to compare.
|
|
2555
|
-
const to = flag("--to");
|
|
2556
|
-
if (to) {
|
|
2557
|
-
const targets = to.split(",").map((t) => t.trim()).filter(Boolean).map((t) => {
|
|
2558
|
-
const [agent, machine] = t.split("@");
|
|
2559
|
-
return { agent: agent ? governedChatAgentId(agent) : undefined, machine: machine || undefined, label: t };
|
|
2560
|
-
});
|
|
2561
|
-
const group = `fanout-${Date.now().toString(36)}`;
|
|
2562
|
-
const runs = [];
|
|
2563
|
-
for (const target of targets) {
|
|
2564
|
-
runs.push({ target, run: await call("", { instructions, agent: target.agent, machine: target.machine, model: flag("--model"), repo: flag("--repo"), ...safetyFlags(), group }) });
|
|
2565
|
-
}
|
|
2566
|
-
if (waitIndex < 0) {
|
|
2567
|
-
if (json) console.log(JSON.stringify(runs.map((r) => r.run), null, 2));
|
|
2568
|
-
else for (const { target, run } of runs) console.log(`${target.label}: run ${run.runId} (${run.status})`);
|
|
2569
|
-
return;
|
|
2570
|
-
}
|
|
2571
|
-
const finished = await Promise.all(runs.map(async ({ target, run }) => ({ target, run: await waitFor(run.runId, Number(waitArg ?? 600)) })));
|
|
2572
|
-
if (json) { console.log(JSON.stringify(finished.map((f) => f.run), null, 2)); return; }
|
|
2573
|
-
for (const { target, run } of finished) { console.log(`\n── ${target.label}`); print(run); }
|
|
2574
|
-
return;
|
|
2575
|
-
}
|
|
2576
|
-
const agent = flag("--agent");
|
|
2577
|
-
const started = await call("", { instructions, agent: agent ? governedChatAgentId(agent) : undefined, machine: flag("--machine"), model: flag("--model"), repo: flag("--repo"), ...safetyFlags() });
|
|
2578
|
-
if (waitIndex < 0) {
|
|
2579
|
-
if (!json) console.log(`Delegated as run ${started.runId} (${started.status}). Follow it with 'bivy delegate wait ${started.runId}'.`);
|
|
2580
|
-
else print(started);
|
|
2581
|
-
return;
|
|
2582
|
-
}
|
|
2583
|
-
print(await waitFor(started.runId, Number(waitArg ?? 600)));
|
|
2584
|
-
} catch (error) {
|
|
2585
|
-
console.error(c.red(`Delegate failed: ${error?.message || String(error)}`));
|
|
2586
|
-
process.exit(1);
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
2465
|
// Universal app publishing: every agent that can write JSON and run a command
|
|
2591
2466
|
// uses the same contract. Relative static directories are manifest-relative.
|
|
2592
2467
|
async function cmdApp(args = []) {
|
|
@@ -2597,7 +2472,7 @@ async function cmdApp(args = []) {
|
|
|
2597
2472
|
bivy app remove <app-id> [--session <id>]
|
|
2598
2473
|
bivy app shot [app-id] [--widths 390,1280] [--themes light,dark] [--path /page] [--session <id>]
|
|
2599
2474
|
bivy app present [view] [--path /page] [--note "…"] [--session <id>]
|
|
2600
|
-
bivy app share [app-id] [--view <view>] [--
|
|
2475
|
+
bivy app share [app-id] [--view <view>] [--session <id>]
|
|
2601
2476
|
bivy app notes [app-id] [--view <view>] [--since <time>] [--session <id>]
|
|
2602
2477
|
bivy app run [--name <name>] [--restart-on-change] [--session <id>] -- <command> [args…]
|
|
2603
2478
|
bivy app click <x> <y> [--right|--middle] [--double] [--app <app>]
|
|
@@ -2643,10 +2518,9 @@ the chat with the app at phone width (and before/after), and it opens the live
|
|
|
2643
2518
|
preview. Use it when you finish a visible change, or when the user asks to see
|
|
2644
2519
|
it. [view] is an app or view name or ID (default: the one opened last).
|
|
2645
2520
|
"share" mints a reusable link to one web view and prints its URL and expiry
|
|
2646
|
-
(
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
reviewer tools (Point, Draw, notes): people see just the app. Anyone who has the link can use the app, including a live
|
|
2521
|
+
(24 hours, or until Revoke access in Apps, the app is removed or the machine
|
|
2522
|
+
restarts). [app-id] is an app ID or name, --view a view ID or name (default: the
|
|
2523
|
+
one opened last). Anyone who has the link can use the app, including a live
|
|
2650
2524
|
server's backend, until it expires or is revoked: only post it where everyone
|
|
2651
2525
|
who can read it may use the app.
|
|
2652
2526
|
"notes" prints the notes people left through share links (Leave a note), as
|
|
@@ -2683,17 +2557,6 @@ Web previews require operator setup; see docs/apps.md.`);
|
|
|
2683
2557
|
rest.splice(i, 2);
|
|
2684
2558
|
}
|
|
2685
2559
|
}
|
|
2686
|
-
if (action === "share") {
|
|
2687
|
-
const i = rest.indexOf("--for");
|
|
2688
|
-
if (i >= 0) {
|
|
2689
|
-
const value = rest[i + 1];
|
|
2690
|
-
if (!["1h", "1d", "7d"].includes(value)) throw new Error("--for takes 1h, 1d or 7d.");
|
|
2691
|
-
shot.duration = value;
|
|
2692
|
-
rest.splice(i, 2);
|
|
2693
|
-
}
|
|
2694
|
-
const viewOnly = rest.indexOf("--view-only");
|
|
2695
|
-
if (viewOnly >= 0) { shot.controls = false; rest.splice(viewOnly, 1); }
|
|
2696
|
-
}
|
|
2697
2560
|
if (action === "notes") {
|
|
2698
2561
|
const i = rest.indexOf("--since");
|
|
2699
2562
|
if (i >= 0) {
|
|
@@ -2793,7 +2656,7 @@ async function appRequest(action, body, { print = true } = {}) {
|
|
|
2793
2656
|
if (action === "share") {
|
|
2794
2657
|
const { type: _type, requestId: _requestId, ...link } = result;
|
|
2795
2658
|
console.log(JSON.stringify({ ...link, expires: new Date(link.expiresAt).toISOString() }, null, 2));
|
|
2796
|
-
console.error("Anyone with this link can use the app, including its live backend, until it expires or
|
|
2659
|
+
console.error("Anyone with this link can use the app, including its live backend, until it expires or you revoke it (Apps → Revoke access).");
|
|
2797
2660
|
return;
|
|
2798
2661
|
}
|
|
2799
2662
|
if (print) console.log(JSON.stringify(result, null, 2));
|
|
@@ -3174,11 +3037,6 @@ ${c.bold("bivy prune")} — remove old sessions, --clone workspaces, and git wor
|
|
|
3174
3037
|
is never pruned while the node is running, regardless of --keep/--older-than —
|
|
3175
3038
|
only idle worktrees are removed. If the node is down, nothing is live to guard.
|
|
3176
3039
|
|
|
3177
|
-
A session and its worktree are removed together: pruning a worktree also
|
|
3178
|
-
removes the sessions that ran in it, pruning a session also removes its
|
|
3179
|
-
worktree (once no remaining session uses it), and sessions whose worktree is
|
|
3180
|
-
already gone are removed — so no session is left pointing at a deleted checkout.
|
|
3181
|
-
|
|
3182
3040
|
Paths (default to the installed node)
|
|
3183
3041
|
--data-dir <dir> node data dir (default: this install's .bivy, or $BIVY_DATA_DIR)
|
|
3184
3042
|
--workspace <dir> also scan this workspace for worktrees (default: configured workspace)
|
|
@@ -3246,41 +3104,12 @@ async function cmdPrune(args = []) {
|
|
|
3246
3104
|
const now = Date.now();
|
|
3247
3105
|
const plan = [];
|
|
3248
3106
|
let worktreeGuard = { reachable: false, protected: 0 };
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
// Worktrees: keep the newest N overall (across all roots), matching the node
|
|
3256
|
-
// prune script. Scan roots: the data dir plus the configured workspace repo.
|
|
3257
|
-
const worktreeRoots = findWorktreeRoots([dataDir, workspace].filter(Boolean));
|
|
3258
|
-
const staleWorktrees = doWorktrees ? selectStale(worktreeRoots.flatMap((r) => pruneListEntries(r, "dir")), keepFor("worktrees"), ageMs, now) : [];
|
|
3259
|
-
// Guard: never delete a worktree a live agent is using. Ask the daemon what's
|
|
3260
|
-
// live (it owns every runtime and PTY); protected worktrees drop out of the
|
|
3261
|
-
// removal set entirely, so an aggressive --older-than can't nuke a running
|
|
3262
|
-
// agent's checkout. Node down ⇒ nothing live ⇒ policy alone is safe.
|
|
3263
|
-
const live = await liveWorktreePaths(config);
|
|
3264
|
-
const isLive = (p) => isLiveWorktree(p, live.paths);
|
|
3265
|
-
const idleWorktrees = staleWorktrees.filter((e) => !isLive(e.path));
|
|
3266
|
-
worktreeGuard = { reachable: live.reachable, protected: staleWorktrees.length - idleWorktrees.length };
|
|
3267
|
-
// A session and its worktree go together: removing one without the other left
|
|
3268
|
-
// sessions pointing at a deleted checkout (or checkouts nobody can reach).
|
|
3269
|
-
const linked = linkSessionWorktrees({
|
|
3270
|
-
sessions: allSessions,
|
|
3271
|
-
staleSessions: selectedSessions,
|
|
3272
|
-
staleWorktrees: idleWorktrees.map((e) => e.path),
|
|
3273
|
-
isLive,
|
|
3274
|
-
exists: (p) => fs.existsSync(p),
|
|
3275
|
-
});
|
|
3276
|
-
const staleSessions = linked.sessions;
|
|
3277
|
-
const worktreeMtime = new Map(idleWorktrees.map((e) => [path.resolve(e.path), e.mtimeMs]));
|
|
3278
|
-
const worktreesToRemove = linked.worktrees.map((p) => {
|
|
3279
|
-
let mtimeMs = worktreeMtime.get(p);
|
|
3280
|
-
if (mtimeMs === undefined) { try { mtimeMs = fs.statSync(p).mtimeMs; } catch { mtimeMs = now; } }
|
|
3281
|
-
return { path: p, mtimeMs };
|
|
3282
|
-
}).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
3283
|
-
if (doSessions || staleSessions.length) {
|
|
3107
|
+
if (doSessions) {
|
|
3108
|
+
// Sessions live in the metadata index (all agents) plus each agent's own
|
|
3109
|
+
// transcript store — not just .bivy/pi/sessions. Select from metadata so
|
|
3110
|
+
// shim/native (Claude Code, Codex, …) sessions are actually covered; the old
|
|
3111
|
+
// pi-sessions-only scan silently no-oped for every terminal-started session.
|
|
3112
|
+
const staleSessions = selectStaleSessions(loadMetadataSessions(dataDir).sessions, keepFor("sessions"), ageMs, now);
|
|
3284
3113
|
plan.push({
|
|
3285
3114
|
kind: "sessions",
|
|
3286
3115
|
root: metadataFilePath(dataDir),
|
|
@@ -3303,8 +3132,20 @@ async function cmdPrune(args = []) {
|
|
|
3303
3132
|
const dir = path.join(dataDir, "workspaces");
|
|
3304
3133
|
plan.push({ kind: "workspaces", root: dir, remove: selectStale(pruneListEntries(dir, "dir"), keepFor("workspaces"), ageMs, now) });
|
|
3305
3134
|
}
|
|
3306
|
-
if (doWorktrees
|
|
3307
|
-
|
|
3135
|
+
if (doWorktrees) {
|
|
3136
|
+
// Keep the newest N worktrees overall (across all roots), matching the node
|
|
3137
|
+
// prune script. Scan roots: the data dir plus the configured workspace repo.
|
|
3138
|
+
const roots = findWorktreeRoots([dataDir, workspace].filter(Boolean));
|
|
3139
|
+
const all = roots.flatMap((r) => pruneListEntries(r, "dir"));
|
|
3140
|
+
const stale = selectStale(all, keepFor("worktrees"), ageMs, now);
|
|
3141
|
+
// Guard: never delete a worktree a live agent is using. Ask the daemon what's
|
|
3142
|
+
// live (it owns every runtime and PTY); protected worktrees drop out of the
|
|
3143
|
+
// removal set entirely, so an aggressive --older-than can't nuke a running
|
|
3144
|
+
// agent's checkout. Node down ⇒ nothing live ⇒ policy alone is safe.
|
|
3145
|
+
const live = await liveWorktreePaths(config);
|
|
3146
|
+
const remove = stale.filter((e) => !isLiveWorktree(e.path, live.paths));
|
|
3147
|
+
worktreeGuard = { reachable: live.reachable, protected: stale.length - remove.length };
|
|
3148
|
+
plan.push({ kind: "worktrees", root: roots.join(", ") || "(none found)", remove });
|
|
3308
3149
|
}
|
|
3309
3150
|
const total = plan.reduce((n, p) => n + p.remove.length, 0);
|
|
3310
3151
|
|
|
@@ -5615,9 +5456,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
|
|
|
5615
5456
|
case "suggest":
|
|
5616
5457
|
await cmdSuggest(args);
|
|
5617
5458
|
break;
|
|
5618
|
-
case "delegate":
|
|
5619
|
-
await cmdDelegate(args);
|
|
5620
|
-
break;
|
|
5621
5459
|
case "completions":
|
|
5622
5460
|
case "completion":
|
|
5623
5461
|
cmdCompletions(args);
|