@botbuddy/cli 1.63.33 → 1.63.35
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/daemon/bin/darwin-arm64/botbuddy-daemon +0 -0
- package/daemon/bin/linux-x64/botbuddy-daemon +0 -0
- package/daemon-release/darwin-arm64.json +4 -4
- package/daemon-release/linux-x64.json +4 -4
- package/daemon-release/manifest.json +5 -5
- package/package.json +1 -1
- package/src/run.mjs +47 -2
- package/src/wait-core.mjs +24 -1
- package/src/wait.mjs +14 -0
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.35",
|
|
3
|
+
"source_commit": "4517f4b0fe820afa3c976ac2a07bbbf02253fb86",
|
|
4
|
+
"source_sha256": "20ba1366169f553685a44b2ffb1d08b8155f8c91e6a6c215909c5fb0825a5f81",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "e397a1b045b4ccd213e9a7a7dbc3631b83d8af2e0dff67b90b81c365ed9b1b41"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.35",
|
|
3
|
+
"source_commit": "4517f4b0fe820afa3c976ac2a07bbbf02253fb86",
|
|
4
|
+
"source_sha256": "20ba1366169f553685a44b2ffb1d08b8155f8c91e6a6c215909c5fb0825a5f81",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "c2a2d4b815867bf1e6c4008cbb2eda12d1aecee16e3f11727e445307742a5521"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.63.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.63.35",
|
|
4
|
+
"source_commit": "4517f4b0fe820afa3c976ac2a07bbbf02253fb86",
|
|
5
|
+
"source_sha256": "20ba1366169f553685a44b2ffb1d08b8155f8c91e6a6c215909c5fb0825a5f81",
|
|
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": "e397a1b045b4ccd213e9a7a7dbc3631b83d8af2e0dff67b90b81c365ed9b1b41",
|
|
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": "c2a2d4b815867bf1e6c4008cbb2eda12d1aecee16e3f11727e445307742a5521",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/run.mjs
CHANGED
|
@@ -35,6 +35,11 @@ export const EXIT = Object.freeze({ OK: 0, AUTH: 3, INVALID: 4, BACKEND: 5, INTE
|
|
|
35
35
|
export const DEFAULT_TIMEOUT_SECONDS = 8 * 60 * 60;
|
|
36
36
|
export const TIMEOUT_GRACE_MS = 5_000;
|
|
37
37
|
const MAX_CAPTURE_BYTES = 8_192;
|
|
38
|
+
// BOT-2530: a detached worker holds a server-clocked liveness lease on its run.
|
|
39
|
+
// It renews every 30 s; four missed renewals (120 s) let the server's reconciler
|
|
40
|
+
// terminalize the run as owner_lost so a `command-run:` waiter always ends.
|
|
41
|
+
export const WORKER_HEARTBEAT_TTL_SECONDS = 120;
|
|
42
|
+
export const WORKER_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
38
43
|
// BOT-1510: how often the worker flushes lane case verdicts to the backend.
|
|
39
44
|
// Well under AC-3's 5 s so a begin/verdict is visible within the window.
|
|
40
45
|
const LANE_FLUSH_INTERVAL_MS = 2_500;
|
|
@@ -583,6 +588,7 @@ export async function launchRun(argv, {
|
|
|
583
588
|
command_hash: commandFingerprint, ...(launchSourceSha ? { source_sha: launchSourceSha } : {}), environment_hash: environmentHash(cwd, opts.environment),
|
|
584
589
|
environment: opts.environment, command_kind: opts.kind, expected_duration_seconds: opts.expectedDuration,
|
|
585
590
|
rerun_reason: opts.rerunReason,
|
|
591
|
+
worker_heartbeat_ttl_seconds: WORKER_HEARTBEAT_TTL_SECONDS,
|
|
586
592
|
});
|
|
587
593
|
let registration = await effectiveCall("register_command", registerArgs());
|
|
588
594
|
// A cached session can be revoked server-side before its local expiry. The
|
|
@@ -610,7 +616,7 @@ export async function launchRun(argv, {
|
|
|
610
616
|
// every callback is attributable to the same agent. It deliberately stays
|
|
611
617
|
// outside child_env, receipts, and the workload's process environment.
|
|
612
618
|
const knownSession = opts.sessionId ?? managedCredential?.identity?.sessionId ?? managedCredential?.sessionId;
|
|
613
|
-
await writeJson(context, { schema_version: RUN_SCHEMA_VERSION, context_path: context, run_id: runId, run_credential: registration.data.run_credential, receipt_upload: registration.data.receipt_upload, command, command_hash: commandFingerprint, source_sha: launchSourceSha, cwd, timeout_seconds: opts.timeout, receipt_path: receipt, launched_at: new Date().toISOString(), server_url: SERVER_URL, ...(opts.sessionToken ? { callback_session_token: opts.sessionToken } : {}), ...(knownSession ? { callback_session_id: knownSession } : {}), ...(childEnv && Object.keys(childEnv).length ? { child_env: childEnv } : {}), ...(testRun ? { test_run: testRun } : {}) });
|
|
619
|
+
await writeJson(context, { schema_version: RUN_SCHEMA_VERSION, context_path: context, run_id: runId, run_credential: registration.data.run_credential, receipt_upload: registration.data.receipt_upload, command, command_hash: commandFingerprint, source_sha: launchSourceSha, cwd, timeout_seconds: opts.timeout, receipt_path: receipt, launched_at: new Date().toISOString(), server_url: SERVER_URL, worker_heartbeat_ttl_seconds: WORKER_HEARTBEAT_TTL_SECONDS, ...(opts.sessionToken ? { callback_session_token: opts.sessionToken } : {}), ...(knownSession ? { callback_session_id: knownSession } : {}), ...(childEnv && Object.keys(childEnv).length ? { child_env: childEnv } : {}), ...(testRun ? { test_run: testRun } : {}) });
|
|
614
620
|
const [file, args] = workerInvocation(context);
|
|
615
621
|
try {
|
|
616
622
|
const worker = spawnImpl(file, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
@@ -623,7 +629,10 @@ export async function launchRun(argv, {
|
|
|
623
629
|
}
|
|
624
630
|
return { exitCode: EXIT.OK, receipt: { schema_version: RUN_SCHEMA_VERSION, outcome: "launched", run_id: runId,
|
|
625
631
|
...(knownSession ? { session_id: knownSession } : { session_binding: "server_derived" }),
|
|
626
|
-
durable_owner: "detached_worker", receipt_path: receipt, reentry: "update_command_run publishes the terminal callback for this run_id",
|
|
632
|
+
durable_owner: "detached_worker", receipt_path: receipt, reentry: "update_command_run publishes the terminal callback for this run_id",
|
|
633
|
+
// BOT-2318: the exact pending wait for this run. Keep its result pending in
|
|
634
|
+
// the same work session; its return is what resumes the caller (BOT-2501).
|
|
635
|
+
wait_command: `bb wait 'command-run:id=${runId}'`, exit_code: EXIT.OK } };
|
|
627
636
|
}
|
|
628
637
|
|
|
629
638
|
export async function runWorker(contextFile, {
|
|
@@ -633,11 +642,14 @@ export async function runWorker(contextFile, {
|
|
|
633
642
|
terminate = terminateProcessGroup,
|
|
634
643
|
// BOT-2600: the bounded terminal delivery (see retryTerminalUpdate).
|
|
635
644
|
sleep, now, terminalBudgetMs, startDelivery,
|
|
645
|
+
// BOT-2530: worker liveness lease renewal cadence (tests inject a fake timer).
|
|
646
|
+
workerHeartbeatIntervalMs = WORKER_HEARTBEAT_INTERVAL_MS, setIntervalFn = setInterval,
|
|
636
647
|
} = {}) {
|
|
637
648
|
const retry = { sleep, now, budgetMs: terminalBudgetMs, startDelivery };
|
|
638
649
|
const context = JSON.parse(await readFile(contextFile, "utf8"));
|
|
639
650
|
const callbackCall = callWithContextIdentity(context, call);
|
|
640
651
|
const ownerHeartbeat = startOwnerHeartbeat(context, callbackCall);
|
|
652
|
+
const workerLease = startWorkerLease(context, callbackCall, { intervalMs: workerHeartbeatIntervalMs, setIntervalFn });
|
|
641
653
|
const startedAt = new Date().toISOString();
|
|
642
654
|
let timedOut = false;
|
|
643
655
|
let child;
|
|
@@ -738,11 +750,44 @@ export async function runWorker(contextFile, {
|
|
|
738
750
|
const receipt = await finalize(context, { status, startedAt, endedAt, exitCode: result.exitCode, signal: result.signal, output: capture.output, outputCharacters: capture.characters }, callbackCall, retry);
|
|
739
751
|
return receipt;
|
|
740
752
|
} finally {
|
|
753
|
+
if (workerLease) await workerLease.stop();
|
|
741
754
|
if (ownerHeartbeat) await ownerHeartbeat.stop();
|
|
742
755
|
signalEmitter.removeListener("SIGTERM", cancellationHandler);
|
|
743
756
|
}
|
|
744
757
|
}
|
|
745
758
|
|
|
759
|
+
// BOT-2530: renew this run's liveness lease while the worker lives. The first
|
|
760
|
+
// renewal also moves a registered run to running. A rejected or failed renewal
|
|
761
|
+
// is not retried specially: the next tick tries again, and a terminal run
|
|
762
|
+
// refuses renewals server-side. Contexts sealed by an older launcher (no
|
|
763
|
+
// declared lease) send nothing.
|
|
764
|
+
export function startWorkerLease(context, call, { intervalMs = WORKER_HEARTBEAT_INTERVAL_MS, setIntervalFn = setInterval } = {}) {
|
|
765
|
+
if (!Number.isInteger(context.worker_heartbeat_ttl_seconds)) return null;
|
|
766
|
+
let stopped = false;
|
|
767
|
+
let chain = Promise.resolve();
|
|
768
|
+
const tick = () => {
|
|
769
|
+
chain = chain.then(async () => {
|
|
770
|
+
if (stopped) return;
|
|
771
|
+
try {
|
|
772
|
+
await call("update_command_run", { run_id: context.run_id, run_credential: context.run_credential, status: "running", worker_heartbeat: true }, {
|
|
773
|
+
signal: AbortSignal.timeout(DELIVERY_CALL_TIMEOUT_MS),
|
|
774
|
+
...(typeof context.server_url === "string" && context.server_url ? { serverUrl: context.server_url } : {}),
|
|
775
|
+
});
|
|
776
|
+
} catch { /* the lease lapses only after several missed renewals */ }
|
|
777
|
+
});
|
|
778
|
+
};
|
|
779
|
+
tick();
|
|
780
|
+
const timer = setIntervalFn(tick, intervalMs);
|
|
781
|
+
timer.unref?.();
|
|
782
|
+
return { async stop() {
|
|
783
|
+
stopped = true;
|
|
784
|
+
clearInterval(timer);
|
|
785
|
+
let bound;
|
|
786
|
+
await Promise.race([chain, new Promise((resolve) => { bound = setTimeout(resolve, 2_000); bound.unref?.(); })]);
|
|
787
|
+
clearTimeout(bound);
|
|
788
|
+
} };
|
|
789
|
+
}
|
|
790
|
+
|
|
746
791
|
function startOwnerHeartbeat(context, call, { intervalMs = 5 * 60_000, setIntervalFn = setInterval } = {}) {
|
|
747
792
|
// A detached worker must never acquire liveness through its ambient default
|
|
748
793
|
// identity. It can only refresh the bearer sealed for its owner.
|
package/src/wait-core.mjs
CHANGED
|
@@ -46,6 +46,12 @@ export const EXIT = Object.freeze({
|
|
|
46
46
|
// four, optionally wrapped in ONE pair of braces.
|
|
47
47
|
const LEASE_UUID_SPELLING = /^(?:\{(?=.*\}$)|(?!.*\}$))((?:[0-9a-f]{4}-?){7}[0-9a-f]{4})\}?$/;
|
|
48
48
|
|
|
49
|
+
// BOT-2318: bb run generates a lower-case UUID before register_command.
|
|
50
|
+
const COMMAND_RUN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
51
|
+
export const COMMAND_RUN_TERMINAL_STATUSES = Object.freeze(new Set([
|
|
52
|
+
"succeeded", "failed", "cancelled", "timed_out", "owner_lost", "invalidated",
|
|
53
|
+
]));
|
|
54
|
+
|
|
49
55
|
// BOT-2013: stack_lease frame states that mean the lease is gone, not granted.
|
|
50
56
|
export const LEASE_LOST_STATES = Object.freeze(new Set(["reaping", "reaped", "reap_failed"]));
|
|
51
57
|
// BOT-2400: the lease states a `lease:id=…,state=<s>` wait may target.
|
|
@@ -198,6 +204,18 @@ const VALIDATORS = {
|
|
|
198
204
|
if (!p.id) throw new Error("test-run needs id=<run_id>");
|
|
199
205
|
return { runId: String(p.id) };
|
|
200
206
|
},
|
|
207
|
+
// BOT-2318: server binds this selector to the registering tenant, agent and work session.
|
|
208
|
+
"command-run": (p, raw) => {
|
|
209
|
+
const pair = /^\s*id\s*=\s*([0-9A-Fa-f-]+)\s*$/.exec(raw ?? "");
|
|
210
|
+
if (!pair || Object.keys(p).length !== 1 || p.id !== pair[1]) {
|
|
211
|
+
throw new Error("command-run needs exactly one id=<registered run id>");
|
|
212
|
+
}
|
|
213
|
+
const runId = pair[1].toLowerCase();
|
|
214
|
+
if (!COMMAND_RUN_ID.test(runId)) {
|
|
215
|
+
throw new Error("command-run id must be the canonical UUID that bb run printed");
|
|
216
|
+
}
|
|
217
|
+
return { runId };
|
|
218
|
+
},
|
|
201
219
|
// BOT-2629: this is deliberately a dedicated condition rather than a raw
|
|
202
220
|
// event escape hatch. The UUID is chosen before dispatch and the exact subject
|
|
203
221
|
// prevents another run on the same idle/re-adopted lease waking this waiter.
|
|
@@ -620,7 +638,7 @@ export function parseConditions(specs) {
|
|
|
620
638
|
return;
|
|
621
639
|
}
|
|
622
640
|
try {
|
|
623
|
-
const params = validator(parseParams(rawParams));
|
|
641
|
+
const params = validator(parseParams(rawParams), rawParams);
|
|
624
642
|
conditions.push({ id: `c${index + 1}`, type, params });
|
|
625
643
|
} catch (err) {
|
|
626
644
|
errors.push({ spec, message: err.message });
|
|
@@ -1210,6 +1228,11 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
|
|
|
1210
1228
|
prReviewFiltersMatch(signal, params, condition.review_guard);
|
|
1211
1229
|
case "test-run":
|
|
1212
1230
|
return signal.signal_type === "test_run" && signal.subject_key === `run:${params.runId}`;
|
|
1231
|
+
case "command-run":
|
|
1232
|
+
return signal.signal_type === "command_run" &&
|
|
1233
|
+
signal.subject_key === `run:${params.runId}` &&
|
|
1234
|
+
signal.payload?.run_id === params.runId &&
|
|
1235
|
+
COMMAND_RUN_TERMINAL_STATUSES.has(signal.payload?.status);
|
|
1213
1236
|
case "stack-run":
|
|
1214
1237
|
return signal.signal_type === "stack_run" && signal.subject_key === `stack-run:${params.id}` &&
|
|
1215
1238
|
signal.payload?.run_id === params.id && Number.isInteger(signal.payload?.exit_code);
|
package/src/wait.mjs
CHANGED
|
@@ -152,6 +152,14 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
|
|
|
152
152
|
valid workflow=/workflow_id=/workflow_path= lines.
|
|
153
153
|
test-run:id=<run_id> a test run reaching a terminal status (owner-scoped);
|
|
154
154
|
bb test <lane> prints the exact line to arm.
|
|
155
|
+
command-run:id=<run_id> BOT-2318: the bb run you registered reaching a terminal
|
|
156
|
+
outcome (succeeded, failed, cancelled, timed_out,
|
|
157
|
+
owner_lost, invalidated). Exit 0 on any terminal outcome;
|
|
158
|
+
the receipt's status says how the command ended. Arm it
|
|
159
|
+
from the same work session that ran bb run, as the ONLY
|
|
160
|
+
condition, and keep this wait's result pending: its
|
|
161
|
+
return is what resumes your task. A run whose worker is
|
|
162
|
+
lost ends owner_lost (bb run inspect <run_id>).
|
|
155
163
|
stack-run:id=<run UUID> one named bb stack run reaching its terminal outcome
|
|
156
164
|
(BOT-2629). Choose the UUID BEFORE dispatch and pass it as
|
|
157
165
|
bb stack run --run-id <UUID>; the receipt carries exit_code,
|
|
@@ -585,6 +593,12 @@ async function registerWait(opts, conditions, deadlineIso) {
|
|
|
585
593
|
// caller-actionable hard stops, not the untracked live-only fallback.
|
|
586
594
|
"chat_not_participant",
|
|
587
595
|
"chat_participant_ambiguous",
|
|
596
|
+
// BOT-2318: command-run targets are authorization-bound to the registering
|
|
597
|
+
// tenant, agent and work session, and the condition cannot be composed.
|
|
598
|
+
// Both refusals require the caller to change its target/conditions; falling
|
|
599
|
+
// back to an untracked wait would only hide that error behind a timeout.
|
|
600
|
+
"command_run_unavailable",
|
|
601
|
+
"command_run_single_condition_required",
|
|
588
602
|
]);
|
|
589
603
|
if (INVALID_CONDITION_CODES.has(body.error)) {
|
|
590
604
|
const err = new Error(body.detail || body.error);
|