@botbuddy/cli 1.64.26 → 1.64.28
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.
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.64.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.64.28",
|
|
3
|
+
"source_commit": "68e4229ab423ab6cb69a029a794cd6f9bbdab501",
|
|
4
|
+
"source_sha256": "e456c16060302ab7b6d5642d057985316384a24e806ba400dcdba74836a5c886",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "0234fb211982fc176db2da07f2c8be94d5c33f93e11ad2fc07e22c32ed62a8bd"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.64.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.64.28",
|
|
3
|
+
"source_commit": "68e4229ab423ab6cb69a029a794cd6f9bbdab501",
|
|
4
|
+
"source_sha256": "e456c16060302ab7b6d5642d057985316384a24e806ba400dcdba74836a5c886",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "402e8e84890e6095422c1e0ffa85639e9842fa3ffd3f5abd6d28a80f664b34b2"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.64.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.64.28",
|
|
4
|
+
"source_commit": "68e4229ab423ab6cb69a029a794cd6f9bbdab501",
|
|
5
|
+
"source_sha256": "e456c16060302ab7b6d5642d057985316384a24e806ba400dcdba74836a5c886",
|
|
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": "
|
|
11
|
+
"sha256": "0234fb211982fc176db2da07f2c8be94d5c33f93e11ad2fc07e22c32ed62a8bd",
|
|
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": "
|
|
18
|
+
"sha256": "402e8e84890e6095422c1e0ffa85639e9842fa3ffd3f5abd6d28a80f664b34b2",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/stack.mjs
CHANGED
|
@@ -139,6 +139,16 @@ export const EXIT = Object.freeze({
|
|
|
139
139
|
CLEANUP_FAILED: 8, // child completed but signed physical reap was not proven
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* BOT-3094: BOT-2968's reap reasons for a queued lease whose bound session can never promote it.
|
|
144
|
+
* The place is lost either way, so a parked `stack up` / `stack run` requests a new lease ONCE
|
|
145
|
+
* from a freshly read session; the server binds it only to a current work session.
|
|
146
|
+
*/
|
|
147
|
+
export const SESSION_REAP_REASONS = new Set(["session_ended", "session_stale", "session_invalid"]);
|
|
148
|
+
|
|
149
|
+
/** BOT-3094: cancel_unclaimed_stack_lease refusals that mean "claimed or not cancellable": reap nothing. */
|
|
150
|
+
const CANCEL_REFUSAL_CODES = new Set(["PROVISION_ALREADY_CLAIMED", "PROVISION_AUTHORITY_HISTORY_FENCE", "LEASE_NOT_PROVISIONING", "PROVISION_JOB_NOT_FOUND"]);
|
|
151
|
+
|
|
142
152
|
const STACK_READINESS_HELP = `${bold("READINESS")}
|
|
143
153
|
An active lease proves infrastructure activation. It does not prove application readiness:
|
|
144
154
|
the child may still be initializing its schema, runtime configuration, or worktree locks.
|
|
@@ -162,7 +172,7 @@ ${bold("USAGE")}
|
|
|
162
172
|
include the owner's attested local/remote forwards
|
|
163
173
|
botbuddy stack touch <lease_id> Bump the idle clock (and report measured use) so the reaper doesn't stop it
|
|
164
174
|
botbuddy stack done <lease_id> Release (done): idle the stack for re-adoption (--reap tears it down)
|
|
165
|
-
botbuddy stack cancel <lease_id> Give a queued lease's place up
|
|
175
|
+
botbuddy stack cancel <lease_id> Give a queued lease's place up; never reaps a claimed lease (BOT-3094)
|
|
166
176
|
botbuddy stack run [options] -- <executable> [args...]
|
|
167
177
|
Own one Helper-backed test/fix batch end-to-end
|
|
168
178
|
|
|
@@ -443,7 +453,8 @@ export function stackCommandHelp(command) {
|
|
|
443
453
|
const selected = all.filter((section) => headings[command]?.some((heading) => section.startsWith(bold(heading))));
|
|
444
454
|
const usage = command === "run" ? "[options] -- <executable> [args...]"
|
|
445
455
|
: command === "up" ? "[options]" : "<lease_id> [options]";
|
|
446
|
-
const detail = command === "cancel"
|
|
456
|
+
const detail = command === "cancel"
|
|
457
|
+
? "\n Cancel gives up a queued (or unclaimed provisioning) lease's place atomically. It never reaps a lease a\n Helper has claimed: release that with `stack done <lease_id> --reap`.\n" : "";
|
|
447
458
|
return `${bold(`botbuddy stack ${command}`)} ${usage}${detail}\n\n${selected.join("\n")}\n\n${STACK_READINESS_HELP}\n Use bb stack --help for all commands and options.`;
|
|
448
459
|
}
|
|
449
460
|
|
|
@@ -508,6 +519,8 @@ export function parseStackArgs(argv) {
|
|
|
508
519
|
disposition: "destroy", json: false, receiptMaxBytes: DEFAULT_RECEIPT_MAX_BYTES,
|
|
509
520
|
// BOT-1803: eager release (reap) instead of the default idle release (done/run).
|
|
510
521
|
reap: cancel,
|
|
522
|
+
// BOT-3094: cancel goes through the atomic cancel_unclaimed_stack_lease, never a plain reap.
|
|
523
|
+
cancel,
|
|
511
524
|
};
|
|
512
525
|
const positionals = [];
|
|
513
526
|
const need = (name, v) => {
|
|
@@ -718,6 +731,9 @@ export function parseStackArgs(argv) {
|
|
|
718
731
|
"own supabase/config.toml (distinct project_id + remapped ports).");
|
|
719
732
|
}
|
|
720
733
|
if (opts.releasePortClaim && (command !== "done" || !opts.reap || opts.localExec)) errors.push("--release-port-claim requires `stack done --reap` without --local-exec");
|
|
734
|
+
// BOT-3094: the atomic cancel finalizes a self-reserved provisioning lease server-side, so a local-exec stack's
|
|
735
|
+
// own `supabase stop` would never run. A local-exec lease is released only through `stack done --local-exec`.
|
|
736
|
+
if (opts.cancel && opts.localExec) errors.push("`stack cancel` does not take --local-exec; release a local-exec lease with `stack done <lease_id> --local-exec`");
|
|
721
737
|
if (opts.reap && !["done", "run"].includes(command)) {
|
|
722
738
|
errors.push("--reap is valid only with `stack done` or `stack run`");
|
|
723
739
|
}
|
|
@@ -2574,7 +2590,48 @@ async function runLocalDone(leaseId, opts, deps = {}) {
|
|
|
2574
2590
|
return { exitCode, receipt };
|
|
2575
2591
|
}
|
|
2576
2592
|
|
|
2577
|
-
|
|
2593
|
+
/** BOT-3094: the one stderr line a session re-request prints, shared by `stack up` and `stack run`. */
|
|
2594
|
+
function sessionRequeueNotice(lost) {
|
|
2595
|
+
return `stack: lease ${lost.lease_id} was reaped while queued (reap_reason=${lost.reap_reason}): its session can no longer promote it, ` +
|
|
2596
|
+
"so its place is gone — requesting a new lease once from the current session";
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
/**
|
|
2600
|
+
* `stack up`. BOT-3094: a queue park that ends in a session reap (SESSION_REAP_REASONS) requests a
|
|
2601
|
+
* new lease once, re-reading the session credential; every receipt of that attempt carries
|
|
2602
|
+
* `requeued_from`. A second session reap, or any other outcome, ends the command as before.
|
|
2603
|
+
*/
|
|
2604
|
+
export async function cmdUp(opts, deps = {}) {
|
|
2605
|
+
const emitResult = deps.emitResult ?? emit;
|
|
2606
|
+
const clock = deps.now ?? Date.now;
|
|
2607
|
+
const startedAtMs = clock();
|
|
2608
|
+
const first = await cmdUpOnce(opts, deps, { allowRequeue: true });
|
|
2609
|
+
if (typeof first !== "object" || first === null || !first.requeue) return first;
|
|
2610
|
+
const requeuedFrom = first.requeue;
|
|
2611
|
+
process.stderr.write(`${sessionRequeueNotice(requeuedFrom)}\n`);
|
|
2612
|
+
// One QUEUE-park budget (--timeout) across both attempts, so an outer wait sized to it still holds.
|
|
2613
|
+
// Only the queue park is shortened: the Helper provision waits keep the full --timeout.
|
|
2614
|
+
const retryOpts = { ...opts, queueParkTimeout: remainingParkSecs(opts.timeout, startedAtMs, clock()) };
|
|
2615
|
+
return cmdUpOnce(retryOpts, { ...deps, emitResult: (receipt, o, code) => emitResult({ ...receipt, requeued_from: requeuedFrom }, o,
|
|
2616
|
+
// The command's outcome is still the lost lease: a refused re-request (no new lease) is LEASE_FAILED.
|
|
2617
|
+
requeueRefused(receipt.lease_id, receipt.code, code, receipt.outcome) ? EXIT.LEASE_FAILED : code) }, { allowRequeue: false });
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
/** BOT-3094: seconds left of a --timeout park budget (at least 1). */
|
|
2621
|
+
function remainingParkSecs(timeoutSec, startedAtMs, nowMs) {
|
|
2622
|
+
return Math.max(1, Math.ceil(timeoutSec - (nowMs - startedAtMs) / 1000));
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
/**
|
|
2626
|
+
* BOT-3094: the re-request was answered with a refusal and minted no lease. Only an `error` outcome
|
|
2627
|
+
* that would exit BACKEND or INVALID: a capacity-wait TIMEOUT, an interrupt and AUTH keep their exits.
|
|
2628
|
+
*/
|
|
2629
|
+
export function requeueRefused(leaseId, code, exitCode, outcome) {
|
|
2630
|
+
return !leaseId && outcome === "error" && typeof code === "string" && code.length > 0 &&
|
|
2631
|
+
(exitCode === EXIT.BACKEND || exitCode === EXIT.INVALID);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
async function cmdUpOnce(opts, {
|
|
2578
2635
|
runPreflight = runLocalExecPreflight,
|
|
2579
2636
|
callTool = callToolJson,
|
|
2580
2637
|
authProvider = stackAuthHeader,
|
|
@@ -2599,7 +2656,7 @@ export async function cmdUp(opts, {
|
|
|
2599
2656
|
reauth = (current, kind) => reauthenticateParkedLease(current, kind),
|
|
2600
2657
|
sleep = null,
|
|
2601
2658
|
now = null,
|
|
2602
|
-
} = {}) {
|
|
2659
|
+
} = {}, { allowRequeue = false } = {}) {
|
|
2603
2660
|
let slot;
|
|
2604
2661
|
try { slot = deriveSlot(opts); } catch (e) {
|
|
2605
2662
|
return emitResult(buildReceipt({ command: "up", outcome: "error", error: e.message }), opts, EXIT.INVALID);
|
|
@@ -2747,9 +2804,9 @@ export async function cmdUp(opts, {
|
|
|
2747
2804
|
if (linkNotice) process.stderr.write(`${linkNotice}\n`);
|
|
2748
2805
|
// BOT-2099: every lease wait in `stack up` (queue park, Helper provision, Helper takeover)
|
|
2749
2806
|
// survives auth/availability blips (parkOnLease) and adopts the re-minted credential.
|
|
2750
|
-
const resilientWait = async (isDone, isFailed) => {
|
|
2807
|
+
const resilientWait = async (isDone, isFailed, { timeoutSec = opts.timeout } = {}) => {
|
|
2751
2808
|
const result = await parkOnLease(leaseId, isDone, isFailed, {
|
|
2752
|
-
timeoutSec
|
|
2809
|
+
timeoutSec, auth, wait: waitFn, reauth, ...(sleep ? { sleep } : {}), ...(now ? { now } : {}),
|
|
2753
2810
|
// Codex #1168 R2 P1: probe with the credential the park just re-minted — `auth`/`call`
|
|
2754
2811
|
// are only updated after the park returns, and an old-credential read would 401 and
|
|
2755
2812
|
// miss a grant or reap that landed during the outage.
|
|
@@ -2798,9 +2855,13 @@ export async function cmdUp(opts, {
|
|
|
2798
2855
|
}
|
|
2799
2856
|
// Park zero-poll until the lease leaves the queue (granted in BOT-1187 order).
|
|
2800
2857
|
// BOT-2013: a lease reaped while queued (orphaned_agent, …) is lost, not "left the queue".
|
|
2801
|
-
|
|
2858
|
+
// BOT-3094: a session re-request parks only for what is left of --timeout.
|
|
2859
|
+
const parkSecs = opts.queueParkTimeout ?? opts.timeout;
|
|
2860
|
+
const parked = await resilientWait(nonQueued, isReaped, { timeoutSec: parkSecs });
|
|
2802
2861
|
if (parked.authUnavailable) return emitAuthUnavailable(parked);
|
|
2803
2862
|
if (parked.failed) {
|
|
2863
|
+
// BOT-3094: nothing was provisioned for a queued lease, so its loss leaves nothing to clean up.
|
|
2864
|
+
if (allowRequeue && SESSION_REAP_REASONS.has(parked.reap_reason)) return { requeue: { lease_id: leaseId, reap_reason: parked.reap_reason } };
|
|
2804
2865
|
return emitResult(buildReceipt({
|
|
2805
2866
|
command: "up", outcome: "error", lease_id: leaseId, state: parked.state, slot,
|
|
2806
2867
|
reap_reason: parked.reap_reason ?? null,
|
|
@@ -2824,8 +2885,8 @@ export async function cmdUp(opts, {
|
|
|
2824
2885
|
// BOT-2237: a park that ended inside an unrecovered platform incident never observed admission.
|
|
2825
2886
|
const blind = parked.incident && !parked.incident.recovered;
|
|
2826
2887
|
const why = blind
|
|
2827
|
-
? `parked ${
|
|
2828
|
-
: `parked ${
|
|
2888
|
+
? `parked ${parkSecs}s inside a platform incident (${parked.incident.reason} since ${parked.incident.first_at}), so its admission was never observed`
|
|
2889
|
+
: `parked ${parkSecs}s without capacity`;
|
|
2829
2890
|
// BOT-2400: a held place is PARK_DEADLINE; the BOT-2237 incident code still wins when the
|
|
2830
2891
|
// park never observed admission (the place is held either way — see queue_hold).
|
|
2831
2892
|
const code = blind ? parked.incident.reason : (queueHold.held ? PARK_DEADLINE : PARK_HOLD_UNCONFIRMED);
|
|
@@ -3516,11 +3577,28 @@ export async function cmdDone(leaseId, opts, {
|
|
|
3516
3577
|
const idleRelease = !opts.reap && !opts.localExec;
|
|
3517
3578
|
// BOT-2847: an unanswered release (RPC_ERROR, transport, 5xx) is retried for about 60 s; a
|
|
3518
3579
|
// definite refusal is reported at once. An exhausted retry names the cause and the attempts.
|
|
3580
|
+
// BOT-3094: `stack cancel` is the atomic cancel_unclaimed_stack_lease, which gives up a queued
|
|
3581
|
+
// (or unclaimed provisioning) lease under the admission lock and refuses once a Helper claimed it.
|
|
3582
|
+
// A plain release here would reap whatever the lease became after the caller read it as queued.
|
|
3583
|
+
// An already-reaped lease answers a no-op success, so a retried cancel is safe.
|
|
3519
3584
|
const released = await callWithTransientRetry(
|
|
3520
|
-
() =>
|
|
3585
|
+
() => (opts.cancel
|
|
3586
|
+
? call("cancel_unclaimed_stack_lease", { lease_id: leaseId })
|
|
3587
|
+
: call("release_stack_lease", { lease_id: leaseId, disposition: opts.disposition, ...(idleRelease ? { idle: true } : {}) })), releaseRetry);
|
|
3521
3588
|
const r = released.result;
|
|
3522
3589
|
const unanswered = released.exhausted ? `${describeStackRpcFailure(r)} (no answer in ${released.attempts} attempts over ${Math.round(released.elapsedMs / 1000)}s)` : null;
|
|
3523
3590
|
if (!r.ok) return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, error: unanswered ?? r.error }), opts, r.auth ? EXIT.AUTH : EXIT.BACKEND);
|
|
3591
|
+
if (!r.data.success && opts.cancel && !unanswered) {
|
|
3592
|
+
// Every definite refusal reaps nothing. Only a claimed / not-cancellable lease gets the explicit release hint.
|
|
3593
|
+
const state = typeof r.data.state === "string" ? r.data.state : null;
|
|
3594
|
+
const code = r.data.code ?? "CANCEL_REFUSED";
|
|
3595
|
+
const why = CANCEL_REFUSAL_CODES.has(code)
|
|
3596
|
+
? `lease ${leaseId} ${state ? `is ${state} and ` : ""}is no longer an unclaimed queued lease, so nothing was reaped. ` +
|
|
3597
|
+
`Inspect it with \`bb stack status ${leaseId}\`; release it explicitly with \`bb stack done ${leaseId} --reap\`.`
|
|
3598
|
+
: `nothing was reaped (${r.data.message ?? "the control plane refused the cancel"}).`;
|
|
3599
|
+
return emitResult(buildReceipt({ command: "done", outcome: "refused", lease_id: leaseId, code, ...(state ? { state } : {}),
|
|
3600
|
+
error: `cancel refused (${code}): ${why}` }), opts, EXIT.LEASE_FAILED);
|
|
3601
|
+
}
|
|
3524
3602
|
if (!r.data.success) return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, code: r.data.code, error: unanswered ?? r.data.code }), opts, EXIT.BACKEND);
|
|
3525
3603
|
if (idleRelease && r.data.idle === true) {
|
|
3526
3604
|
return emitResult(buildReceipt({
|
|
@@ -3641,7 +3719,8 @@ export async function cmdDone(leaseId, opts, {
|
|
|
3641
3719
|
portBlock = { released: false, reason: `local claim retained: ${error.code ?? error.message}` };
|
|
3642
3720
|
}
|
|
3643
3721
|
}
|
|
3644
|
-
return emitResult(buildReceipt({ command: "done", outcome: portBlock?.released === false ? "error" : "released", lease_id: leaseId, state, disposition,
|
|
3722
|
+
return emitResult(buildReceipt({ command: "done", outcome: portBlock?.released === false ? "error" : (opts.cancel ? "cancelled" : "released"), lease_id: leaseId, state, disposition,
|
|
3723
|
+
...(opts.cancel && r.data.noop === true ? { noop: true } : {}),
|
|
3645
3724
|
...retried,
|
|
3646
3725
|
...(portBlock ? { port_block: portBlock } : {}),
|
|
3647
3726
|
...(helperTeardown ? { teardown: "helper" } : {}),
|
|
@@ -3827,13 +3906,34 @@ export async function runStackLifecycle(opts, childArgv, adapters = {}) {
|
|
|
3827
3906
|
// A receipt always says what happened to a port-block claim. Before allocation
|
|
3828
3907
|
// no claim exists, which is a safe, explicit disposition rather than omission.
|
|
3829
3908
|
// BOT-1803: whether this run re-adopted / re-provisioned an idle lease, and its stack digest.
|
|
3830
|
-
const
|
|
3831
|
-
let
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3909
|
+
const freshBox = () => ({ incident: null, execution: null, idleLease: null, webUrl: null, portBlock: { released: false, reason: "no port-block claim was created before stack allocation" } });
|
|
3910
|
+
let incidentBox = freshBox();
|
|
3911
|
+
const startedAtMs = (adapters.now ?? Date.now)();
|
|
3912
|
+
const once = async (attemptOpts = opts) => {
|
|
3913
|
+
try {
|
|
3914
|
+
return await runStackLifecycleOnce(attemptOpts, childArgv, adapters, incidentBox);
|
|
3915
|
+
} catch (error) {
|
|
3916
|
+
return { exitCode: classifyStackAuthError(error), outcome: "error", error: String(error?.message ?? error) };
|
|
3917
|
+
}
|
|
3918
|
+
};
|
|
3919
|
+
let result = await once();
|
|
3920
|
+
// BOT-3094: a queue park lost to a session reap requests a new lease ONCE, from scratch: a new
|
|
3921
|
+
// Once re-reads the session credential, re-resolves the stack and claims fresh ports. The lost
|
|
3922
|
+
// lease's cleanup must be proven first, or its ports could still be held.
|
|
3923
|
+
let requeuedFrom = null;
|
|
3924
|
+
// A signal that landed during that cleanup means the caller is stopping: never start again.
|
|
3925
|
+
if (result?.exitCode === EXIT.LEASE_FAILED && result.lostWhileQueued === true && SESSION_REAP_REASONS.has(result.reapReason) &&
|
|
3926
|
+
result.cleanup?.ok !== false && !incidentBox.stopSignal) {
|
|
3927
|
+
requeuedFrom = { lease_id: result.leaseId, reap_reason: result.reapReason };
|
|
3928
|
+
process.stderr.write(`${sessionRequeueNotice(requeuedFrom)}\n`);
|
|
3929
|
+
const { incident } = incidentBox;
|
|
3930
|
+
incidentBox = { ...freshBox(), incident };
|
|
3931
|
+
// One park budget (--timeout) across both attempts.
|
|
3932
|
+
const remaining = remainingParkSecs(opts.timeout, startedAtMs, (adapters.now ?? Date.now)());
|
|
3933
|
+
result = await once({ ...opts, queueParkTimeout: remaining });
|
|
3934
|
+
if (result && typeof result === "object" && requeueRefused(result.leaseId, result.code, result.exitCode, result.outcome)) result = { ...result, exitCode: EXIT.LEASE_FAILED };
|
|
3836
3935
|
}
|
|
3936
|
+
if (requeuedFrom && result && typeof result === "object") result = { ...result, requeuedFrom };
|
|
3837
3937
|
const withIncident = incidentBox.incident && !result?.incident ? { ...result, incident: incidentBox.incident } : result;
|
|
3838
3938
|
const withExecution = incidentBox.execution && !withIncident?.execution ? { ...withIncident, execution: incidentBox.execution } : withIncident;
|
|
3839
3939
|
const withIdle = incidentBox.idleLease && !withExecution?.idleLease ? { ...withExecution, idleLease: incidentBox.idleLease } : withExecution;
|
|
@@ -3851,6 +3951,25 @@ export async function runStackLifecycle(opts, childArgv, adapters = {}) {
|
|
|
3851
3951
|
/** BOT-2467: where a lifecycle result carries its outcome-record channel. */
|
|
3852
3952
|
export const OUTCOME_CHANNEL = Symbol.for("botbuddy.stackRunOutcomeChannel");
|
|
3853
3953
|
|
|
3954
|
+
/** The lease calls `bb stack run` makes over its retrying caller (`rpc.call(name, args, opts)`). */
|
|
3955
|
+
export function defaultStackRunApi(rpc) {
|
|
3956
|
+
return {
|
|
3957
|
+
request: (args) => rpc.call("request_stack_lease", args),
|
|
3958
|
+
// BOT-2780 (beta requester origin): BOT-2499's identified admission, never a fallback.
|
|
3959
|
+
requestIdentified: (args) => rpc.call("request_stack_lease_identified", args),
|
|
3960
|
+
getAdmission: (args) => rpc.call("get_stack_lease_admission", args),
|
|
3961
|
+
get: (leaseId, { signal } = {}) => rpc.call("get_stack_lease", { lease_id: leaseId }, { signal }),
|
|
3962
|
+
// BOT-1803: a heartbeat carries the measured client use when there is one. BOT-3095: it is the
|
|
3963
|
+
// RUN's heartbeat, so it says the run is alive — the server then keeps a quiet live run out of
|
|
3964
|
+
// `no_use` idling (and preemption) up to its ceiling. `bb stack touch` never sends this.
|
|
3965
|
+
touch: (leaseId, usage, { signal } = {}) => rpc.call("touch_stack_lease", { lease_id: leaseId, ...(usage ? { usage } : {}), run_alive: true }, { signal }),
|
|
3966
|
+
// BOT-1803: `{ idle: true }` idles an ACTIVE lease instead of reaping it.
|
|
3967
|
+
release: (leaseId, signal, { idle = false } = {}) => rpc.call("release_stack_lease", { lease_id: leaseId, disposition: "destroy", ...(idle ? { idle: true } : {}) }, { signal }),
|
|
3968
|
+
// BOT-2400: keep a queued lease's place when the park deadline passes.
|
|
3969
|
+
hold: (leaseId, holdSecs) => holdQueuePlace((name, args) => rpc.call(name, args), leaseId, holdSecs),
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3854
3973
|
async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
3855
3974
|
// BOT-1750 (Codex R9/R11 P2): `stack run` is a lease-creating entry point AND a
|
|
3856
3975
|
// long-lived one (queue → provision → child → heartbeat → cleanup). A cached token
|
|
@@ -3872,19 +3991,7 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
3872
3991
|
});
|
|
3873
3992
|
// BOT-2467: cmdRun records the run's outcome with the credential this run ended on.
|
|
3874
3993
|
incidentBox.outcomeChannel = { get auth() { return rpc.auth; }, callTool: adapters.callTool ?? callToolJson };
|
|
3875
|
-
const api = adapters.api ??
|
|
3876
|
-
request: (args) => rpc.call("request_stack_lease", args),
|
|
3877
|
-
// BOT-2780 (beta requester origin): BOT-2499's identified admission, never a fallback.
|
|
3878
|
-
requestIdentified: (args) => rpc.call("request_stack_lease_identified", args),
|
|
3879
|
-
getAdmission: (args) => rpc.call("get_stack_lease_admission", args),
|
|
3880
|
-
get: (leaseId, { signal } = {}) => rpc.call("get_stack_lease", { lease_id: leaseId }, { signal }),
|
|
3881
|
-
// BOT-1803: a heartbeat carries the measured client use when there is one.
|
|
3882
|
-
touch: (leaseId, usage, { signal } = {}) => rpc.call("touch_stack_lease", { lease_id: leaseId, ...(usage ? { usage } : {}) }, { signal }),
|
|
3883
|
-
// BOT-1803: `{ idle: true }` idles an ACTIVE lease instead of reaping it.
|
|
3884
|
-
release: (leaseId, signal, { idle = false } = {}) => rpc.call("release_stack_lease", { lease_id: leaseId, disposition: "destroy", ...(idle ? { idle: true } : {}) }, { signal }),
|
|
3885
|
-
// BOT-2400: keep a queued lease's place when the park deadline passes.
|
|
3886
|
-
hold: (leaseId, holdSecs) => holdQueuePlace((name, args) => rpc.call(name, args), leaseId, holdSecs),
|
|
3887
|
-
};
|
|
3994
|
+
const api = adapters.api ?? defaultStackRunApi(rpc);
|
|
3888
3995
|
// An injected api double without `hold` keeps the pre-BOT-2400 release unless the test also
|
|
3889
3996
|
// injected the tool transport (never a real network call from a test).
|
|
3890
3997
|
const holdPlace = api.hold ?? (adapters.callTool ? (leaseId, holdSecs) => holdQueuePlace((name, args) => rpc.call(name, args), leaseId, holdSecs) : null);
|
|
@@ -4800,7 +4907,10 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4800
4907
|
// BOT-1630: ONE park budget (--timeout) across every park. A Helper that finds the host under
|
|
4801
4908
|
// pressure mid-provision defers the lease back to `queued` (its place kept); run then parks
|
|
4802
4909
|
// again against the same deadline instead of timing out on the provision clock.
|
|
4803
|
-
|
|
4910
|
+
// BOT-3094: a session re-request's queue park gets only what is left of --timeout (queueParkTimeout);
|
|
4911
|
+
// the provision and own-reap waits keep the full --timeout.
|
|
4912
|
+
const parkSecs = opts.queueParkTimeout ?? opts.timeout;
|
|
4913
|
+
const parkDeadlineMs = (adapters.now ?? Date.now)() + parkSecs * 1000;
|
|
4804
4914
|
let parkView = request.data;
|
|
4805
4915
|
let parkReason = null;
|
|
4806
4916
|
for (;;) {
|
|
@@ -4818,7 +4928,7 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4818
4928
|
// BOT-2013: every reap state (reaping / reaped / reap_failed) means the queued lease is lost.
|
|
4819
4929
|
// BOT-2099: an auth/availability blip is retried inside the park until its deadline;
|
|
4820
4930
|
// it never releases a queued lease or becomes the lease's outcome.
|
|
4821
|
-
const parked = await resilientWait(leaseId, (s) => s !== "queued" && s != null, isReaped, { timeoutSec:
|
|
4931
|
+
const parked = await resilientWait(leaseId, (s) => s !== "queued" && s != null, isReaped, { timeoutSec: parkSecs, deadlineMs: parkDeadlineMs, auth: rpc.auth, signal: activeWaitAbort.signal });
|
|
4822
4932
|
activeWaitAbort = null;
|
|
4823
4933
|
const incident = parkIncident ? { incident: parkIncident } : {};
|
|
4824
4934
|
if (parked?.interrupted || receivedSignal) { const cleanup = await requestCleanup(); return { exitCode: SIGNAL_EXIT[receivedSignal], outcome: "interrupted", leaseId, cleanup, ...incident }; }
|
|
@@ -4835,8 +4945,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4835
4945
|
// lease's admission, so it must not claim the host had no capacity.
|
|
4836
4946
|
const blind = parkIncident && !parkIncident.recovered;
|
|
4837
4947
|
const why = blind
|
|
4838
|
-
? `lease ${leaseId} parked ${
|
|
4839
|
-
: `parked ${
|
|
4948
|
+
? `lease ${leaseId} parked ${parkSecs}s inside a platform incident (${parkIncident.reason} since ${parkIncident.first_at}), so its admission was never observed`
|
|
4949
|
+
: `parked ${parkSecs}s without capacity`;
|
|
4840
4950
|
// BOT-2400 (item 1): the park deadline never gives the queue place up. Hold the lease
|
|
4841
4951
|
// for the owner's next run (which re-attaches to it) and exit PARK_DEADLINE; only
|
|
4842
4952
|
// `stack cancel` / `stack done` or the owner's death releases it.
|
|
@@ -4877,7 +4987,12 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4877
4987
|
...incident, ...(ledger ? { ledger } : {}), ...(queueHold ? { queueHold } : {}), ...(parkReason ? { parkReason } : {}),
|
|
4878
4988
|
};
|
|
4879
4989
|
}
|
|
4880
|
-
if (parked?.failed) {
|
|
4990
|
+
if (parked?.failed) {
|
|
4991
|
+
const cleanup = await requestCleanup();
|
|
4992
|
+
// BOT-3094: lostWhileQueued + reapReason let runStackLifecycle re-request after a session reap.
|
|
4993
|
+
return { exitCode: EXIT.LEASE_FAILED, outcome: "error", leaseId, error: `lease was ${parked.state} while queued (reap_reason=${parked.reap_reason ?? "unknown"})`, cleanup,
|
|
4994
|
+
lostWhileQueued: true, ...(parked.reap_reason ? { reapReason: parked.reap_reason } : {}), ...incident };
|
|
4995
|
+
}
|
|
4881
4996
|
if (parked?.error || parked?.auth) {
|
|
4882
4997
|
const cleanup = await requestCleanup();
|
|
4883
4998
|
return {
|
|
@@ -5188,6 +5303,10 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
5188
5303
|
};
|
|
5189
5304
|
heartbeat = clock.setInterval(touchLease, cadenceMs);
|
|
5190
5305
|
heartbeat.unref?.();
|
|
5306
|
+
// BOT-3095: one heartbeat as the child starts, not a full cadence later, so a run that
|
|
5307
|
+
// re-adopted an idle stack says it is alive before the next minute's idle pass.
|
|
5308
|
+
const firstTouch = clock.setTimeout(() => touchLease(), 0);
|
|
5309
|
+
firstTouch?.unref?.();
|
|
5191
5310
|
if (opts.hardTtl) hardTimer = clock.setTimeout(() => stopChild("SIGTERM", EXIT.TIMEOUT), opts.hardTtl * 1000);
|
|
5192
5311
|
hardTimer?.unref?.();
|
|
5193
5312
|
childResult = await waitForChild(child);
|
|
@@ -5320,6 +5439,9 @@ export function runReceiptFields(result) {
|
|
|
5320
5439
|
...(result.idleLease?.stackDigest ? { stack_digest: result.idleLease.stackDigest } : {}),
|
|
5321
5440
|
...(result.idleLease?.readoptHealth ? { readopt_health: result.idleLease.readoptHealth } : {}),
|
|
5322
5441
|
...(result.parkReason ? { park_reason: result.parkReason } : {}),
|
|
5442
|
+
// BOT-3094: why a queued lease was lost, and the lease a session re-request replaced.
|
|
5443
|
+
...(result.reapReason ? { reap_reason: result.reapReason } : {}),
|
|
5444
|
+
...(result.requeuedFrom ? { requeued_from: result.requeuedFrom } : {}),
|
|
5323
5445
|
// BOT-2142: where the lease ran and how the child reached it.
|
|
5324
5446
|
execution_mode: result.execution?.mode ?? null,
|
|
5325
5447
|
execution_host_key: result.execution?.hostKey ?? null,
|