@botbuddy/cli 1.64.11 → 1.64.12

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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.64.11",
3
- "source_commit": "b6f2d83fd2179b508bcc5695f8fb25579c25c463",
4
- "source_sha256": "9c7a5f7704e390901aba149696d263398e96efc63c280979022b1da9ca634dcb",
2
+ "release_version": "1.64.12",
3
+ "source_commit": "ac15a977c04652b558eb4353f232ba4e26c87740",
4
+ "source_sha256": "6eadb8a389e466c1de0b4e3ad98a4978482eef4ae4afa59013f367efdd9b8805",
5
5
  "target": "darwin-arm64",
6
- "sha256": "a466dac8ca5a03259c0e3bf03538f5c24ce0da9de09b8b3b626c63f6279c0b65"
6
+ "sha256": "50423b55ca6646d05cc79d73f3eac4ecc55f0d7bbed05f288e8b297c2a5b3cec"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.64.11",
3
- "source_commit": "b6f2d83fd2179b508bcc5695f8fb25579c25c463",
4
- "source_sha256": "9c7a5f7704e390901aba149696d263398e96efc63c280979022b1da9ca634dcb",
2
+ "release_version": "1.64.12",
3
+ "source_commit": "ac15a977c04652b558eb4353f232ba4e26c87740",
4
+ "source_sha256": "6eadb8a389e466c1de0b4e3ad98a4978482eef4ae4afa59013f367efdd9b8805",
5
5
  "target": "linux-x64",
6
- "sha256": "53eab9515e51d2b35b7da60db3a8ab5107310499b22a77614e19bffcbd40ca88"
6
+ "sha256": "851f72b5be98d48eb631ea5dac259e0be32bcc1cf218c53e666bcdcc0bd02e6d"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.64.11",
4
- "source_commit": "b6f2d83fd2179b508bcc5695f8fb25579c25c463",
5
- "source_sha256": "9c7a5f7704e390901aba149696d263398e96efc63c280979022b1da9ca634dcb",
3
+ "release_version": "1.64.12",
4
+ "source_commit": "ac15a977c04652b558eb4353f232ba4e26c87740",
5
+ "source_sha256": "6eadb8a389e466c1de0b4e3ad98a4978482eef4ae4afa59013f367efdd9b8805",
6
6
  "targets": {
7
7
  "darwin-arm64": {
8
8
  "goos": "darwin",
9
9
  "goarch": "arm64",
10
10
  "binary": "daemon/bin/darwin-arm64/botbuddy-daemon",
11
- "sha256": "a466dac8ca5a03259c0e3bf03538f5c24ce0da9de09b8b3b626c63f6279c0b65",
11
+ "sha256": "50423b55ca6646d05cc79d73f3eac4ecc55f0d7bbed05f288e8b297c2a5b3cec",
12
12
  "mode": "0755"
13
13
  },
14
14
  "linux-x64": {
15
15
  "goos": "linux",
16
16
  "goarch": "amd64",
17
17
  "binary": "daemon/bin/linux-x64/botbuddy-daemon",
18
- "sha256": "53eab9515e51d2b35b7da60db3a8ab5107310499b22a77614e19bffcbd40ca88",
18
+ "sha256": "851f72b5be98d48eb631ea5dac259e0be32bcc1cf218c53e666bcdcc0bd02e6d",
19
19
  "mode": "0755"
20
20
  }
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.64.11",
3
+ "version": "1.64.12",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -284,7 +284,10 @@ export async function takeForLane({
284
284
  const g = guidance("pool_empty", guidanceVars(ctx, { persona: entry.alias }));
285
285
  effective = { ...health, reason: "pool_empty", agentAction: g.agent_action, remedy: remedyLine(g), message: g.message, cause: g.cause, actor: g.actor_label, next: g.next, doc: g.doc, help: g.help };
286
286
  }
287
+ // BOT-2952: the pool's own detail (an Auth outage at take time) survives the health's.
288
+ const poolDetail = error.detail;
287
289
  for (const field of GUIDANCE_FIELDS) if (effective[field] !== undefined) error[field] = effective[field];
290
+ if (poolDetail) error.detail = [poolDetail, effective.detail].filter(Boolean).join(" ");
288
291
  throw error;
289
292
  }
290
293
  }
@@ -135,7 +135,7 @@ export const ERROR_CODES = Object.freeze({
135
135
  // already asked the human-installed job to refill now, so the agent waits once and retries.
136
136
  pool_empty: entry("agent", "take",
137
137
  "The staging session pool is healthy but has no ready session for {persona}.",
138
- "Agents took every ready session since the last tick, or the ready ones were dead or AAL1 (factor rotated) and were discarded; automatic refill creates a durable job before the single retry.",
138
+ "Agents took every ready session since the last tick, or the ready ones were dead or AAL1 (factor rotated) and were discarded, or staging Auth did not answer their liveness check and they were kept (detail says so); automatic refill creates a durable job before the single retry.",
139
139
  "Automatic refill is requested by bb pw auth; wait for its refill job, then {retry} once; if it fails, report this code", "retry_once"),
140
140
  pool_refill_unavailable: entry("agent", "take",
141
141
  "The staging session pool is empty and automatic refill did not return a durable job.",
@@ -483,6 +483,7 @@ export function createSessionPool({
483
483
  const entry = (health.personas[persona] ??= {
484
484
  belowTargetTicks: 0,
485
485
  consecutiveMintFailures: 0,
486
+ backoffMintFailures: 0,
486
487
  nextMintAttemptAt: null,
487
488
  lastMintError: null,
488
489
  });
@@ -505,20 +506,33 @@ export function createSessionPool({
505
506
  await mintIntoReady(persona);
506
507
  counts[persona] += 1;
507
508
  entry.consecutiveMintFailures = 0;
509
+ entry.backoffMintFailures = 0;
508
510
  entry.nextMintAttemptAt = null;
509
511
  entry.lastMintError = null;
510
512
  } catch (error) {
511
513
  summary.mintFailures += 1;
512
514
  entry.consecutiveMintFailures += 1;
513
515
  entry.lastMintError = error?.code ?? "error";
514
- entry.nextMintAttemptAt = new Date(
515
- now() + nextMintBackoffMs({ failures: entry.consecutiveMintFailures, intervalMs }),
516
- ).toISOString();
516
+ // BOT-2952: an outage that never got a password grant created no AAL1 session and
517
+ // ran no MFA verify, so it retries on the next tick and does not lengthen a later
518
+ // backoff. A 429 is Auth asking us to slow down, so it backs off like any other
519
+ // failure, on its own streak.
520
+ const outage =
521
+ error?.transient === true && error?.passwordGrantSucceeded === false && error?.rateLimited !== true;
522
+ if (outage) {
523
+ entry.nextMintAttemptAt = null;
524
+ } else {
525
+ entry.backoffMintFailures = (entry.backoffMintFailures ?? 0) + 1;
526
+ entry.nextMintAttemptAt = new Date(
527
+ now() + nextMintBackoffMs({ failures: entry.backoffMintFailures, intervalMs }),
528
+ ).toISOString();
529
+ }
517
530
  logger({
518
531
  event: "mint_failed",
519
532
  persona,
520
533
  code: entry.lastMintError,
521
534
  message: error?.message ?? String(error),
535
+ outage,
522
536
  consecutiveMintFailures: entry.consecutiveMintFailures,
523
537
  nextMintAttemptAt: entry.nextMintAttemptAt,
524
538
  });
@@ -567,6 +581,7 @@ export function createSessionPool({
567
581
  }
568
582
  }
569
583
  candidates.sort((a, b) => b.verifiedAt - a.verifiedAt);
584
+ let inconclusive = null;
570
585
  for (const { slotId } of candidates) {
571
586
  const takenDir = join(dirs.taken, slotId);
572
587
  if (!(await claim(join(dirs.ready, slotId), takenDir))) continue;
@@ -582,8 +597,23 @@ export function createSessionPool({
582
597
  continue;
583
598
  }
584
599
  const result = await signIn.refresh({ target, storedSession: slot.storedSession, persona });
600
+ if (!result.ok && result.transient) {
601
+ // BOT-2952: Auth did not answer, which says nothing about this slot. It goes back to
602
+ // ready unchanged (the tick does the same, BOT-2947) and the next slot is tried; a
603
+ // persona has at most 4. Nothing unverified is ever handed out.
604
+ inconclusive = result.reason;
605
+ await fs.rm(join(takenDir, "claim.json"), { force: true });
606
+ try {
607
+ await fs.rename(takenDir, join(dirs.ready, slotId));
608
+ } catch (error) {
609
+ // A tick purged it between the claim marker's removal and the rename.
610
+ if (error?.code !== "ENOENT") throw error;
611
+ }
612
+ logger({ event: "take_inconclusive", slotId, persona, reason: result.reason });
613
+ continue;
614
+ }
585
615
  if (!result.ok) {
586
- logger({ event: "take_discard", slotId, persona, reason: result.reason, transient: Boolean(result.transient) });
616
+ logger({ event: "take_discard", slotId, persona, reason: result.reason, transient: false });
587
617
  await removeSlot(takenDir);
588
618
  continue;
589
619
  }
@@ -602,7 +632,11 @@ export function createSessionPool({
602
632
  logger({ event: "take_ok", slotId, persona, owner: meta.owner });
603
633
  return { slotId, statePath: join(takenDir, "state.json"), dir: takenDir, meta };
604
634
  }
605
- throw new PoolEmptyError(persona, tickIntervalHint);
635
+ const empty = new PoolEmptyError(persona, tickIntervalHint);
636
+ if (inconclusive) {
637
+ empty.detail = `Staging Auth did not answer the liveness check (${inconclusive}); the ready sessions were kept for a retry.`;
638
+ }
639
+ throw empty;
606
640
  }
607
641
 
608
642
  async function discardTaken(slotId) {
@@ -16,12 +16,17 @@ import { assertPersonaAllowed } from "./policy.mjs";
16
16
  import { decodeJwtClaims, toStoredSession } from "./state-file.mjs";
17
17
 
18
18
  export class SignInError extends Error {
19
- constructor(code, message, { transient = false, status } = {}) {
19
+ constructor(code, message, { transient = false, status, passwordGrantSucceeded, rateLimited } = {}) {
20
20
  super(message);
21
21
  this.name = "SignInError";
22
22
  this.code = code;
23
23
  this.transient = transient;
24
24
  this.status = status;
25
+ // BOT-2952: set on mint_retries_exhausted. false = no attempt got a password grant, so no
26
+ // AAL1 session existed and no MFA verify ran (the pool retries next tick, no backoff).
27
+ if (passwordGrantSucceeded !== undefined) this.passwordGrantSucceeded = passwordGrantSucceeded;
28
+ // A 429 on any attempt: Auth answered "slow down", so the pool keeps its backoff.
29
+ if (rateLimited !== undefined) this.rateLimited = rateLimited;
25
30
  }
26
31
  }
27
32
 
@@ -93,9 +98,12 @@ export function createGoTrueTotpSignIn({
93
98
  async function mint({ target, persona }) {
94
99
  assertPersonaAllowed(target, persona);
95
100
  let lastFailure = "no attempt made";
101
+ let passwordGrantSucceeded = false;
102
+ let rateLimited = false;
96
103
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
97
- const retry = async (reason, { waitWindow = false } = {}) => {
104
+ const retry = async (reason, { waitWindow = false, response } = {}) => {
98
105
  lastFailure = reason;
106
+ if (response?.status === 429) rateLimited = true;
99
107
  logger({ event: "mint_retry", persona, attempt, reason });
100
108
  if (attempt < maxAttempts) {
101
109
  if (waitWindow) await waitForNextTotpWindow();
@@ -108,7 +116,7 @@ export function createGoTrueTotpSignIn({
108
116
  });
109
117
  if (password.status !== 200) {
110
118
  if (isTransient(password)) {
111
- await retry(`password grant ${describeFailure(password)}`);
119
+ await retry(`password grant ${describeFailure(password)}`, { response: password });
112
120
  continue;
113
121
  }
114
122
  if (isCredentialRefusal(password)) {
@@ -120,6 +128,7 @@ export function createGoTrueTotpSignIn({
120
128
  status: password.status,
121
129
  });
122
130
  }
131
+ passwordGrantSucceeded = true;
123
132
  const aal1Token = password.body?.access_token;
124
133
  const factor = (password.body?.user?.factors ?? []).find(
125
134
  (candidate) => candidate.factor_type === "totp" && candidate.status === "verified",
@@ -135,7 +144,7 @@ export function createGoTrueTotpSignIn({
135
144
  continue;
136
145
  }
137
146
  if (isTransient(challenge)) {
138
- await retry(`challenge ${describeFailure(challenge)}`);
147
+ await retry(`challenge ${describeFailure(challenge)}`, { response: challenge });
139
148
  continue;
140
149
  }
141
150
  throw new SignInError("challenge_failed", `MFA challenge refused: ${describeFailure(challenge)}.`, {
@@ -158,7 +167,7 @@ export function createGoTrueTotpSignIn({
158
167
  continue;
159
168
  }
160
169
  if (isTransient(verify)) {
161
- await retry(`verify ${describeFailure(verify)}`);
170
+ await retry(`verify ${describeFailure(verify)}`, { response: verify });
162
171
  continue;
163
172
  }
164
173
  throw new SignInError("verify_failed", `MFA verify refused: ${describeFailure(verify)}.`, { status: verify.status });
@@ -176,6 +185,8 @@ export function createGoTrueTotpSignIn({
176
185
  }
177
186
  throw new SignInError("mint_retries_exhausted", `Mint gave up after ${maxAttempts} attempts (last: ${lastFailure}).`, {
178
187
  transient: true,
188
+ passwordGrantSucceeded,
189
+ rateLimited,
179
190
  });
180
191
  }
181
192