@bivy/bivy 0.18.11-staging.12 → 0.18.11-staging.14
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/bivy.mjs +2 -1
- package/dist/run-tools.js +5 -2
- package/dist/server.js +35 -16
- package/dist/session/sibling-client.js +9 -0
- package/package.json +1 -1
package/bin/bivy.mjs
CHANGED
|
@@ -2506,7 +2506,8 @@ async function cmdDelegate(args = []) {
|
|
|
2506
2506
|
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
|
2507
2507
|
return data;
|
|
2508
2508
|
};
|
|
2509
|
-
|
|
2509
|
+
// needs_attention = parked by the machine's policy; it won't move on its own.
|
|
2510
|
+
const terminal = (run) => ["succeeded", "failed", "cancelled", "needs_attention"].includes(run?.status);
|
|
2510
2511
|
// The node caps one wait at 300s; loop until the child ends or our budget does.
|
|
2511
2512
|
const waitFor = async (runId, seconds) => {
|
|
2512
2513
|
const deadline = Date.now() + seconds * 1000;
|
package/dist/run-tools.js
CHANGED
|
@@ -12,6 +12,9 @@ export const RUN_TOOL_LIMITS = {
|
|
|
12
12
|
maxAnswer: 8_000,
|
|
13
13
|
};
|
|
14
14
|
const TERMINAL = new Set(["succeeded", "failed", "cancelled"]);
|
|
15
|
+
// Parked by the node's policy (usage limit, missing credentials): it won't
|
|
16
|
+
// progress without a person, so waiting ends there and the reason comes back.
|
|
17
|
+
const SETTLED = new Set([...TERMINAL, "needs_attention"]);
|
|
15
18
|
const STATUSES = new Set(["pending", "claimed", "running", "waiting", "needs_attention", "succeeded", "failed", "cancelled"]);
|
|
16
19
|
const SOURCE_PREFIX = "agent-delegation:v1:";
|
|
17
20
|
const bounded = (v, n) => typeof v === "string" && v ? v.slice(0, n) : undefined;
|
|
@@ -109,7 +112,7 @@ export class RunDelegationService {
|
|
|
109
112
|
if (!raw || provenance?.parentSessionId !== sessionId)
|
|
110
113
|
throw new Error("delegated Run not found");
|
|
111
114
|
const value = safeRun(raw, provenance);
|
|
112
|
-
if (
|
|
115
|
+
if (SETTLED.has(value.status) && this.backend.answer) {
|
|
113
116
|
// Best-effort: an unreachable machine still leaves status + references.
|
|
114
117
|
const answer = cached?.value.answer ?? await this.backend.answer(raw).catch(() => undefined);
|
|
115
118
|
if (answer)
|
|
@@ -155,7 +158,7 @@ export class RunDelegationService {
|
|
|
155
158
|
if (signal?.aborted)
|
|
156
159
|
throw signal.reason ?? new Error("wait cancelled; the child Run may still be running");
|
|
157
160
|
const result = await this.authorized(sessionId, runId, true);
|
|
158
|
-
if (
|
|
161
|
+
if (SETTLED.has(result.status))
|
|
159
162
|
return result;
|
|
160
163
|
if (this.now() >= deadline)
|
|
161
164
|
return { ...result, timedOut: true, wait: { timedOut: true, childContinues: true } };
|
package/dist/server.js
CHANGED
|
@@ -545,7 +545,10 @@ const runDelegation = new RunDelegationService({
|
|
|
545
545
|
method: "POST",
|
|
546
546
|
body: JSON.stringify({
|
|
547
547
|
title: "Delegated Run",
|
|
548
|
-
body
|
|
548
|
+
// Only the target machine can open its Run body, so seal with ITS room
|
|
549
|
+
// key: this machine's own for local work, the sibling's (learned over
|
|
550
|
+
// the sealed pairing handshake) for another machine.
|
|
551
|
+
body: `bivy-room-v1:${targetId}:${seal(targetId === identity.nodeId ? pairingStore.roomKey() : await siblingRoomKey(targetId), input.instructions)}`,
|
|
549
552
|
repo: input.repo,
|
|
550
553
|
node: machine,
|
|
551
554
|
runtimeId: input.agent,
|
|
@@ -605,6 +608,19 @@ async function siblingRequest(nodeId, command, timeoutMs = 20_000) {
|
|
|
605
608
|
client.close();
|
|
606
609
|
}
|
|
607
610
|
}
|
|
611
|
+
/** Another machine's room key, from a sealed pairing with it. */
|
|
612
|
+
async function siblingRoomKey(nodeId) {
|
|
613
|
+
if (!sessionAdvertiseTarget)
|
|
614
|
+
throw new Error("This machine is not signed in to an account.");
|
|
615
|
+
const client = new SiblingClient({ controlPlaneUrl: sessionAdvertiseTarget.controlPlaneUrl, enrollmentToken: sessionAdvertiseTarget.enrollmentToken, siblingNodeId: nodeId, label: "Bivy delegation" });
|
|
616
|
+
try {
|
|
617
|
+
await client.connect();
|
|
618
|
+
return client.roomKey();
|
|
619
|
+
}
|
|
620
|
+
finally {
|
|
621
|
+
client.close();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
608
624
|
/** Agents this machine can run a delegated task with (installed ones). */
|
|
609
625
|
function localAgents() {
|
|
610
626
|
return runtimeList().filter((rt) => rt.status === "available").map((rt) => ({ id: rt.id, name: String(rt.displayName || rt.id) }));
|
|
@@ -4651,11 +4667,7 @@ async function runIssueTaskInner(cfg, issue, source, overrides = {}) {
|
|
|
4651
4667
|
sessionNamer.setSessionName(record, issueSessionTitle(issue));
|
|
4652
4668
|
// Best-effort model selection for runtimes that support it (e.g. claude-code, pi).
|
|
4653
4669
|
if (directives.model && typeof record.session.setModel === "function") {
|
|
4654
|
-
|
|
4655
|
-
assertSessionModel(record, directives.model);
|
|
4656
|
-
await record.session.setModel("", directives.model);
|
|
4657
|
-
}
|
|
4658
|
-
catch { }
|
|
4670
|
+
await applyRequestedModel(record, { provider: "", id: directives.model });
|
|
4659
4671
|
}
|
|
4660
4672
|
if (!record.worktree)
|
|
4661
4673
|
throw new Error("worktree was not created for the issue session");
|
|
@@ -4850,8 +4862,16 @@ async function runSessionTurn(record, prompt, signal) {
|
|
|
4850
4862
|
if (signal?.aborted)
|
|
4851
4863
|
throw signal.reason ?? new Error("Run cancelled");
|
|
4852
4864
|
let unsubscribe = () => { };
|
|
4865
|
+
// A turn the agent failed (usage limit, auth, crash) still ends with
|
|
4866
|
+
// agent_end; remember why so the Run fails instead of reporting success.
|
|
4867
|
+
let turnError = "";
|
|
4853
4868
|
const finished = new Promise((resolve) => {
|
|
4854
4869
|
unsubscribe = record.session.subscribe((event) => {
|
|
4870
|
+
const e = event;
|
|
4871
|
+
if (e.type === "session.error" && typeof e.error === "string")
|
|
4872
|
+
turnError ||= e.error;
|
|
4873
|
+
if (e.type === "message_end" && e.message?.stopReason === "error" && typeof e.message.errorMessage === "string")
|
|
4874
|
+
turnError ||= e.message.errorMessage;
|
|
4855
4875
|
if (event.type === "agent_end")
|
|
4856
4876
|
resolve();
|
|
4857
4877
|
});
|
|
@@ -4866,6 +4886,8 @@ async function runSessionTurn(record, prompt, signal) {
|
|
|
4866
4886
|
try {
|
|
4867
4887
|
await turnWatchdog.promptWithWatchdog(record, prompt);
|
|
4868
4888
|
await (signal ? Promise.race([finished, cancelled]) : finished);
|
|
4889
|
+
if (turnError)
|
|
4890
|
+
throw new Error(turnError);
|
|
4869
4891
|
}
|
|
4870
4892
|
finally {
|
|
4871
4893
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -5325,6 +5347,10 @@ async function delegationAnswer(sessionId) {
|
|
|
5325
5347
|
const message = messages[i];
|
|
5326
5348
|
if (message?.role !== "assistant")
|
|
5327
5349
|
continue;
|
|
5350
|
+
// A turn the agent failed (usage limit, auth) ends on an error marker, not
|
|
5351
|
+
// prose: hand the parent the reason instead of an empty answer.
|
|
5352
|
+
if (message.stopReason === "error" && typeof message.errorMessage === "string" && message.errorMessage.trim())
|
|
5353
|
+
return `The delegated agent failed: ${message.errorMessage.trim()}`;
|
|
5328
5354
|
const text = runtimeContentText(message.content).trim();
|
|
5329
5355
|
if (text)
|
|
5330
5356
|
return text;
|
|
@@ -5485,12 +5511,8 @@ async function executeWorkItem(item, report, signal) {
|
|
|
5485
5511
|
approvalMode: safety.approval,
|
|
5486
5512
|
});
|
|
5487
5513
|
sessionNamer.setSessionName(record, `${issue.identifier}: ${issue.title}`);
|
|
5488
|
-
if (item.model)
|
|
5489
|
-
|
|
5490
|
-
await record.session.setModel("", item.model);
|
|
5491
|
-
}
|
|
5492
|
-
catch { }
|
|
5493
|
-
}
|
|
5514
|
+
if (item.model)
|
|
5515
|
+
await applyRequestedModel(record, { provider: "", id: item.model });
|
|
5494
5516
|
await report({ output: { sessionId: record.id, branch }, events: [{ at: new Date().toISOString(), kind: "branch", summary: "Linear issue working branch and session created.", ref: branch, url: issue.url }] });
|
|
5495
5517
|
await runSessionTurn(record, buildLinearTaskPrompt(issue, item.body), signal);
|
|
5496
5518
|
if (signal.aborted)
|
|
@@ -5567,10 +5589,7 @@ async function executeWorkItem(item, report, signal) {
|
|
|
5567
5589
|
sessionNamer.setSessionName(record, item.title);
|
|
5568
5590
|
persistSessionMetadata(record);
|
|
5569
5591
|
if (item.model) {
|
|
5570
|
-
|
|
5571
|
-
await record.session.setModel("", item.model);
|
|
5572
|
-
}
|
|
5573
|
-
catch { }
|
|
5592
|
+
await applyRequestedModel(record, { provider: "", id: item.model });
|
|
5574
5593
|
}
|
|
5575
5594
|
// Record the session id with the control plane BEFORE the (potentially long)
|
|
5576
5595
|
// turn runs, not just after it completes. A machine restart mid-turn must leave
|
|
@@ -30,6 +30,7 @@ export class SiblingClient {
|
|
|
30
30
|
opts;
|
|
31
31
|
ws;
|
|
32
32
|
cipher;
|
|
33
|
+
siblingRoomKey;
|
|
33
34
|
keypair = newDeviceKeypair();
|
|
34
35
|
reassembler = new FrameReassembler({ onReject: () => this.ws?.close() });
|
|
35
36
|
transfers = new EventTransferReceiver({
|
|
@@ -147,6 +148,7 @@ export class SiblingClient {
|
|
|
147
148
|
if (k === "pair.welcome") {
|
|
148
149
|
try {
|
|
149
150
|
const roomKey = acceptWelcome(this.keypair, { nodePublicKeyB64: String(p.nodePublicKeyB64 ?? ""), wrapped: String(p.wrapped ?? "") });
|
|
151
|
+
this.siblingRoomKey = roomKey;
|
|
150
152
|
this.cipher = new RoomCipher(roomKey);
|
|
151
153
|
this.resolvePaired?.();
|
|
152
154
|
}
|
|
@@ -169,6 +171,13 @@ export class SiblingClient {
|
|
|
169
171
|
}
|
|
170
172
|
this.opts.onEvent?.(event);
|
|
171
173
|
}
|
|
174
|
+
/** The sibling's room key, once paired: what seals content only it can read
|
|
175
|
+
* (e.g. a delegated Run's instructions queued for that machine). */
|
|
176
|
+
roomKey() {
|
|
177
|
+
if (!this.siblingRoomKey)
|
|
178
|
+
throw new Error("sibling client not paired");
|
|
179
|
+
return this.siblingRoomKey;
|
|
180
|
+
}
|
|
172
181
|
/** Send a command and await its correlated reply event (by requestId). */
|
|
173
182
|
request(command, timeoutMs = 30_000) {
|
|
174
183
|
const requestId = `repl-${++this.reqSeq}-${Date.now()}`;
|
package/package.json
CHANGED