@botbuddy/cli 1.63.40 → 1.63.42

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.63.40",
3
- "source_commit": "6a30ed1a16dc6a97351c3cea5cc2d03918dbe0fc",
4
- "source_sha256": "de12a3e513169b3a152a617b48c7e9c4c25c30e47b549854994744c369268ff1",
2
+ "release_version": "1.63.42",
3
+ "source_commit": "68cd7a93519197cc7b96a190fa0c4b4799a9c45d",
4
+ "source_sha256": "3a4e94f577771c564aa24d54529368c6b6d325be1e64ef0a30da1531f02be6ed",
5
5
  "target": "darwin-arm64",
6
- "sha256": "9732532ac31d3e184fedafe19089b5efa938b2f3c2c4952cbc95866d86f4b8ae"
6
+ "sha256": "3c5f90061df675f1e06408ce5e1f3c9b6350e5a237216b0dbc11d71d656a1b35"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.40",
3
- "source_commit": "6a30ed1a16dc6a97351c3cea5cc2d03918dbe0fc",
4
- "source_sha256": "de12a3e513169b3a152a617b48c7e9c4c25c30e47b549854994744c369268ff1",
2
+ "release_version": "1.63.42",
3
+ "source_commit": "68cd7a93519197cc7b96a190fa0c4b4799a9c45d",
4
+ "source_sha256": "3a4e94f577771c564aa24d54529368c6b6d325be1e64ef0a30da1531f02be6ed",
5
5
  "target": "linux-x64",
6
- "sha256": "8abdd484047020cb2d96a7adc2a2bacb3b860bafaaf0f56e517eb844bb8550a1"
6
+ "sha256": "a5a474195eca0a8b57ddf8c5d50cfddb88f083a57bc99e80dc4f1a7d10e99535"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.63.40",
4
- "source_commit": "6a30ed1a16dc6a97351c3cea5cc2d03918dbe0fc",
5
- "source_sha256": "de12a3e513169b3a152a617b48c7e9c4c25c30e47b549854994744c369268ff1",
3
+ "release_version": "1.63.42",
4
+ "source_commit": "68cd7a93519197cc7b96a190fa0c4b4799a9c45d",
5
+ "source_sha256": "3a4e94f577771c564aa24d54529368c6b6d325be1e64ef0a30da1531f02be6ed",
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": "9732532ac31d3e184fedafe19089b5efa938b2f3c2c4952cbc95866d86f4b8ae",
11
+ "sha256": "3c5f90061df675f1e06408ce5e1f3c9b6350e5a237216b0dbc11d71d656a1b35",
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": "8abdd484047020cb2d96a7adc2a2bacb3b860bafaaf0f56e517eb844bb8550a1",
18
+ "sha256": "a5a474195eca0a8b57ddf8c5d50cfddb88f083a57bc99e80dc4f1a7d10e99535",
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.63.40",
3
+ "version": "1.63.42",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -295,6 +295,8 @@ export async function runStagingAuth({ plan, host, env, deps, socketRun, resolve
295
295
  }
296
296
  const found = { binding: ctxPool.poolBinding };
297
297
  Object.assign(vars, { allowed: Object.keys(found.binding.personas ?? {}), tenant: ctxPool.tenant ?? undefined, poolId: ctxPool.poolId ?? undefined });
298
+ // BOT-2836: a retry addresses the same pool when one was named (several installed).
299
+ if (plan.pool) vars.retry = `${authLine} --pool ${ctxPool.poolId}`;
298
300
  const { findPersona } = await loadBinding();
299
301
  const persona = findPersona(found.binding, plan.persona);
300
302
  if (!persona) return refuse(io, "persona_not_allowlisted", 2, {}, vars);
@@ -317,7 +319,10 @@ export async function runStagingAuth({ plan, host, env, deps, socketRun, resolve
317
319
  // The refusal's code is the pool's health reason when it is unhealthy (e.g. a
318
320
  // count-0 persona is persona_not_configured), so code, message and help agree.
319
321
  const reasonCode = typeof error.reason === "string" && error.reason !== "healthy" && isKnownCode(error.reason) ? error.reason : "pool_empty";
320
- return refuse(io, reasonCode, 3, Object.fromEntries(["reason", "detail", ...GUIDED_FIELDS].filter((key) => error[key] !== undefined).map((key) => [key, error[key]])), vars);
322
+ // BOT-2836: a healthy empty pool is the agent's own wait-and-retry, so it is
323
+ // rendered here with this lane; an unhealthy pool keeps the pool's health guidance.
324
+ const forwarded = reasonCode === "pool_empty" ? ["reason", "detail"] : ["reason", "detail", ...GUIDED_FIELDS];
325
+ return refuse(io, reasonCode, 3, Object.fromEntries(forwarded.filter((key) => error[key] !== undefined).map((key) => [key, error[key]])), vars);
321
326
  }
322
327
  const code = typeof error?.code === "string" && isKnownCode(error.code) ? error.code : "pool_take_failed";
323
328
  return refuse(io, code, 3, { detail: String(error?.message ?? error).slice(0, 300) }, vars);
package/src/stack.mjs CHANGED
@@ -1356,7 +1356,7 @@ export function leaseRefusalExit(code) {
1356
1356
  // server refusing the credential — never BACKEND, which a gate reads as "down".
1357
1357
  if (CALL_AUTH_ERROR_CODES.has(code)) return EXIT.AUTH;
1358
1358
  // BOT-1926: a reused lease requested in the other execution mode is fixable by the caller.
1359
- return code === STACK_PATH_ROOT_REFUSED_CODE || code === "STACK_EXECUTION_MODE_MISMATCH" ? EXIT.INVALID : EXIT.BACKEND;
1359
+ return code === STACK_PATH_ROOT_REFUSED_CODE || code === "STACK_EXECUTION_MODE_MISMATCH" || code === "AGENT_DEEPLINK_REQUIRED" ? EXIT.INVALID : EXIT.BACKEND;
1360
1360
  }
1361
1361
 
1362
1362
  /**
@@ -1386,6 +1386,8 @@ export const REMOTE_REFUSAL_GUIDANCE = Object.freeze({
1386
1386
  "re-run (the bundle is built automatically unless it failed — see the warning above) or use --local-only.",
1387
1387
  });
1388
1388
 
1389
+ const AGENT_DEEPLINK_REQUIRED_GUIDANCE = "the control plane refused this stack because its owning agent has no canonical profile or native session link. Reconnect with `bb register`, then retry. No lease was created.";
1390
+
1389
1391
  /** The refusal text for a receipt: the server's message, else the CLI's guidance, else the code. */
1390
1392
  export function leaseRefusalError(data) {
1391
1393
  if (data?.lease_id && ["STACK_CONTEXT_BIND_FAILED", "STACK_CONTEXT_ALREADY_BOUND"].includes(data?.code)) {
@@ -1398,7 +1400,7 @@ export function leaseRefusalError(data) {
1398
1400
  return `the server's identified admission failed in the database${data.sqlstate ? ` (SQLSTATE ${data.sqlstate})` : ""} and rolled back, ` +
1399
1401
  "so no lease was created and this stack's port claim is released. This is a BotBuddy server fault: report it with this run's admission request id.";
1400
1402
  }
1401
- return data?.message || REMOTE_REFUSAL_GUIDANCE[data?.code] || data?.code || "lease request refused";
1403
+ return data?.message || (data?.code === "AGENT_DEEPLINK_REQUIRED" ? AGENT_DEEPLINK_REQUIRED_GUIDANCE : null) || REMOTE_REFUSAL_GUIDANCE[data?.code] || data?.code || "lease request refused";
1402
1404
  }
1403
1405
 
1404
1406
  /** One stderr line naming a remote-lease refusal (null for every other code). */
@@ -236,6 +236,16 @@ export function resolveInstalledPool({ home = homedir(), poolId = null, tenantHi
236
236
  throw Object.assign(new ConfigError(`Several staging session pools are installed: ${installed.map((pool) => pool.poolId).join(", ")}.`, "pool_ambiguous"), { poolIds: installed.map((pool) => pool.poolId) });
237
237
  }
238
238
 
239
+ // BOT-2836: after a take, ask the installed launchd job to run its tick now, so the
240
+ // pool refills without waiting for the interval or a human `tick`. The job signs in
241
+ // (with its own backoff); the agent never does. No -k: a running tick is never killed.
242
+ // Best effort: an unloaded job surfaces through health, and never fails the take.
243
+ async function requestRefill({ ctx, exec, uid }) {
244
+ const label = ctx.poolBinding?.launchdLabel;
245
+ if (!label) return;
246
+ await createLaunchctl({ exec, uid }).kickstart(label).catch(() => {});
247
+ }
248
+
239
249
  /**
240
250
  * Agent path, used by `bb pw … auth`: claim one verified session for a persona
241
251
  * (alias or email) of the installed pool. Returns { taken, target, binding,
@@ -263,9 +273,11 @@ export async function takeForLane({
263
273
  const pool = buildPool({ ctx, target, now, logger, intervalSeconds, fetchImpl });
264
274
  try {
265
275
  const taken = await pool.take({ persona: entry.email, owner });
276
+ await requestRefill({ ctx, exec, uid });
266
277
  return { taken, target, binding: ctx.poolBinding, persona: entry, discard: () => pool.discardTaken(taken.slotId) };
267
278
  } catch (error) {
268
279
  if (!(error instanceof PoolEmptyError)) throw error;
280
+ await requestRefill({ ctx, exec, uid });
269
281
  const health = await assessHealth({ ctx, pool, now, exec, uid, requestedPersona: entry.email });
270
282
  let effective = health;
271
283
  if (health.ok) {
@@ -131,10 +131,12 @@ export const ERROR_CODES = Object.freeze({
131
131
  `${AUTH} # once; if it repeats, report this code and detail`, "retry_once"),
132
132
 
133
133
  // ── the pool's health (human repairs; agents report once and stop)
134
- pool_empty: entry("human", "repair",
134
+ // BOT-2836: a healthy pool emptied between ticks is a wait, not a repair. The take
135
+ // already asked the human-installed job to refill now, so the agent waits once and retries.
136
+ pool_empty: entry("agent", "take",
135
137
  "The staging session pool is healthy but has no ready session for {persona}.",
136
- "Agents took every ready session since the last tick, or the ready ones were dead or AAL1 (factor rotated) and were discarded.",
137
- "{tickCommand}"),
138
+ "Agents took every ready session since the last tick, or the ready ones were dead or AAL1 (factor rotated) and were discarded; the pool job was asked to refill now.",
139
+ "bb wait 'timer:duration=60' && {retry} # once; if it repeats, report this code", "retry_once"),
138
140
  job_not_loaded: entry("human", "install",
139
141
  "The staging session pool is not installed (or not loaded) on this Mac.",
140
142
  "The one-time install has not been run here, or the Mac was migrated.",
@@ -30,8 +30,8 @@ import {
30
30
  export class PoolEmptyError extends Error {
31
31
  constructor(persona, intervalHint) {
32
32
  super(
33
- `No ready ${persona} session in the pool. The human-owned pool job refills on its next tick${intervalHint}; ` +
34
- "report this blocker and its remedy once, then stop; ask the human to check the job. " +
33
+ `No ready ${persona} session in the pool. The human-owned pool job refills on its next tick${intervalHint} ` +
34
+ "and was asked to refill now; wait once, retry once, and report it if it repeats. " +
35
35
  "Agents never sign in to staging themselves.",
36
36
  );
37
37
  this.name = "PoolEmptyError";