@botbuddy/cli 1.42.1 → 1.43.0
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 +1 -1
- package/src/stack.mjs +49 -2
package/package.json
CHANGED
package/src/stack.mjs
CHANGED
|
@@ -60,6 +60,9 @@ const CALLBACK_PORT_REMOTE_REFUSAL =
|
|
|
60
60
|
"(pre-forward GatewayPorts probe). Use --local-only for a run that needs a callback port.";
|
|
61
61
|
export const DEFAULT_RECEIPT_MAX_BYTES = 10240;
|
|
62
62
|
export const DEFAULT_TIMEOUT_SEC = 3600;
|
|
63
|
+
/** BOT-2230: the bounds the control plane enforces on an owner place hold (hold_stack_lease_place). */
|
|
64
|
+
export const HOLD_ON_TIMEOUT_MIN_SEC = 60;
|
|
65
|
+
export const HOLD_ON_TIMEOUT_MAX_SEC = 14400;
|
|
63
66
|
/** How often a run renews its stack directory's retention while it still exists
|
|
64
67
|
* (BOT-1798): frequent enough to cover the pre-child queue/provision waits. */
|
|
65
68
|
const RETENTION_REFRESH_MS = 5 * 60 * 1000;
|
|
@@ -104,6 +107,13 @@ ${bold("up OPTIONS")}
|
|
|
104
107
|
--idle-ttl <seconds> Idle seconds before the reaper STOPS an unused stack (default 1800).
|
|
105
108
|
--timeout <seconds> Max seconds to park for capacity before giving up (default ${DEFAULT_TIMEOUT_SEC}).
|
|
106
109
|
--no-wait If the host is full, print the queue position and exit (don't park).
|
|
110
|
+
--hold-on-timeout <seconds>
|
|
111
|
+
BOT-2230: when the --timeout park runs out with the lease still queued,
|
|
112
|
+
keep its queue place for <seconds> (60–14400) instead of losing it: the
|
|
113
|
+
lease stays queued but is not dispatched, and re-running the same
|
|
114
|
+
\`stack up\` (same agent + slot) before then re-attaches to it. The exit-2
|
|
115
|
+
receipt carries queue_hold {held, until, queue_position}; \`stack done
|
|
116
|
+
<lease_id>\` gives the place up.
|
|
107
117
|
--claim-only LOCAL-EXEC only: wait for and hold a provisioning lease without starting
|
|
108
118
|
Supabase. Resume with the identical \`stack up --local-exec\` command.
|
|
109
119
|
--wait-capacity <dur> Opt-in (BOT-2024): on a TRANSIENT HOST_CAPACITY_SOURCE_STALE refusal, park
|
|
@@ -255,7 +265,7 @@ export function parseStackArgs(argv) {
|
|
|
255
265
|
slot: null, host: null, repo: null, ticket: null, ticketUrl: null,
|
|
256
266
|
prId: null, prUrl: null, purpose: null, idleTtl: null, stackPath: ".", stackPathExplicit: false,
|
|
257
267
|
timeout: DEFAULT_TIMEOUT_SEC, provisionTimeout: null, reapTimeout: 300, hardTtl: null,
|
|
258
|
-
connectionFile: null, noWait: false, claimOnly: false, localExec: false, waitCapacity: null,
|
|
268
|
+
connectionFile: null, noWait: false, claimOnly: false, localExec: false, waitCapacity: null, holdOnTimeout: null,
|
|
259
269
|
dockerContext: null, dockerEndpoint: null,
|
|
260
270
|
// BOT-2142: execution placement (null = command default), remote-lease callback + bundle cap.
|
|
261
271
|
execution: null, remote: false, localOnly: false, callbackPort: null, bundleMaxBytes: null,
|
|
@@ -356,6 +366,15 @@ export function parseStackArgs(argv) {
|
|
|
356
366
|
}
|
|
357
367
|
break;
|
|
358
368
|
}
|
|
369
|
+
case "--hold-on-timeout": {
|
|
370
|
+
if (need(a, rest[i + 1])) {
|
|
371
|
+
const n = Number(rest[++i]);
|
|
372
|
+
if (!Number.isInteger(n) || n < HOLD_ON_TIMEOUT_MIN_SEC || n > HOLD_ON_TIMEOUT_MAX_SEC) {
|
|
373
|
+
errors.push(`--hold-on-timeout must be a whole number of seconds from ${HOLD_ON_TIMEOUT_MIN_SEC} to ${HOLD_ON_TIMEOUT_MAX_SEC}`);
|
|
374
|
+
} else opts.holdOnTimeout = n;
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
359
378
|
case "--no-wait": opts.noWait = true; break;
|
|
360
379
|
case "--claim-only": opts.claimOnly = true; break;
|
|
361
380
|
case "--local-exec": opts.localExec = true; break;
|
|
@@ -410,6 +429,11 @@ export function parseStackArgs(argv) {
|
|
|
410
429
|
if (opts.waitCapacity != null && !["up", "run"].includes(command)) {
|
|
411
430
|
errors.push("--wait-capacity is valid only with `stack up` or `stack run`");
|
|
412
431
|
}
|
|
432
|
+
if (opts.holdOnTimeout != null && command !== "up") {
|
|
433
|
+
errors.push("--hold-on-timeout is valid only with `stack up`");
|
|
434
|
+
} else if (opts.holdOnTimeout != null && opts.noWait) {
|
|
435
|
+
errors.push("--hold-on-timeout cannot be combined with --no-wait: without a park there is no timeout to keep a place across");
|
|
436
|
+
}
|
|
413
437
|
if (opts.claimOnly && (command !== "up" || !opts.localExec)) {
|
|
414
438
|
errors.push("--claim-only is valid only with `stack up --local-exec`");
|
|
415
439
|
}
|
|
@@ -453,6 +477,22 @@ export function describeQueuePosition(d) {
|
|
|
453
477
|
return d?.scored_ticket_id ? `${where}, scored under ${d.scored_ticket_id}` : where;
|
|
454
478
|
}
|
|
455
479
|
|
|
480
|
+
/**
|
|
481
|
+
* BOT-2230: hold a timed-out queued lease's place for its owner (`hold_stack_lease_place`).
|
|
482
|
+
* Never throws: a refusal or a control plane without the tool is reported as `held: false`
|
|
483
|
+
* with the server's code, so the caller decides (the hermetic lane then cancels as before).
|
|
484
|
+
*/
|
|
485
|
+
export async function holdQueuePlace(call, leaseId, holdSecs) {
|
|
486
|
+
let res;
|
|
487
|
+
try { res = await call("hold_stack_lease_place", { lease_id: leaseId, hold_secs: holdSecs }); }
|
|
488
|
+
catch (err) { return { held: false, code: null, error: String(err?.message ?? err) }; }
|
|
489
|
+
const d = res?.data;
|
|
490
|
+
if (res?.ok && d?.success) {
|
|
491
|
+
return { held: true, until: d.queue_hold_until ?? null, queue_position: d.queue_position ?? null, hold_secs: d.hold_secs ?? holdSecs };
|
|
492
|
+
}
|
|
493
|
+
return { held: false, code: d?.code ?? null, error: d?.message || d?.error || res?.error || "hold refused" };
|
|
494
|
+
}
|
|
495
|
+
|
|
456
496
|
/** BOT-2019: the queue fields a server returned for a queued lease, for a receipt. */
|
|
457
497
|
/**
|
|
458
498
|
* BOT-2099: the human lines for the server's unit ledger (`stack_lease_unit_ledger`) — who
|
|
@@ -2107,7 +2147,14 @@ export async function cmdUp(opts, {
|
|
|
2107
2147
|
let ledger = d.ledger ?? null;
|
|
2108
2148
|
try { const read = await call("get_stack_lease", { lease_id: leaseId }); if (read?.ok && read.data?.success && read.data.ledger) ledger = read.data.ledger; } catch { /* keep the request-time ledger */ }
|
|
2109
2149
|
for (const line of describeUnitLedger(ledger)) process.stderr.write(`stack: ${line}\n`);
|
|
2110
|
-
|
|
2150
|
+
// BOT-2230: keep the queue place for the owner's next attempt instead of losing it.
|
|
2151
|
+
const queueHold = opts.holdOnTimeout ? await holdQueuePlace(call, leaseId, opts.holdOnTimeout) : null;
|
|
2152
|
+
if (queueHold?.held) {
|
|
2153
|
+
process.stderr.write(`stack: lease ${leaseId} keeps queue place ${queueHold.queue_position ?? "?"} until ${queueHold.until} — re-run the same \`stack up\` before then to re-attach (\`stack done ${leaseId}\` gives it up)\n`);
|
|
2154
|
+
} else if (queueHold) {
|
|
2155
|
+
process.stderr.write(`${yellow("⚠")} stack: lease ${leaseId} could not keep its queue place (${queueHold.code ?? "error"}: ${queueHold.error})\n`);
|
|
2156
|
+
}
|
|
2157
|
+
return emitResult(buildReceipt({ command: "up", outcome: "timeout", lease_id: leaseId, state: "queued", slot, error: `parked ${opts.timeout}s without capacity`, ...(ledger ? { unit_ledger: ledger } : {}), ...(queueHold ? { queue_hold: queueHold } : {}) }), opts, EXIT.TIMEOUT);
|
|
2111
2158
|
}
|
|
2112
2159
|
if (parked.auth) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: "unauthorized on wait stream" }), opts, EXIT.AUTH);
|
|
2113
2160
|
if (parked.error) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: parked.error }), opts, EXIT.BACKEND);
|