@botbuddy/cli 1.40.0 → 1.40.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/package.json +1 -1
- package/src/codex-bridge.mjs +27 -3
- package/src/commands.mjs +30 -14
- package/src/session-bootstrap.mjs +5 -1
package/package.json
CHANGED
package/src/codex-bridge.mjs
CHANGED
|
@@ -99,6 +99,23 @@ export function applyTurnModelOverride(bootstrappedSessions, threadId, model, re
|
|
|
99
99
|
return true;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// BOT-2058: a bridge-created thread does not have a visible command-result
|
|
103
|
+
// surface before its first turn. Put an advisory live-holder warning in that
|
|
104
|
+
// turn's input, so the agent sees it before it can edit or claim a resource.
|
|
105
|
+
export function prependTicketSessionWarning(prompt, concurrency) {
|
|
106
|
+
const warning = concurrency?.warning;
|
|
107
|
+
if (typeof warning !== "string" || !warning.trim()) return prompt;
|
|
108
|
+
return `BotBuddy ticket-session warning: ${warning}\n\n${prompt}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function consumeTicketSessionWarning(bootstrappedSessions, threadId, result) {
|
|
112
|
+
if (result?.error) return false;
|
|
113
|
+
const stored = bootstrappedSessions?.get?.(threadId);
|
|
114
|
+
if (!stored?.ticket_session_warning) return false;
|
|
115
|
+
stored.ticket_session_warning = null;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
// BOT-1650: retire a thread's admission only after its terminal (archived)
|
|
103
120
|
// lifecycle update has persisted. relayPost returns the relay's error body (no
|
|
104
121
|
// throw) on a transient 4xx/5xx, so a missing thread_id means the archived
|
|
@@ -677,8 +694,11 @@ export async function runBridge(args) {
|
|
|
677
694
|
call: callToolJson,
|
|
678
695
|
callOptions: { auth: await authHeaders() },
|
|
679
696
|
});
|
|
680
|
-
if (bootstrap.ticket_signals?.
|
|
681
|
-
throw new Error(bootstrap.ticket_signals.concurrency.
|
|
697
|
+
if (bootstrap.ticket_signals?.policy) {
|
|
698
|
+
throw new Error(bootstrap.ticket_signals.concurrency?.warning || "Ticket-session exclusivity could not be verified.");
|
|
699
|
+
}
|
|
700
|
+
if (bootstrap.ticket_signals?.concurrency?.holders?.length) {
|
|
701
|
+
console.warn(` ${yellow("!")} Ticket-session warning: ${bootstrap.ticket_signals.concurrency.warning}`);
|
|
682
702
|
}
|
|
683
703
|
if (bootstrap.linkage?.status === "gap") {
|
|
684
704
|
throw new Error(bootstrap.linkage.warning || "Session bootstrap requires ticket attribution.");
|
|
@@ -699,6 +719,7 @@ export async function runBridge(args) {
|
|
|
699
719
|
// overwritten to the default on the first 15s heartbeat.
|
|
700
720
|
model: cmd.params.model || model,
|
|
701
721
|
cwd: requestedWorktree,
|
|
722
|
+
ticket_session_warning: bootstrap.ticket_signals?.concurrency || null,
|
|
702
723
|
});
|
|
703
724
|
const linkPayload = {
|
|
704
725
|
session_id: sessionId,
|
|
@@ -725,6 +746,7 @@ export async function runBridge(args) {
|
|
|
725
746
|
display_name: bootstrap.display_name,
|
|
726
747
|
session_id: bootstrap.session_id,
|
|
727
748
|
expires_at: bootstrap.expires_at,
|
|
749
|
+
...(bootstrap.ticket_signals?.concurrency ? { ticket_session_warning: bootstrap.ticket_signals.concurrency } : {}),
|
|
728
750
|
};
|
|
729
751
|
}
|
|
730
752
|
break;
|
|
@@ -740,9 +762,10 @@ export async function runBridge(args) {
|
|
|
740
762
|
result = { error: "Thread session bootstrap did not complete; start a new thread after resolving its authentication or ticket conflict." };
|
|
741
763
|
break;
|
|
742
764
|
}
|
|
765
|
+
const bootstrap = bootstrappedSessions.get(codexThreadId);
|
|
743
766
|
result = await sendWs("turn/start", {
|
|
744
767
|
threadId: codexThreadId,
|
|
745
|
-
input: [{ type: "text", text: cmd.params.prompt || cmd.params.text || "" }],
|
|
768
|
+
input: [{ type: "text", text: prependTicketSessionWarning(cmd.params.prompt || cmd.params.text || "", bootstrap?.ticket_session_warning) }],
|
|
746
769
|
...(cmd.params.model && { model: cmd.params.model }),
|
|
747
770
|
});
|
|
748
771
|
// BOT-1650 (Codex round-35 P2): a per-turn model override must be written
|
|
@@ -750,6 +773,7 @@ export async function runBridge(args) {
|
|
|
750
773
|
// creation-time model and overwrites agents.model — misattributing the
|
|
751
774
|
// overridden turn on the dashboard and in effectiveness reporting.
|
|
752
775
|
applyTurnModelOverride(bootstrappedSessions, codexThreadId, cmd.params.model, result);
|
|
776
|
+
consumeTicketSessionWarning(bootstrappedSessions, codexThreadId, result);
|
|
753
777
|
break;
|
|
754
778
|
}
|
|
755
779
|
|
package/src/commands.mjs
CHANGED
|
@@ -399,6 +399,17 @@ export function parseLoginArgs(args) {
|
|
|
399
399
|
return out;
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
/**
|
|
403
|
+
* BOT-2030/BOT-2044: the sign-in mode for parsed login options. `--device` /
|
|
404
|
+
* `--loopback` win; `--no-browser` is a loopback-only option (print the URL,
|
|
405
|
+
* keep the localhost listener), so on its own it is an explicit loopback
|
|
406
|
+
* choice; otherwise shouldUseDeviceFlow auto-detects (SSH, display-less Linux).
|
|
407
|
+
*/
|
|
408
|
+
export function resolveLoginDevice(options = {}, { env = process.env, platform = process.platform } = {}) {
|
|
409
|
+
const flag = typeof options.device === "boolean" ? options.device : options.noBrowser ? false : undefined;
|
|
410
|
+
return shouldUseDeviceFlow({ env, platform, flag });
|
|
411
|
+
}
|
|
412
|
+
|
|
402
413
|
// BOT-1580: resolve the caller identity from parsed flags, falling back to the
|
|
403
414
|
// BOTBUDDY_CALLER* environment variables so an agent harness can advertise who it
|
|
404
415
|
// is without every wrapper having to pass flags. A flag always wins over the env.
|
|
@@ -477,7 +488,7 @@ async function cmdLogin(args, { errorLog = (line) => console.error(line) } = {})
|
|
|
477
488
|
// BOT-1580: merge caller flags with the BOTBUDDY_CALLER* env fallback.
|
|
478
489
|
const caller = resolveCaller(options, process.env);
|
|
479
490
|
try {
|
|
480
|
-
const device =
|
|
491
|
+
const device = resolveLoginDevice(options, { env: process.env, platform: process.platform });
|
|
481
492
|
await doLogin({ noBrowser: options.noBrowser, tenant: options.tenant, device, ...caller });
|
|
482
493
|
} catch (err) {
|
|
483
494
|
die(err.message);
|
|
@@ -507,7 +518,7 @@ export async function cmdSetup(args, {
|
|
|
507
518
|
return error.exitCode === 2 ? 2 : 4;
|
|
508
519
|
}
|
|
509
520
|
if (options.help) {
|
|
510
|
-
log("bb setup [--no-browser] [--tenant <slug>] [--device | --loopback]\n\nSigns in, verifies the committed .botbuddy-agent.json binding, and caches this worktree's session. Over SSH the sign-in uses a device code you approve from any browser (--device / --loopback force a mode). After it succeeds, use `bb wait <condition>` with no credential flags or environment variables.");
|
|
521
|
+
log("bb setup [--no-browser] [--tenant <slug>] [--device | --loopback]\n\nSigns in, verifies the committed .botbuddy-agent.json binding, and caches this worktree's session. Over SSH the sign-in uses a device code you approve from any browser (--device / --loopback force a mode). --loopback or --no-browser over SSH keeps the localhost callback on the remote host, so first forward its port with ssh -L <port>:127.0.0.1:<port> <host> (the port is in the printed URL), or setup times out. After it succeeds, use `bb wait <condition>` with no credential flags or environment variables.");
|
|
511
522
|
return 0;
|
|
512
523
|
}
|
|
513
524
|
let bound;
|
|
@@ -533,19 +544,18 @@ export async function cmdSetup(args, {
|
|
|
533
544
|
// reusable client key with a tenant-sealed one.
|
|
534
545
|
await login({
|
|
535
546
|
noBrowser: options.noBrowser,
|
|
536
|
-
device:
|
|
547
|
+
device: resolveLoginDevice(options, { env, platform }),
|
|
537
548
|
...callerResolver(options, env),
|
|
538
549
|
});
|
|
539
550
|
// Setup is an explicit session bootstrap/repair command. Rotate before
|
|
540
551
|
// resolving so it cannot merely report an old cache as ready.
|
|
541
552
|
await clearState(cwd);
|
|
542
553
|
const session = await selfHeal({ cwd, env });
|
|
543
|
-
// BOT-
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
// the
|
|
547
|
-
//
|
|
548
|
-
// and is not itself a second holder.
|
|
554
|
+
// BOT-2058: register_agent reports any OTHER live session already holding
|
|
555
|
+
// this ticket (heartbeat < 15m) in `ticket_signals`. This is advisory: a
|
|
556
|
+
// previous harness can remain visible while a human intentionally resumes
|
|
557
|
+
// the ticket in a new session. Shared resources stay protected by typed
|
|
558
|
+
// locks, which remain exclusive.
|
|
549
559
|
const holders = session.ticketSignals?.concurrency?.holders;
|
|
550
560
|
if (Array.isArray(holders) && holders.length > 0) {
|
|
551
561
|
const warning = session.ticketSignals.concurrency.warning
|
|
@@ -553,8 +563,7 @@ export async function cmdSetup(args, {
|
|
|
553
563
|
const who = holders
|
|
554
564
|
.map((h) => `${h.agent ?? h.agent_id}${h.branch ? ` (${h.branch})` : ""}`)
|
|
555
565
|
.join(", ");
|
|
556
|
-
errorLog(`bb setup: ${warning} — held by ${who}.
|
|
557
|
-
return 5;
|
|
566
|
+
errorLog(`bb setup: warning: ${warning} — held by ${who}. Confirm the active work before editing shared resources; typed locks remain required.`);
|
|
558
567
|
}
|
|
559
568
|
log(`bb setup: ${tenant} is ready (${session.source} worktree session). Run bb wait '<condition>'.`);
|
|
560
569
|
await reportUsageAgent(usageAgent, { cwd, log });
|
|
@@ -604,8 +613,9 @@ ${bold("HOW IT WORKS")}
|
|
|
604
613
|
${bold("OPTIONS")}
|
|
605
614
|
--no-browser Don't launch a browser; print the authorization URL to open
|
|
606
615
|
in a browser on THIS machine yourself. The callback listener
|
|
607
|
-
still runs on this machine's localhost
|
|
608
|
-
|
|
616
|
+
still runs on this machine's localhost, so this implies
|
|
617
|
+
${cyan("--loopback")} unless ${cyan("--device")} is also given.
|
|
618
|
+
--device Sign in with a device code: print a link and a short code,
|
|
609
619
|
approve it in a browser on ANY machine (laptop, phone), and
|
|
610
620
|
the login finishes on its own. No browser or listener here.
|
|
611
621
|
--loopback Force the browser + localhost callback flow, even over SSH.
|
|
@@ -638,6 +648,9 @@ ${bold("NOTES")}
|
|
|
638
648
|
listener and state.
|
|
639
649
|
• ${bold("Remote / SSH:")} over SSH (or on Linux with no display) login uses the device
|
|
640
650
|
code automatically: open the printed link on any machine and approve.
|
|
651
|
+
${cyan("--loopback")} or ${cyan("--no-browser")} over SSH keeps the localhost callback on the
|
|
652
|
+
remote host, so first forward its port, e.g. ${cyan("ssh -L <port>:127.0.0.1:<port> <host>")}
|
|
653
|
+
(the port is in the printed URL), or the login times out.
|
|
641
654
|
|
|
642
655
|
${bold("RECOVERY")}
|
|
643
656
|
If ${cyan("botbuddy status")} shows no client key, run ${cyan("botbuddy login")}. For an
|
|
@@ -813,7 +826,10 @@ async function cmdBootstrap(args) {
|
|
|
813
826
|
if (state.linkage?.status === "gap") die(state.linkage.warning || "bb bootstrap requires ticket attribution or --no-ticket-reason.");
|
|
814
827
|
// BOT-1945: name (never the bearer of) an unowned cached session this bootstrap
|
|
815
828
|
// replaced, so a leftover live holder is visible instead of silent.
|
|
816
|
-
|
|
829
|
+
// BOT-2058: the launcher must see a known-holder warning before it starts a
|
|
830
|
+
// new harness. Keep it out of the credential cache, but preserve the
|
|
831
|
+
// non-secret concurrency payload in this command receipt.
|
|
832
|
+
console.log(JSON.stringify({ agent_id: state.agent_id, display_name: state.display_name, session_id: state.session_id, expires_at: state.expires_at, ...(state.ticket_signals?.concurrency ? { ticket_session_warning: state.ticket_signals.concurrency } : {}), ...(state.replaced_cached_session ? { replaced_cached_session: state.replaced_cached_session } : {}) }));
|
|
817
833
|
}
|
|
818
834
|
|
|
819
835
|
// BOT-1566 A4: after the local-metadata lines, ask the SERVER whether the stored
|
|
@@ -273,7 +273,11 @@ export async function bootstrapHarnessSession({
|
|
|
273
273
|
// Persist only the four session-secret fields. Ticket signals are intentionally
|
|
274
274
|
// returned to the launcher, never copied into the runtime credential file.
|
|
275
275
|
const { ticket_signals: ticketSignals, display_name: displayName, linkage, ...session } = response.data;
|
|
276
|
-
|
|
276
|
+
// A live-holder report is advisory: agents frequently resume a ticket in a
|
|
277
|
+
// new harness session after a previous session's shutdown was not observed.
|
|
278
|
+
// The signal remains on the returned result so callers can show the holder;
|
|
279
|
+
// resource locks, not ticket presence, continue to arbitrate shared state.
|
|
280
|
+
if (ticketSignals?.policy) throw new Error(ticketSignals.concurrency?.warning || "Ticket-session exclusivity could not be verified.");
|
|
277
281
|
if (linkage?.status === "gap") throw new Error(linkage.warning || "Session bootstrap requires ticket attribution.");
|
|
278
282
|
// A new harness may be admitted only after an abandoned cache aged out of
|
|
279
283
|
// the server's liveness window. Before replacing that file, ask the server
|