@botbuddy/cli 1.59.0 → 1.59.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.59.0",
3
+ "version": "1.59.3",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/stack.mjs CHANGED
@@ -134,6 +134,8 @@ ${bold("up OPTIONS")}
134
134
  deadline (exit 2) and a failed wait (its own exit code) never retry.
135
135
  --local-exec FALLBACK (no Helper): run 'supabase start' in an ISOLATED --stack-path
136
136
  stack and self-activate. Refused at the worktree root (shared stack).
137
+ --keep-volumes LOCAL-EXEC only: preserve a reaped stack's volumes; refuses when the
138
+ latest signed Helper receipt requires a one-time recovery stop.
137
139
  --provision-timeout N LOCAL-EXEC: seconds 'supabase start' may run (default ${LOCAL_EXEC_PROVISION_TIMEOUT_SEC}). On
138
140
  timeout up runs 'stack done --local-exec' itself and exits 6 with
139
141
  park_reason provision_timeout (BOT-2154). The server also fails a local-exec
@@ -328,7 +330,7 @@ export function parseStackArgs(argv) {
328
330
  slot: null, host: null, repo: null, ticket: null, ticketUrl: null,
329
331
  prId: null, prUrl: null, purpose: null, idleTtl: null, stackPath: ".", stackPathExplicit: false,
330
332
  timeout: DEFAULT_TIMEOUT_SEC, provisionTimeout: null, reapTimeout: 300, hardTtl: null,
331
- connectionFile: null, noWait: false, claimOnly: false, localExec: false, waitCapacity: null, holdOnTimeout: null,
333
+ connectionFile: null, noWait: false, claimOnly: false, localExec: false, keepVolumes: false, waitCapacity: null, holdOnTimeout: null,
332
334
  dockerContext: null, dockerEndpoint: null,
333
335
  // BOT-2142: execution placement (null = command default), remote-lease callback + bundle cap.
334
336
  execution: null, remote: false, localOnly: false, callbackPort: null, bundleMaxBytes: null,
@@ -453,6 +455,7 @@ export function parseStackArgs(argv) {
453
455
  case "--no-wait": opts.noWait = true; break;
454
456
  case "--claim-only": opts.claimOnly = true; break;
455
457
  case "--local-exec": opts.localExec = true; break;
458
+ case "--keep-volumes": opts.keepVolumes = true; break;
456
459
  case "--docker-context": if (need(a, rest[i + 1])) opts.dockerContext = rest[++i]; break;
457
460
  case "--docker-endpoint": if (need(a, rest[i + 1])) opts.dockerEndpoint = rest[++i]; break;
458
461
  case "--stop": opts.disposition = "stop"; break;
@@ -520,6 +523,9 @@ export function parseStackArgs(argv) {
520
523
  if (opts.claimOnly && (command !== "up" || !opts.localExec)) {
521
524
  errors.push("--claim-only is valid only with `stack up --local-exec`");
522
525
  }
526
+ if (opts.keepVolumes && (command !== "up" || !opts.localExec)) {
527
+ errors.push("--keep-volumes is valid only with `stack up --local-exec`");
528
+ }
523
529
  if ((opts.dockerContext || opts.dockerEndpoint) && !(["up", "done"].includes(command) && opts.localExec)) {
524
530
  errors.push("--docker-context and --docker-endpoint are valid only with `stack up --local-exec` or `stack done --local-exec`");
525
531
  }
@@ -1397,13 +1403,13 @@ export async function waitForLease(leaseId, isDone, isFailed, { timeoutSec, auth
1397
1403
  try { sig = JSON.parse(f.data); } catch { continue; }
1398
1404
  if (sig.signal_type !== "stack_lease" || sig.subject_key !== `lease:${leaseId}`) continue;
1399
1405
  const st = sig.payload?.state ?? null;
1400
- if (isFailed(st)) {
1406
+ if (isFailed(st, sig.payload)) {
1401
1407
  // BOT-2013: carry the server's reap_reason so the receipt says WHY the lease is gone.
1402
1408
  const reapReason = typeof sig.payload?.reap_reason === "string" ? sig.payload.reap_reason : null;
1403
1409
  decided = { reason: "failed", result: { failed: true, state: st, ...(reapReason ? { reap_reason: reapReason } : {}) }, status: "error" };
1404
1410
  break frameLoop;
1405
1411
  }
1406
- if (isDone(st)) { decided = { reason: "matched", result: { woke: true, state: st }, status: "matched" }; break frameLoop; }
1412
+ if (isDone(st, sig.payload)) { decided = { reason: "matched", result: { woke: true, state: st }, status: "matched" }; break frameLoop; }
1407
1413
  }
1408
1414
  }
1409
1415
  } catch (err) {
@@ -1603,8 +1609,8 @@ export async function parkOnLease(leaseId, isDone, isFailed, {
1603
1609
  // Codex #1168 P1: an authenticated read of the lease proves the outage is over, so a later,
1604
1610
  // independent rotation starts a fresh auth budget instead of inheriting this one.
1605
1611
  if (state != null) recovered();
1606
- if (state != null && isFailed(state)) return done({ failed: true, state, ...(read?.reap_reason ? { reap_reason: read.reap_reason } : {}) });
1607
- if (state != null && isDone(state)) return done({ woke: true, state });
1612
+ if (state != null && isFailed(state, read)) return done({ failed: true, state, ...(read?.reap_reason ? { reap_reason: read.reap_reason } : {}) });
1613
+ if (state != null && isDone(state, read)) return done({ woke: true, state });
1608
1614
  }
1609
1615
  }
1610
1616
  }
@@ -2097,6 +2103,60 @@ export function localTeardown(stackDir, dockerTarget, disposition, spawn = spawn
2097
2103
  return labelScopedTeardown(projectId, disposition, spawn, dockerEnv, log);
2098
2104
  }
2099
2105
 
2106
+ /** BOT-2398: a prior signed `volumes_reusable=false` receipt permits one exact,
2107
+ * bounded Supabase recovery stop before a local-exec start. This deliberately
2108
+ * has no label-scoped fallback: the signed Helper receipt authorizes only this
2109
+ * Supabase project on this pinned daemon, and any uncertainty leaves the lease
2110
+ * fenced without starting. */
2111
+ export async function localVolumeRecoveryStop(opts, execution, dockerTarget, {
2112
+ targetCheck = runLocalExecTargetCheck,
2113
+ spawn = spawnSync,
2114
+ readProjectId = projectIdUnder,
2115
+ acquireLock = acquireStackLock,
2116
+ lockHeld = false,
2117
+ env = process.env,
2118
+ } = {}) {
2119
+ if (!opts?.localExec || !ADMITTED_DOCKER_VALIDATIONS.has(dockerTarget?.validation) ||
2120
+ !dockerTarget?.resolved_endpoint || !dockerTarget?.server_id) return false;
2121
+ const stackDir = stackDirFor(execution);
2122
+ const projectId = readProjectId(stackDir);
2123
+ if (typeof projectId !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(projectId)) return false;
2124
+ let lock;
2125
+ if (!lockHeld) {
2126
+ try { lock = await acquireLock(lockPathForProject(projectId), { timeoutMs: 0 }); }
2127
+ catch { return false; }
2128
+ }
2129
+ try {
2130
+ const currentTarget = async () => {
2131
+ const checked = await targetCheck(opts);
2132
+ return checked?.exitCode === 0 ? dockerTargetFromPreflight(checked.receipt) : null;
2133
+ };
2134
+ const before = await currentTarget();
2135
+ if (!dockerTargetsMatch(dockerTarget, before)) return false;
2136
+ const dockerEnv = dockerEnvForTarget(dockerTarget, env);
2137
+ const stop = spawn("supabase", ["stop", "--workdir", stackDir, "--no-backup"], {
2138
+ encoding: "utf8", env: dockerEnv, timeout: 120_000, killSignal: "SIGTERM",
2139
+ stdio: ["ignore", "pipe", "pipe"], maxBuffer: 1024 * 1024,
2140
+ });
2141
+ if (stop?.error || stop?.status !== 0) return false;
2142
+ const after = await currentTarget();
2143
+ if (!dockerTargetsMatch(dockerTarget, after)) return false;
2144
+ const listProjectResources = (args) => spawn("docker", args, {
2145
+ encoding: "utf8", env: dockerEnv, timeout: 30_000, killSignal: "SIGTERM",
2146
+ stdio: ["ignore", "pipe", "pipe"], maxBuffer: 1024 * 1024,
2147
+ });
2148
+ const containers = listProjectResources(["ps", "-aq", "--filter", `label=com.supabase.cli.project=${projectId}`]);
2149
+ if (containers?.error || containers?.status !== 0 || String(containers.stdout || "").trim()) return false;
2150
+ const volumes = listProjectResources(["volume", "ls", "-q", "--filter", `label=com.supabase.cli.project=${projectId}`]);
2151
+ if (volumes?.error || volumes?.status !== 0 || String(volumes.stdout || "").trim()) return false;
2152
+ const networks = listProjectResources(["network", "ls", "-q", "--filter", `label=com.supabase.cli.project=${projectId}`]);
2153
+ if (networks?.error || networks?.status !== 0 || String(networks.stdout || "").trim()) return false;
2154
+ return true;
2155
+ } finally {
2156
+ lock?.release?.();
2157
+ }
2158
+ }
2159
+
2100
2160
  /** BOT-2001: remove exactly one Supabase project's containers, network and (for `destroy`)
2101
2161
  * volumes by the CLI's project label; true iff a re-list finds NONE of the resource classes
2102
2162
  * this disposition removes (Codex #1012: a busy volume or network fails closed too). */
@@ -2147,9 +2207,17 @@ export async function cmdUp(opts, {
2147
2207
  callTool = callToolJson,
2148
2208
  authProvider = stackAuthHeader,
2149
2209
  localProvisionFn = localProvision,
2210
+ // BOT-2398: exactly one fixed `supabase stop --no-backup` against the
2211
+ // validated stack directory/engine, only when a signed prior reap requires it.
2212
+ // Keep the fourth argument: the caller holds the project lock across this
2213
+ // stop and the immediately following start, so the recovery primitive must
2214
+ // not try to acquire the same lock recursively.
2215
+ recoveryStopFn = localVolumeRecoveryStop,
2150
2216
  // BOT-2154: the self-release after a timed-out local start (`stack done --local-exec`).
2151
2217
  localDoneFn = null,
2152
2218
  waitFn = waitForLease,
2219
+ // BOT-2398 focused seam; production uses the existing lease SSE park below.
2220
+ waitHandoffFn = null,
2153
2221
  emitResult = emit,
2154
2222
  machineUuidFn = machineUuid,
2155
2223
  assertIsolated = validateLocalExecTarget,
@@ -2460,10 +2528,181 @@ export async function cmdUp(opts, {
2460
2528
  const reservedProjectId = projectIdUnder(stackDirFor(execution));
2461
2529
  const reservedConnection = connectionWithDockerTarget(
2462
2530
  reservedProjectId ? { project_id: reservedProjectId } : {}, localDockerTarget);
2463
- const reserved = await call("reserve_stack_lease", {
2464
- lease_id: leaseId,
2465
- connection: reservedConnection,
2466
- });
2531
+ let volumeRecoveryRequired = false;
2532
+ let provisionReserved = false;
2533
+ if (d.volume_recovery_protocol === 1) {
2534
+ const priorRead = await call("get_stack_lease", { lease_id: leaseId });
2535
+ const current = priorRead?.ok && priorRead.data?.success ? priorRead.data : null;
2536
+ const exactCurrentContext = current && current.state === "provisioning" &&
2537
+ current.host_key === d.host_key && current.slot === slot &&
2538
+ current.worktree_root === execution.worktreeRoot && current.stack_path === execution.stackPath &&
2539
+ current.execution_mode === "local_exec";
2540
+ if (!exactCurrentContext) {
2541
+ const cancelled = await call("cancel_unclaimed_stack_lease", { lease_id: leaseId });
2542
+ const leaseCancellation = cancelled.ok && cancelled.data?.success
2543
+ ? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
2544
+ : { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
2545
+ return emitResult(buildReceipt({
2546
+ command: "up", outcome: "refused", code: "VOLUME_RECOVERY_CONTEXT_UNVERIFIED", lease_id: leaseId, slot,
2547
+ error: "could not verify the exact local-exec host/slot/worktree before checking volume-recovery state",
2548
+ lease_cancellation: leaseCancellation,
2549
+ }), opts, EXIT.LEASE_FAILED);
2550
+ }
2551
+ const hasPriorSource = Object.prototype.hasOwnProperty.call(current, "prior_volume_source_lease_id");
2552
+ const hasPriorDecision = Object.prototype.hasOwnProperty.call(current, "prior_volumes_reusable");
2553
+ const priorSource = current.prior_volume_source_lease_id;
2554
+ if (!hasPriorSource || !hasPriorDecision ||
2555
+ (priorSource !== null && (typeof priorSource !== "string" || !priorSource.trim()))) {
2556
+ const cancelled = await call("cancel_unclaimed_stack_lease", { lease_id: leaseId });
2557
+ const leaseCancellation = cancelled.ok && cancelled.data?.success
2558
+ ? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
2559
+ : { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
2560
+ return emitResult(buildReceipt({
2561
+ command: "up", outcome: "refused", code: "VOLUME_RECOVERY_HISTORY_UNVERIFIED", lease_id: leaseId, slot,
2562
+ error: "the server omitted or malformed prior local-exec history; no recovery or start was attempted",
2563
+ lease_cancellation: leaseCancellation,
2564
+ }), opts, EXIT.LEASE_FAILED);
2565
+ }
2566
+ const reuseState = current.prior_volumes_reusable;
2567
+ if (reuseState !== undefined && reuseState !== null && typeof reuseState !== "boolean") {
2568
+ const cancelled = await call("cancel_unclaimed_stack_lease", { lease_id: leaseId });
2569
+ const leaseCancellation = cancelled.ok && cancelled.data?.success
2570
+ ? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
2571
+ : { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
2572
+ return emitResult(buildReceipt({
2573
+ command: "up", outcome: "refused", code: "VOLUME_RECOVERY_STATE_INVALID", lease_id: leaseId, slot,
2574
+ error: "the server returned an invalid prior volume-reuse state; no recovery or start was attempted",
2575
+ lease_cancellation: leaseCancellation,
2576
+ }), opts, EXIT.LEASE_FAILED);
2577
+ }
2578
+ if (priorSource !== null && reuseState !== true && reuseState !== false) {
2579
+ // The Helper handoff is bound to the already-reserved ordinary
2580
+ // provision job. Reserve it before enqueueing the distinct control
2581
+ // action; neither job may race the local start.
2582
+ const handoffReservation = await call("reserve_stack_lease", {
2583
+ lease_id: leaseId, connection: reservedConnection,
2584
+ });
2585
+ if (!handoffReservation.ok || handoffReservation.data?.success !== true) {
2586
+ return emitResult(buildReceipt({ command: "up", outcome: "error", code: "PREDECESSOR_HANDOFF_RESERVATION_FAILED",
2587
+ lease_id: leaseId, slot, error: "the local provision job could not be reserved before requesting the Helper fence handoff; local start was not attempted" }),
2588
+ opts, EXIT.LEASE_FAILED);
2589
+ }
2590
+ provisionReserved = true;
2591
+ const requestedHandoff = await call("request_stack_lease_predecessor_handoff", { lease_id: leaseId });
2592
+ if (!requestedHandoff.ok || requestedHandoff.data?.success !== true ||
2593
+ requestedHandoff.data?.handoff_required !== true ||
2594
+ typeof requestedHandoff.data?.handoff_job_id !== "string" ||
2595
+ !requestedHandoff.data.handoff_job_id.trim()) {
2596
+ const cancelled = requestedHandoff.ok && requestedHandoff.data?.success === true &&
2597
+ requestedHandoff.data?.handoff_required === false
2598
+ ? await call("cancel_unclaimed_stack_lease", { lease_id: leaseId }) : null;
2599
+ const leaseCancellation = cancelled
2600
+ ? (cancelled.ok && cancelled.data?.success
2601
+ ? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
2602
+ : { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" })
2603
+ : null;
2604
+ return emitResult(buildReceipt({
2605
+ command: "up", outcome: "refused", code: "PREDECESSOR_HANDOFF_UNAVAILABLE", lease_id: leaseId, slot,
2606
+ error: "the owner-authenticated Helper could not prove an accepted exact schema-1 predecessor handoff; refusing recovery stop and local start",
2607
+ ...(leaseCancellation ? { lease_cancellation: leaseCancellation } : {}),
2608
+ }), opts, EXIT.LEASE_FAILED);
2609
+ }
2610
+ const handoffJobID = requestedHandoff.data.handoff_job_id;
2611
+ const isAcceptedHandoff = (value) => value?.state === "provisioning" &&
2612
+ value?.predecessor_handoff_job_id === handoffJobID &&
2613
+ value?.predecessor_handoff_status === "completed" &&
2614
+ value?.predecessor_handoff_outcome === "succeeded";
2615
+ const isFailedHandoff = (value) => value?.state === "provisioning" &&
2616
+ value?.predecessor_handoff_job_id === handoffJobID &&
2617
+ ["failed", "stale"].includes(value?.predecessor_handoff_status);
2618
+ let handoffState = await call("get_stack_lease", { lease_id: leaseId });
2619
+ if (!handoffState.ok || handoffState.data?.success !== true ||
2620
+ (handoffState.data.predecessor_handoff_job_id != null &&
2621
+ handoffState.data.predecessor_handoff_job_id !== handoffJobID)) {
2622
+ return emitResult(buildReceipt({ command: "up", outcome: "error", code: "PREDECESSOR_HANDOFF_STATE_UNVERIFIED",
2623
+ lease_id: leaseId, slot, error: "the exact Helper handoff job status could not be verified; local start was not attempted" }),
2624
+ opts, EXIT.LEASE_FAILED);
2625
+ }
2626
+ if (!isAcceptedHandoff(handoffState.data) && !isFailedHandoff(handoffState.data)) {
2627
+ const waited = waitHandoffFn
2628
+ ? await waitHandoffFn(leaseId, handoffJobID)
2629
+ : await resilientWait(
2630
+ (leaseState, payload) => leaseState === "provisioning" &&
2631
+ payload?.predecessor_handoff_job_id === handoffJobID &&
2632
+ payload?.predecessor_handoff_status === "completed" &&
2633
+ payload?.predecessor_handoff_outcome === "succeeded",
2634
+ (leaseState, payload) => leaseState === "reap_failed" ||
2635
+ (leaseState === "provisioning" && payload?.predecessor_handoff_job_id === handoffJobID &&
2636
+ ["failed", "stale"].includes(payload?.predecessor_handoff_status)),
2637
+ );
2638
+ if (!waited?.woke) {
2639
+ return emitResult(buildReceipt({ command: "up", outcome: waited?.timeout ? "timeout" : "error",
2640
+ code: waited?.timeout ? "PREDECESSOR_HANDOFF_TIMEOUT" : "PREDECESSOR_HANDOFF_FAILED",
2641
+ lease_id: leaseId, slot, error: "the Helper predecessor handoff did not reach accepted success; recovery stop and local start were not attempted" }),
2642
+ opts, waited?.timeout ? EXIT.TIMEOUT : EXIT.LEASE_FAILED);
2643
+ }
2644
+ handoffState = await call("get_stack_lease", { lease_id: leaseId });
2645
+ }
2646
+ if (!handoffState.ok || !isAcceptedHandoff(handoffState.data)) {
2647
+ return emitResult(buildReceipt({ command: "up", outcome: "error", code: "PREDECESSOR_HANDOFF_FAILED",
2648
+ lease_id: leaseId, slot, error: "the exact Helper handoff has no accepted successful terminal receipt; recovery stop and local start were not attempted" }),
2649
+ opts, EXIT.LEASE_FAILED);
2650
+ }
2651
+ // The signed Helper acknowledgement rebinds exact local custody,
2652
+ // but schema 1 did not record whether old volumes were reusable. A
2653
+ // recovery stop would be destructive, so honour --keep-volumes before
2654
+ // it can be attempted.
2655
+ if (opts.keepVolumes) {
2656
+ const cancelled = await call("cancel_unclaimed_stack_lease", { lease_id: leaseId });
2657
+ const leaseCancellation = cancelled.ok && cancelled.data?.success
2658
+ ? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
2659
+ : { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
2660
+ return emitResult(buildReceipt({
2661
+ command: "up", outcome: "refused", code: "VOLUME_RECOVERY_REQUIRED", lease_id: leaseId, slot,
2662
+ error: "the accepted predecessor handoff has no reusable-volume decision; --keep-volumes prevents the required recovery stop",
2663
+ lease_cancellation: leaseCancellation,
2664
+ }), opts, EXIT.LEASE_FAILED);
2665
+ }
2666
+ // Run the established bounded no-backup stop under the project lock
2667
+ // before starting this exact successor.
2668
+ volumeRecoveryRequired = true;
2669
+ }
2670
+ if (reuseState === false || reuseState === true) {
2671
+ const priorProject = current.prior_project_id;
2672
+ const priorTarget = current.prior_docker_target;
2673
+ const exactPriorTarget = priorSource !== null && priorProject === reservedProjectId &&
2674
+ dockerTargetsMatch(priorTarget, localDockerTarget);
2675
+ if (!exactPriorTarget) {
2676
+ const cancelled = await call("cancel_unclaimed_stack_lease", { lease_id: leaseId });
2677
+ const leaseCancellation = cancelled.ok && cancelled.data?.success
2678
+ ? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
2679
+ : { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
2680
+ return emitResult(buildReceipt({
2681
+ command: "up", outcome: "refused", code: "VOLUME_RECOVERY_TARGET_MISMATCH", lease_id: leaseId, slot,
2682
+ error: "the prior signed non-reusable-volume receipt is for a different or incomplete Docker target/project; refusing to start",
2683
+ lease_cancellation: leaseCancellation,
2684
+ }), opts, EXIT.LEASE_FAILED);
2685
+ }
2686
+ if (reuseState === false && opts.keepVolumes) {
2687
+ const cancelled = await call("cancel_unclaimed_stack_lease", { lease_id: leaseId });
2688
+ const leaseCancellation = cancelled.ok && cancelled.data?.success
2689
+ ? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
2690
+ : { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
2691
+ return emitResult(buildReceipt({
2692
+ command: "up", outcome: "refused", code: "VOLUME_RECOVERY_REQUIRED", lease_id: leaseId, slot,
2693
+ error: "the exact prior Helper receipt marked volumes non-reusable; --keep-volumes prevents the required recovery stop",
2694
+ lease_cancellation: leaseCancellation,
2695
+ }), opts, EXIT.LEASE_FAILED);
2696
+ }
2697
+ volumeRecoveryRequired = reuseState === false;
2698
+ }
2699
+ }
2700
+ const reserved = provisionReserved
2701
+ ? { ok: true, data: { success: true, reserved: true } }
2702
+ : await call("reserve_stack_lease", {
2703
+ lease_id: leaseId,
2704
+ connection: reservedConnection,
2705
+ });
2467
2706
  if (!reserved.ok || !reserved.data?.success) {
2468
2707
  // A Helper can win the reservation race by taking the queued provision
2469
2708
  // job between `request_stack_lease` and `reserve_stack_lease`. That is NOT
@@ -2505,10 +2744,46 @@ export async function cmdUp(opts, {
2505
2744
  }), opts, EXIT.LEASE_FAILED);
2506
2745
  }
2507
2746
  } else {
2747
+ let recoveryLock = null;
2748
+ if (volumeRecoveryRequired) {
2749
+ if (opts.keepVolumes) {
2750
+ const cancelled = await call("cancel_unclaimed_stack_lease", { lease_id: leaseId });
2751
+ return emitResult(buildReceipt({
2752
+ command: "up", outcome: "refused", code: "VOLUME_RECOVERY_REQUIRED", lease_id: leaseId, slot,
2753
+ error: "the exact predecessor handoff requires a recovery stop; --keep-volumes prevents the required recovery",
2754
+ lease_cancellation: cancelled.ok && cancelled.data?.success
2755
+ ? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
2756
+ : { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" },
2757
+ }), opts, EXIT.LEASE_FAILED);
2758
+ }
2759
+ if (typeof reservedProjectId !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(reservedProjectId)) {
2760
+ return emitResult(buildReceipt({
2761
+ command: "up", outcome: "error", code: "VOLUME_RECOVERY_PROJECT_UNVERIFIED", lease_id: leaseId, slot,
2762
+ error: "the isolated stack project id is unavailable; the recovery stop and supabase start were not attempted",
2763
+ }), opts, EXIT.LEASE_FAILED);
2764
+ }
2765
+ try { recoveryLock = await acquireStackLock(lockPathForProject(reservedProjectId), { timeoutMs: 0 }); }
2766
+ catch {
2767
+ return emitResult(buildReceipt({
2768
+ command: "up", outcome: "error", code: "VOLUME_RECOVERY_PROJECT_BUSY", lease_id: leaseId, slot,
2769
+ error: "the isolated stack project lock is held; the recovery stop and supabase start were not attempted",
2770
+ }), opts, EXIT.LEASE_FAILED);
2771
+ }
2772
+ let recovered = false;
2773
+ try { recovered = await recoveryStopFn(opts, execution, localDockerTarget, { lockHeld: true }) === true; } catch { recovered = false; }
2774
+ if (!recovered) {
2775
+ recoveryLock.release();
2776
+ return emitResult(buildReceipt({
2777
+ command: "up", outcome: "error", code: "VOLUME_RECOVERY_STOP_FAILED", lease_id: leaseId, slot,
2778
+ error: "the exact bounded `supabase stop --no-backup` recovery failed; the reserved lease remains fenced and `supabase start` was not attempted",
2779
+ }), opts, EXIT.LEASE_FAILED);
2780
+ }
2781
+ }
2508
2782
  let conn;
2509
2783
  try {
2510
2784
  conn = connectionWithDockerTarget(localProvisionFn(opts, localDockerTarget, execution), localDockerTarget);
2511
2785
  } catch (e) {
2786
+ recoveryLock?.release();
2512
2787
  if (e?.code === "PROVISION_TIMEOUT") {
2513
2788
  // BOT-2154: a bounded start that ran out of time. Release the lease ourselves —
2514
2789
  // `stack done --local-exec` stops whatever the start left behind and reaps the lease
@@ -2533,6 +2808,7 @@ export async function cmdUp(opts, {
2533
2808
  // review); do NOT cancel.
2534
2809
  return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: e.message }), opts, EXIT.LEASE_FAILED);
2535
2810
  }
2811
+ recoveryLock?.release();
2536
2812
  const act = await call("activate_stack_lease", { lease_id: leaseId, connection: conn });
2537
2813
  if (!act.ok || !act.data?.success) {
2538
2814
  return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: act.error || act.data?.code || "activate failed" }), opts, EXIT.BACKEND);
@@ -2691,6 +2967,8 @@ export async function cmdDone(leaseId, opts, {
2691
2967
  if (!auth) return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, error: "not authenticated — run `botbuddy login`" }), opts, EXIT.AUTH);
2692
2968
  const call = rpc.call;
2693
2969
  let dockerTarget = null;
2970
+ let expectedDockerTarget = null;
2971
+ let provisionedProjectId = null;
2694
2972
  let legacyTargetProof = null;
2695
2973
  let teardownDir = process.cwd();
2696
2974
  let leaseBefore = null;
@@ -2751,6 +3029,7 @@ export async function cmdDone(leaseId, opts, {
2751
3029
  // old lease would still finalize. Refuse on project drift (the provisioned project_id is
2752
3030
  // persisted on the lease connection at `up`).
2753
3031
  const provisionedProject = current.data.connection?.project_id;
3032
+ provisionedProjectId = provisionedProject ?? null;
2754
3033
  if (provisionedProject) {
2755
3034
  const currentProject = projectIdUnder(teardownDir);
2756
3035
  if (currentProject && currentProject !== provisionedProject) {
@@ -2763,6 +3042,7 @@ export async function cmdDone(leaseId, opts, {
2763
3042
  }
2764
3043
  }
2765
3044
  const expected = current.data.connection?.botbuddy_docker_target;
3045
+ expectedDockerTarget = expected ?? null;
2766
3046
  // BOT-1711 (Codex R13 P1): a modern-target lease that lacks a persisted provisioned
2767
3047
  // project_id was NOT provisioned by BOT-1711+ local-exec (e.g. a pre-1.32 lease that
2768
3048
  // recorded a non-root stack_path but actually started the worktree ROOT). Its recorded
@@ -2877,7 +3157,17 @@ export async function cmdDone(leaseId, opts, {
2877
3157
  error: "local `supabase stop` failed — NOT finalizing; the slot stays fenced. Tear the stack down and re-run `stack done --local-exec`, or let the reaper reconcile.",
2878
3158
  }), opts, EXIT.LEASE_FAILED);
2879
3159
  }
2880
- const fin = await call("finalize_stack_lease", { lease_id: leaseId });
3160
+ // BOT-2398: a successful owner-local teardown is itself an authenticated
3161
+ // custody receipt. Carry the exact provisioned project, Docker daemon, and
3162
+ // disposition so the server can persist a reusable decision without waiting
3163
+ // for a Helper reap receipt that will never exist on this path.
3164
+ const localExecCompletion = opts.localExec && expectedDockerTarget && provisionedProjectId
3165
+ ? { project_id: provisionedProjectId, docker_target: dockerTarget, disposition }
3166
+ : undefined;
3167
+ const fin = await call("finalize_stack_lease", {
3168
+ lease_id: leaseId,
3169
+ ...(localExecCompletion ? { local_exec_completion: localExecCompletion } : {}),
3170
+ });
2881
3171
  if (fin.ok && fin.data?.success) state = fin.data.state;
2882
3172
  else process.stderr.write(`${yellow("⚠")} stack: finalize failed (${fin.error || fin.data?.code}); the reaper will reconcile.\n`);
2883
3173
  }
package/src/wait-core.mjs CHANGED
@@ -676,9 +676,12 @@ export function parseSseFrames(buffer) {
676
676
  }
677
677
 
678
678
  // ---------------------------------------------------------------------------
679
- // Cursor — the spine `seq` is a non-negative integer; reject garbage loudly.
679
+ // Cursor — replay uses JavaScript Numbers at the relay boundary, so only exact
680
+ // non-negative safe integers can be replayed without changing the requested floor.
680
681
  // ---------------------------------------------------------------------------
681
682
 
683
+ const MAX_SAFE_SIGNAL_SEQUENCE = BigInt(Number.MAX_SAFE_INTEGER);
684
+
682
685
  /**
683
686
  * @param {string|null|undefined} raw
684
687
  * @returns {{kind: "absent"} | {kind: "ok", value: string} | {kind: "invalid", raw: string}}
@@ -688,7 +691,9 @@ export function normalizeSince(raw) {
688
691
  const t = String(raw).trim();
689
692
  if (t === "") return { kind: "absent" };
690
693
  if (!/^\d+$/.test(t)) return { kind: "invalid", raw };
691
- return { kind: "ok", value: String(BigInt(t)) };
694
+ const value = BigInt(t);
695
+ if (value > MAX_SAFE_SIGNAL_SEQUENCE) return { kind: "invalid", raw };
696
+ return { kind: "ok", value: String(value) };
692
697
  }
693
698
 
694
699
  // ---------------------------------------------------------------------------
package/src/wait.mjs CHANGED
@@ -153,7 +153,7 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
153
153
  OPTIONS
154
154
  --any wake on the first matching condition (default; only mode in v1)
155
155
  --timeout <seconds> max wait (default 7200; hard cap 28800). Timeout is exit 2, not an error.
156
- --since <seq> resume from a prior receipt's next_cursor (replay what you missed)
156
+ --since <seq> resume from a prior receipt's next_cursor (0..9007199254740991; replay what you missed)
157
157
  --receipt-max-bytes <n> cap the receipt (512..65536; default 10240; payloads truncate to pointers)
158
158
  --heartbeat keep this agent session alive while waiting (so it is not reaped)
159
159
  --url <base> relay base URL (default $BOTBUDDY_RELAY_URL or https://api.bot-buddy.ai/functions/v1)
@@ -216,6 +216,7 @@ function parseArgv(argv) {
216
216
  mode: "any",
217
217
  timeout: 7200,
218
218
  since: null,
219
+ sinceProvided: false,
219
220
  receiptMaxBytes: 10240,
220
221
  heartbeat: false,
221
222
  url: process.env.BOTBUDDY_RELAY_URL || "https://api.bot-buddy.ai/functions/v1",
@@ -248,7 +249,7 @@ function parseArgv(argv) {
248
249
  else if (a === "--heartbeat") opts.heartbeat = true;
249
250
  else if (a === "--timeout") opts.timeout = Number(optionValue());
250
251
  else if (a === "--deadline-at") opts.deadlineAt = optionValue(); // internal recovery argv; never shown as public API
251
- else if (a === "--since") opts.since = optionValue();
252
+ else if (a === "--since") { opts.sinceProvided = true; opts.since = optionValue(); }
252
253
  else if (a === "--receipt-max-bytes") opts.receiptMaxBytes = Number(optionValue());
253
254
  else if (a === "--url") opts.url = optionValue();
254
255
  // BOT-1574: --token and --profile are RETIRED. A wait authenticates from
@@ -1572,6 +1573,19 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
1572
1573
  process.exit(EXIT.INVALID);
1573
1574
  }
1574
1575
 
1576
+ // BOT-2453: an explicit replay cursor is a request to replay, never permission
1577
+ // to silently fall back to live-only. Validate it before creating a recoverable
1578
+ // checkpoint, resolving credentials, registering a wait, or opening SSE.
1579
+ if (opts.sinceProvided) {
1580
+ const since = normalizeSince(opts.since);
1581
+ if (since.kind !== "ok") {
1582
+ process.stderr.write("bb-wait: --since must be a non-negative signal sequence <= 9007199254740991\n");
1583
+ emitReceipt({ schema_version: 1, outcome: "error", error: "invalid_since", cursor: opts.since });
1584
+ process.exit(EXIT.INVALID);
1585
+ }
1586
+ opts.since = since.value;
1587
+ }
1588
+
1575
1589
  const { conditions, errors } = parseConditions(opts.conditions);
1576
1590
  if (errors.length > 0) {
1577
1591
  for (const e of errors) process.stderr.write(`bb-wait: invalid condition '${e.spec}': ${e.message}\n`);