@botbuddy/cli 1.40.1 → 1.40.3
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 +10 -9
- package/src/session-bootstrap.mjs +5 -1
- package/src/wait-checkpoint.mjs +22 -8
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
|
@@ -551,12 +551,11 @@ export async function cmdSetup(args, {
|
|
|
551
551
|
// resolving so it cannot merely report an old cache as ready.
|
|
552
552
|
await clearState(cwd);
|
|
553
553
|
const session = await selfHeal({ cwd, env });
|
|
554
|
-
// BOT-
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
// the
|
|
558
|
-
//
|
|
559
|
-
// 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.
|
|
560
559
|
const holders = session.ticketSignals?.concurrency?.holders;
|
|
561
560
|
if (Array.isArray(holders) && holders.length > 0) {
|
|
562
561
|
const warning = session.ticketSignals.concurrency.warning
|
|
@@ -564,8 +563,7 @@ export async function cmdSetup(args, {
|
|
|
564
563
|
const who = holders
|
|
565
564
|
.map((h) => `${h.agent ?? h.agent_id}${h.branch ? ` (${h.branch})` : ""}`)
|
|
566
565
|
.join(", ");
|
|
567
|
-
errorLog(`bb setup: ${warning} — held by ${who}.
|
|
568
|
-
return 5;
|
|
566
|
+
errorLog(`bb setup: warning: ${warning} — held by ${who}. Confirm the active work before editing shared resources; typed locks remain required.`);
|
|
569
567
|
}
|
|
570
568
|
log(`bb setup: ${tenant} is ready (${session.source} worktree session). Run bb wait '<condition>'.`);
|
|
571
569
|
await reportUsageAgent(usageAgent, { cwd, log });
|
|
@@ -828,7 +826,10 @@ async function cmdBootstrap(args) {
|
|
|
828
826
|
if (state.linkage?.status === "gap") die(state.linkage.warning || "bb bootstrap requires ticket attribution or --no-ticket-reason.");
|
|
829
827
|
// BOT-1945: name (never the bearer of) an unowned cached session this bootstrap
|
|
830
828
|
// replaced, so a leftover live holder is visible instead of silent.
|
|
831
|
-
|
|
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 } : {}) }));
|
|
832
833
|
}
|
|
833
834
|
|
|
834
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
|
package/src/wait-checkpoint.mjs
CHANGED
|
@@ -107,6 +107,20 @@ async function processStartedAt(pid) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// The real process-instance probe behind the ownership fence. Tests inject a
|
|
111
|
+
// deterministic one instead of spawning `ps` (BOT-2031).
|
|
112
|
+
const systemProcessProbe = Object.freeze({
|
|
113
|
+
async isAlive(pid) {
|
|
114
|
+
try { process.kill(pid, 0); }
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error?.code === "ESRCH") return false;
|
|
117
|
+
return true; // EPERM is deliberately conservative.
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
startedAt: processStartedAt,
|
|
122
|
+
});
|
|
123
|
+
|
|
110
124
|
function installFenceExitHook() {
|
|
111
125
|
if (installedFenceExitHook) return;
|
|
112
126
|
installedFenceExitHook = true;
|
|
@@ -120,7 +134,11 @@ function installFenceExitHook() {
|
|
|
120
134
|
});
|
|
121
135
|
}
|
|
122
136
|
|
|
123
|
-
export function createWaitCheckpointStore({
|
|
137
|
+
export function createWaitCheckpointStore({
|
|
138
|
+
root = process.cwd(),
|
|
139
|
+
now = () => new Date(),
|
|
140
|
+
processProbe = systemProcessProbe,
|
|
141
|
+
} = {}) {
|
|
124
142
|
const worktree = resolve(root);
|
|
125
143
|
const directory = join(worktree, ".botbuddy", "waits");
|
|
126
144
|
const pathFor = (id) => {
|
|
@@ -184,12 +202,8 @@ export function createWaitCheckpointStore({ root = process.cwd(), now = () => ne
|
|
|
184
202
|
return lock;
|
|
185
203
|
};
|
|
186
204
|
const lockIsLive = async (lock) => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (error?.code === "ESRCH") return false;
|
|
190
|
-
return true; // EPERM is deliberately conservative.
|
|
191
|
-
}
|
|
192
|
-
const startedAt = await processStartedAt(lock.pid);
|
|
205
|
+
if (!await processProbe.isAlive(lock.pid)) return false;
|
|
206
|
+
const startedAt = await processProbe.startedAt(lock.pid);
|
|
193
207
|
// A live PID without a matching process-instance identity is never ownership proof.
|
|
194
208
|
return startedAt != null && startedAt === lock.process_started_at;
|
|
195
209
|
};
|
|
@@ -310,7 +324,7 @@ export function createWaitCheckpointStore({ root = process.cwd(), now = () => ne
|
|
|
310
324
|
const localWaitId = safeId(id);
|
|
311
325
|
await ensureDirectory();
|
|
312
326
|
const path = lockPathFor(localWaitId);
|
|
313
|
-
const processStartedAtValue = await
|
|
327
|
+
const processStartedAtValue = await processProbe.startedAt(process.pid);
|
|
314
328
|
if (!processStartedAtValue) {
|
|
315
329
|
throw new WaitCheckpointError("wait_owner_unknown", "could not establish this process identity for wait recovery");
|
|
316
330
|
}
|