@botbuddy/cli 1.19.0 → 1.19.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/stack.mjs +60 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.19.0",
3
+ "version": "1.19.1",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/stack.mjs CHANGED
@@ -665,6 +665,7 @@ export async function cmdUp(opts, {
665
665
  callTool = callToolJson,
666
666
  authProvider = stackAuthHeader,
667
667
  localProvisionFn = localProvision,
668
+ waitFn = waitForLease,
668
669
  emitResult = emit,
669
670
  } = {}) {
670
671
  let slot;
@@ -720,7 +721,7 @@ export async function cmdUp(opts, {
720
721
  return emitResult(buildReceipt({ command: "up", outcome: "queued", lease_id: leaseId, state, host_key: d.host_key, slot, queued: true, queue_position: d.queue_position, holders: d.holders }), opts, EXIT.QUEUED);
721
722
  }
722
723
  // Park zero-poll until the lease leaves the queue (granted in BOT-1187 order).
723
- const parked = await waitForLease(leaseId, nonQueued, () => false, { timeoutSec: opts.timeout, auth });
724
+ const parked = await waitFn(leaseId, nonQueued, () => false, { timeoutSec: opts.timeout, auth });
724
725
  if (parked.timeout) return emitResult(buildReceipt({ command: "up", outcome: "timeout", lease_id: leaseId, state: "queued", slot, error: `parked ${opts.timeout}s without capacity` }), opts, EXIT.TIMEOUT);
725
726
  if (parked.auth) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: "unauthorized on wait stream" }), opts, EXIT.AUTH);
726
727
  if (parked.error) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: parked.error }), opts, EXIT.BACKEND);
@@ -762,36 +763,67 @@ export async function cmdUp(opts, {
762
763
  connection: connectionWithDockerTarget({}, localDockerTarget),
763
764
  });
764
765
  if (!reserved.ok || !reserved.data?.success) {
765
- const cancelled = await callTool("cancel_unclaimed_stack_lease", { lease_id: leaseId });
766
- const leaseCancellation = cancelled.ok && cancelled.data?.success
767
- ? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
768
- : { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
769
- return emitResult(buildReceipt({
770
- command: "up", outcome: "error", lease_id: leaseId, state, slot,
771
- error: reserved.data?.code || reserved.error || "could not reserve the provision job for local execution",
772
- lease_cancellation: leaseCancellation,
773
- }), opts, EXIT.LEASE_FAILED);
774
- }
775
- let conn;
776
- try {
777
- conn = connectionWithDockerTarget(localProvisionFn(opts, localDockerTarget), localDockerTarget);
778
- } catch (e) {
779
- // Once the local provisioner has run, `supabase start` may have created
780
- // (or fully started) containers even on a nonzero exit or a status-parse
781
- // failure. Cancelling here would reap the lease and free the slot while
782
- // that stack is still up — the exact orphaned/contended-slot hazard this
783
- // change closes. Leave the lease FENCED (provisioning, reserved to us) so
784
- // `botbuddy stack done` / the reaper tears the stack down first (BOT-1421
785
- // review); do NOT cancel.
786
- return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: e.message }), opts, EXIT.LEASE_FAILED);
787
- }
788
- const act = await callTool("activate_stack_lease", { lease_id: leaseId, connection: conn });
789
- if (!act.ok || !act.data?.success) {
790
- return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: act.error || act.data?.code || "activate failed" }), opts, EXIT.BACKEND);
766
+ // A Helper can win the reservation race by taking the queued provision
767
+ // job between `request_stack_lease` and `reserve_stack_lease`. That is NOT
768
+ // a failure nothing local has started, and the Helper owns (or is about
769
+ // to activate) a live stack for this same lease. Cancelling here would
770
+ // abandon that stack until idle reaping. Two shapes signal the takeover,
771
+ // both from `reserve_stack_lease` which checks lease state before job
772
+ // status (BOT-1587):
773
+ // • PROVISION_JOB_HELPER_CLAIMED — Helper claimed the job, lease still
774
+ // `provisioning`: wait for it to reach active.
775
+ // • LEASE_NOT_PROVISIONING with state `active` — the Helper both claimed
776
+ // AND activated before reserve acquired its locks: already live, so
777
+ // skip the wait (the edge-triggered stream would park until timeout on
778
+ // an already-active lease) and go straight to the read.
779
+ // Either way, follow the Helper to the shared authoritative final read
780
+ // below; only a genuine reserve failure is terminal.
781
+ const helperClaimed = reserved.data?.code === "PROVISION_JOB_HELPER_CLAIMED";
782
+ const helperAlreadyActive = reserved.data?.code === "LEASE_NOT_PROVISIONING" && reserved.data?.state === "active";
783
+ if (helperClaimed || helperAlreadyActive) {
784
+ if (!helperAlreadyActive) {
785
+ const active = await waitFn(leaseId, isActive, isReaped, { timeoutSec: opts.timeout, auth });
786
+ if (active.timeout) return emitResult(buildReceipt({ command: "up", outcome: "timeout", lease_id: leaseId, state, slot, error: `a Helper claimed the provision job but it did not reach active in ${opts.timeout}s` }), opts, EXIT.TIMEOUT);
787
+ if (active.auth) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: "unauthorized on wait stream" }), opts, EXIT.AUTH);
788
+ if (active.failed) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, state: active.state, error: "lease was reaped before it became active" }), opts, EXIT.LEASE_FAILED);
789
+ if (active.error) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: active.error }), opts, EXIT.BACKEND);
790
+ }
791
+ // Fall through to the authoritative `get_stack_lease` read below do
792
+ // NOT run the local provisioner; the Helper owns this stack.
793
+ } else {
794
+ const cancelled = await callTool("cancel_unclaimed_stack_lease", { lease_id: leaseId });
795
+ const leaseCancellation = cancelled.ok && cancelled.data?.success
796
+ ? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
797
+ : { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
798
+ return emitResult(buildReceipt({
799
+ command: "up", outcome: "error", lease_id: leaseId, state, slot,
800
+ error: reserved.data?.code || reserved.error || "could not reserve the provision job for local execution",
801
+ lease_cancellation: leaseCancellation,
802
+ }), opts, EXIT.LEASE_FAILED);
803
+ }
804
+ } else {
805
+ let conn;
806
+ try {
807
+ conn = connectionWithDockerTarget(localProvisionFn(opts, localDockerTarget), localDockerTarget);
808
+ } catch (e) {
809
+ // Once the local provisioner has run, `supabase start` may have created
810
+ // (or fully started) containers even on a nonzero exit or a status-parse
811
+ // failure. Cancelling here would reap the lease and free the slot while
812
+ // that stack is still up — the exact orphaned/contended-slot hazard this
813
+ // change closes. Leave the lease FENCED (provisioning, reserved to us) so
814
+ // `botbuddy stack done` / the reaper tears the stack down first (BOT-1421
815
+ // review); do NOT cancel.
816
+ return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: e.message }), opts, EXIT.LEASE_FAILED);
817
+ }
818
+ const act = await callTool("activate_stack_lease", { lease_id: leaseId, connection: conn });
819
+ if (!act.ok || !act.data?.success) {
820
+ return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: act.error || act.data?.code || "activate failed" }), opts, EXIT.BACKEND);
821
+ }
791
822
  }
792
823
  } else {
793
- const active = await waitForLease(leaseId, isActive, isReaped, { timeoutSec: opts.timeout, auth });
824
+ const active = await waitFn(leaseId, isActive, isReaped, { timeoutSec: opts.timeout, auth });
794
825
  if (active.timeout) return emitResult(buildReceipt({ command: "up", outcome: "timeout", lease_id: leaseId, state, slot, error: `provisioning did not reach active in ${opts.timeout}s (Helper may be down — retry with --local-exec)` }), opts, EXIT.TIMEOUT);
826
+ if (active.auth) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: "unauthorized on wait stream" }), opts, EXIT.AUTH);
795
827
  if (active.failed) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, state: active.state, error: "lease was reaped before it became active" }), opts, EXIT.LEASE_FAILED);
796
828
  if (active.error) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: active.error }), opts, EXIT.BACKEND);
797
829
  }