@botbuddy/cli 1.63.44 → 1.64.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.
- 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/daemon/automatic.mjs +81 -0
- package/src/daemon/commands.mjs +8 -0
- package/src/daemon/handover.mjs +2 -2
- package/src/pw/staging-auth.mjs +40 -1
- package/src/staging-pool/cli.mjs +1 -0
- package/src/staging-pool/config.mjs +8 -2
- package/src/staging-pool/refresh-runtime.mjs +1 -1
- package/src/wait-core.mjs +14 -0
- package/src/wait.mjs +6 -0
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.64.1",
|
|
3
|
+
"source_commit": "fd01ca263ebebf1dda5ec3648d5300a788ee7887",
|
|
4
|
+
"source_sha256": "75f8bdc7954b24d97d7da80abe41f3814bf9bc9f9c00558d26cb78c030b58475",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "929631f9c2d05a901a16e8ba132c4166ca62b2c7b30ab5b8317cbcd59c49ee79"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.64.1",
|
|
3
|
+
"source_commit": "fd01ca263ebebf1dda5ec3648d5300a788ee7887",
|
|
4
|
+
"source_sha256": "75f8bdc7954b24d97d7da80abe41f3814bf9bc9f9c00558d26cb78c030b58475",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "fc806cb0d6193a2c6c55eb076a28575b1fabc80955f4f9669b9f15269cbcdabb"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.64.1",
|
|
4
|
+
"source_commit": "fd01ca263ebebf1dda5ec3648d5300a788ee7887",
|
|
5
|
+
"source_sha256": "75f8bdc7954b24d97d7da80abe41f3814bf9bc9f9c00558d26cb78c030b58475",
|
|
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": "929631f9c2d05a901a16e8ba132c4166ca62b2c7b30ab5b8317cbcd59c49ee79",
|
|
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": "fc806cb0d6193a2c6c55eb076a28575b1fabc80955f4f9669b9f15269cbcdabb",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { callToolJson, resolveCallAuth } from "../api.mjs";
|
|
2
|
+
import {
|
|
3
|
+
assertSshHost,
|
|
4
|
+
installedBinaryPath,
|
|
5
|
+
stageRemoteDaemonRelease,
|
|
6
|
+
} from "./installer.mjs";
|
|
7
|
+
import { validateDaemonRelease } from "./release.mjs";
|
|
8
|
+
import { nativeHandoverCommand } from "./handover.mjs";
|
|
9
|
+
const fail = (code) => {
|
|
10
|
+
throw new Error(code);
|
|
11
|
+
};
|
|
12
|
+
// One explicit administrator installation grants only the fixed native
|
|
13
|
+
// coordinator/schedule. Persisted operator opt-in remains a separate drawer act.
|
|
14
|
+
export async function installAutomaticDaemonUpdater(spec, {
|
|
15
|
+
validateRelease = validateDaemonRelease,
|
|
16
|
+
resolveAuth = resolveCallAuth,
|
|
17
|
+
identity = callToolJson,
|
|
18
|
+
stage = stageRemoteDaemonRelease,
|
|
19
|
+
native = nativeHandoverCommand,
|
|
20
|
+
} = {}) {
|
|
21
|
+
if (
|
|
22
|
+
!spec || typeof spec !== "object" || Array.isArray(spec) ||
|
|
23
|
+
Object.keys(spec).some((key) =>
|
|
24
|
+
![
|
|
25
|
+
"platform",
|
|
26
|
+
"arch",
|
|
27
|
+
"sshHost",
|
|
28
|
+
"serviceUser",
|
|
29
|
+
"releaseRoot",
|
|
30
|
+
"expectedHostKey",
|
|
31
|
+
"tenant",
|
|
32
|
+
].includes(key)
|
|
33
|
+
) || !["linux", "darwin"].includes(spec.platform) ||
|
|
34
|
+
typeof spec.serviceUser !== "string" || spec.serviceUser === "root" ||
|
|
35
|
+
!/^[a-z_][a-z0-9_-]*$/i.test(spec.serviceUser) ||
|
|
36
|
+
!/^bbh-[0-9a-f]{24}$/.test(spec.expectedHostKey) ||
|
|
37
|
+
typeof spec.tenant !== "string" || !spec.tenant
|
|
38
|
+
) fail("daemon_automatic_install_spec_invalid");
|
|
39
|
+
assertSshHost(spec.sshHost);
|
|
40
|
+
const release = await validateRelease({ releaseRoot: spec.releaseRoot }, {
|
|
41
|
+
platform: spec.platform,
|
|
42
|
+
arch: spec.arch,
|
|
43
|
+
});
|
|
44
|
+
const resolved = await resolveAuth({
|
|
45
|
+
toolName: "daemon_handover",
|
|
46
|
+
preferSession: false,
|
|
47
|
+
});
|
|
48
|
+
if (resolved?.error || !resolved?.auth) {
|
|
49
|
+
fail("daemon_automatic_install_auth_required");
|
|
50
|
+
}
|
|
51
|
+
const who = await identity("whoami", {}, {
|
|
52
|
+
auth: resolved.auth,
|
|
53
|
+
tenant: spec.tenant,
|
|
54
|
+
});
|
|
55
|
+
if (
|
|
56
|
+
!who?.ok || who.isError || typeof who.data?.owner_email !== "string" ||
|
|
57
|
+
!who.data.owner_email.includes("@") || who.data.tenant_id !== spec.tenant
|
|
58
|
+
) fail("daemon_automatic_install_owner_unavailable");
|
|
59
|
+
const staged = await stage({
|
|
60
|
+
platform: spec.platform,
|
|
61
|
+
host: spec.sshHost,
|
|
62
|
+
user: spec.serviceUser,
|
|
63
|
+
release,
|
|
64
|
+
});
|
|
65
|
+
const binary = installedBinaryPath(release);
|
|
66
|
+
if (staged?.binary !== binary) fail("daemon_automatic_install_stage_invalid");
|
|
67
|
+
const result = await native({
|
|
68
|
+
host: spec.sshHost,
|
|
69
|
+
binary,
|
|
70
|
+
mode: "automatic-install",
|
|
71
|
+
input: {
|
|
72
|
+
host_key: spec.expectedHostKey,
|
|
73
|
+
owner_email: who.data.owner_email,
|
|
74
|
+
tenant_id: who.data.tenant_id,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
if (result?.installed !== true || result.protocol !== 2) {
|
|
78
|
+
fail("daemon_automatic_install_receipt_invalid");
|
|
79
|
+
}
|
|
80
|
+
return { installed: true, protocol: 2, host_key: spec.expectedHostKey };
|
|
81
|
+
}
|
package/src/daemon/commands.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { registerContainerHost } from "../api.mjs";
|
|
8
|
+
import { installAutomaticDaemonUpdater } from "./automatic.mjs";
|
|
8
9
|
import { upgradeDaemonHost } from "./handover.mjs";
|
|
9
10
|
import { awaitHostApproval } from "./approval-wait.mjs";
|
|
10
11
|
import { bootstrapFreshDaemon } from "./bootstrap.mjs";
|
|
@@ -49,6 +50,7 @@ export const daemonHelp = `Usage:
|
|
|
49
50
|
bb daemon config-render '<json host policy>'
|
|
50
51
|
bb daemon enroll '<json host-bound enrollment spec>'
|
|
51
52
|
bb daemon bootstrap '<json fresh-host bootstrap spec>'
|
|
53
|
+
bb daemon updater-install '<json automatic updater installation spec>'
|
|
52
54
|
bb daemon upgrade '<json controlled host upgrade spec>'
|
|
53
55
|
|
|
54
56
|
The Go payload is verified from the extracted universal package before sudo.
|
|
@@ -293,6 +295,7 @@ export async function cmdDaemon(args, {
|
|
|
293
295
|
verifyRemoteHelperIdentity = verifyRemoteHelperIdentityOnTarget,
|
|
294
296
|
bootstrap = bootstrapFreshDaemon,
|
|
295
297
|
upgrade = upgradeDaemonHost,
|
|
298
|
+
installAutomaticUpdater = installAutomaticDaemonUpdater,
|
|
296
299
|
registerHost = registerContainerHost,
|
|
297
300
|
createBootstrapIdentity = createRemoteBootstrapIdentity,
|
|
298
301
|
inspectBootstrapIdentity = inspectRemoteBootstrapIdentity,
|
|
@@ -305,6 +308,11 @@ export async function cmdDaemon(args, {
|
|
|
305
308
|
log(daemonHelp);
|
|
306
309
|
return 0;
|
|
307
310
|
}
|
|
311
|
+
if(args[0]==="updater-install"){
|
|
312
|
+
if(args.length!==2)fail("Usage: bb daemon updater-install '<json automatic updater installation spec>'");
|
|
313
|
+
let spec;try{spec=JSON.parse(args[1]);}catch{fail("daemon_automatic_install_spec_invalid");}
|
|
314
|
+
log(JSON.stringify(await installAutomaticUpdater(spec)));return 0;
|
|
315
|
+
}
|
|
308
316
|
if(args[0]==="upgrade"){
|
|
309
317
|
if(args.length!==2)fail("Usage: bb daemon upgrade '<json controlled host upgrade spec>'");
|
|
310
318
|
let spec;try{spec=JSON.parse(args[1]);}catch{fail("daemon_handover_spec_invalid");}
|
package/src/daemon/handover.mjs
CHANGED
|
@@ -27,9 +27,9 @@ export function nativeHandoverCommand(
|
|
|
27
27
|
{ spawnImpl = spawn } = {},
|
|
28
28
|
) {
|
|
29
29
|
if (
|
|
30
|
-
!immutableBinaryRE.test(binary) || !["inspect", "submit"].includes(mode)
|
|
30
|
+
!immutableBinaryRE.test(binary) || !["inspect", "submit", "automatic-install"].includes(mode)
|
|
31
31
|
) fail("daemon_handover_command_invalid");
|
|
32
|
-
const flag = mode === "inspect"
|
|
32
|
+
const flag = mode === "automatic-install" ? "--native-automatic-update-install" : mode === "inspect"
|
|
33
33
|
? "--native-handover-inspect"
|
|
34
34
|
: "--native-handover-submit";
|
|
35
35
|
let command = "sudo", args = ["-n", binary, flag];
|
package/src/pw/staging-auth.mjs
CHANGED
|
@@ -263,6 +263,35 @@ export function refuse({ stdout, stderr }, code, exitCode = 3, extra = {}, vars
|
|
|
263
263
|
async function loadPool() { return import("../staging-pool/cli.mjs"); }
|
|
264
264
|
async function loadBinding() { return import("../staging-pool/binding.mjs"); }
|
|
265
265
|
|
|
266
|
+
// BOT-2894: pool_empty is an agent-visible symptom, but recovery is owned by
|
|
267
|
+
// the control plane. This fixed call sends only the registered tenant/pool and
|
|
268
|
+
// persona; no host, path, command, credential or retry policy can cross it.
|
|
269
|
+
async function requestPoolRefill({ ctxPool, persona, deps, env }) {
|
|
270
|
+
try {
|
|
271
|
+
if (deps.requestPoolRefill) return await deps.requestPoolRefill({
|
|
272
|
+
tenant: ctxPool.tenant, poolId: ctxPool.poolId, persona,
|
|
273
|
+
});
|
|
274
|
+
const bin = ctxPool.installed?.botbuddyBin;
|
|
275
|
+
if (!bin || !ctxPool.tenant) return { ok: false, code: "POOL_REFILL_UNAVAILABLE" };
|
|
276
|
+
const run = deps.poolRefillExec ?? execFileSync;
|
|
277
|
+
const stdout = String(run(bin, ["call", "request_staging_pool_refill", "--pin", ctxPool.tenant, "--json", JSON.stringify({ pool_id: ctxPool.poolId, persona })], {
|
|
278
|
+
encoding: "utf8", timeout: 10_000, env, stdio: ["ignore", "pipe", "ignore"],
|
|
279
|
+
}));
|
|
280
|
+
const parsed = JSON.parse(stdout);
|
|
281
|
+
return {
|
|
282
|
+
ok: parsed?.ok === true,
|
|
283
|
+
...(typeof parsed?.job_id === "string" ? { job_id: parsed.job_id } : {}),
|
|
284
|
+
...(typeof parsed?.status === "string" ? { status: parsed.status } : {}),
|
|
285
|
+
...(typeof parsed?.code === "string" ? { code: parsed.code } : {}),
|
|
286
|
+
};
|
|
287
|
+
} catch {
|
|
288
|
+
// Recovery request failure does not turn an already truthful pool_empty
|
|
289
|
+
// into a hidden success. The one wait/retry remains bounded and reports its
|
|
290
|
+
// original symptom; the next attempt will request recovery again.
|
|
291
|
+
return { ok: false, code: "POOL_REFILL_UNAVAILABLE" };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
266
295
|
/**
|
|
267
296
|
* `auth --persona <alias>` after the wrapper has proven: environment=staging, a
|
|
268
297
|
* lock-gated lane held by this exact agent, and a staging (or untracked) browser
|
|
@@ -316,13 +345,23 @@ export async function runStagingAuth({ plan, host, env, deps, socketRun, resolve
|
|
|
316
345
|
claim = await poolModule.takeForLane({ persona: persona.email, owner: `lane-${plan.lane}@${host}`, poolId: ctxPool.poolId, tenantHint, ...poolOptions });
|
|
317
346
|
} catch (error) {
|
|
318
347
|
if (error?.code === "pool_empty") {
|
|
348
|
+
const refill = await requestPoolRefill({ ctxPool, persona: persona.email, deps, env });
|
|
319
349
|
// The refusal's code is the pool's health reason when it is unhealthy (e.g. a
|
|
320
350
|
// count-0 persona is persona_not_configured), so code, message and help agree.
|
|
321
351
|
const reasonCode = typeof error.reason === "string" && error.reason !== "healthy" && isKnownCode(error.reason) ? error.reason : "pool_empty";
|
|
352
|
+
// A terminal refill receipt is a real, durable recovery boundary. Keep
|
|
353
|
+
// the old bounded timer only when the request could not return a job id.
|
|
354
|
+
const refillNext = refill.ok === true && typeof refill.job_id === "string"
|
|
355
|
+
? `bb wait 'staging-pool-refill:id=${refill.job_id}' && ${vars.retry ?? authLine} # once; if it fails, report this code`
|
|
356
|
+
: undefined;
|
|
322
357
|
// BOT-2836: a healthy empty pool is the agent's own wait-and-retry, so it is
|
|
323
358
|
// rendered here with this lane; an unhealthy pool keeps the pool's health guidance.
|
|
324
359
|
const forwarded = reasonCode === "pool_empty" ? ["reason", "detail"] : ["reason", "detail", ...GUIDED_FIELDS];
|
|
325
|
-
return refuse(io, reasonCode, 3,
|
|
360
|
+
return refuse(io, reasonCode, 3, {
|
|
361
|
+
...Object.fromEntries(forwarded.filter((key) => error[key] !== undefined).map((key) => [key, error[key]])),
|
|
362
|
+
...(refillNext ? { next: refillNext } : {}),
|
|
363
|
+
automatic_refill: refill.ok === true ? { requested: true, ...(refill.job_id ? { job_id: refill.job_id } : {}), ...(refill.status ? { status: refill.status } : {}) } : { requested: false, code: refill.code },
|
|
364
|
+
}, vars);
|
|
326
365
|
}
|
|
327
366
|
const code = typeof error?.code === "string" && isKnownCode(error.code) ? error.code : "pool_take_failed";
|
|
328
367
|
return refuse(io, code, 3, { detail: String(error?.message ?? error).slice(0, 300) }, vars);
|
package/src/staging-pool/cli.mjs
CHANGED
|
@@ -514,6 +514,7 @@ export async function runStagingPool(argv, deps = {}) {
|
|
|
514
514
|
rawBinding,
|
|
515
515
|
poolBinding,
|
|
516
516
|
botbuddyBin,
|
|
517
|
+
nodeBin,
|
|
517
518
|
intervalSeconds: explicitInterval ?? existing?.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS,
|
|
518
519
|
existingPersonas: existing?.personas,
|
|
519
520
|
tenant,
|
|
@@ -37,7 +37,7 @@ export function poolHomePaths({ home = homedir(), poolId, root } = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// The installed config: { version, binding: <raw .botbuddy-agent.json subset>,
|
|
40
|
-
// personas: { email: count }, botbuddyBin, intervalSeconds }. Returns it with the
|
|
40
|
+
// personas: { email: count }, botbuddyBin, nodeBin, intervalSeconds }. Returns it with the
|
|
41
41
|
// parsed binding attached as `poolBinding`.
|
|
42
42
|
export function validateInstalledConfig(config) {
|
|
43
43
|
if (!config || typeof config !== "object") throw new ConfigError("Pool config is not an object.", "config_invalid");
|
|
@@ -52,6 +52,11 @@ export function validateInstalledConfig(config) {
|
|
|
52
52
|
if (typeof config.botbuddyBin !== "string" || !config.botbuddyBin.startsWith("/")) {
|
|
53
53
|
throw new ConfigError("Pool config botbuddyBin must be an absolute path (re-run install).", "config_invalid");
|
|
54
54
|
}
|
|
55
|
+
// `nodeBin` is added by BOT-2894. Older human-installed pools remain valid,
|
|
56
|
+
// but an autonomous Helper may only advertise a pool that pins its runtime.
|
|
57
|
+
if (config.nodeBin !== undefined && (typeof config.nodeBin !== "string" || !config.nodeBin.startsWith("/"))) {
|
|
58
|
+
throw new ConfigError("Pool config nodeBin must be an absolute path (re-run install).", "config_invalid");
|
|
59
|
+
}
|
|
55
60
|
if (!Number.isInteger(config.intervalSeconds) || config.intervalSeconds < 60 || config.intervalSeconds > 3600) {
|
|
56
61
|
throw new ConfigError("Pool config intervalSeconds must be an integer 60-3600.", "config_invalid");
|
|
57
62
|
}
|
|
@@ -87,7 +92,7 @@ export function readInstalledConfig(path, { readFile = readFileSync } = {}) {
|
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
// rawBinding: { tenant, staging_session_pool } exactly as committed.
|
|
90
|
-
export function buildInstalledConfig({ rawBinding, poolBinding, botbuddyBin, intervalSeconds = DEFAULT_INTERVAL_SECONDS, existingPersonas, tenant, registry, installedAt }) {
|
|
95
|
+
export function buildInstalledConfig({ rawBinding, poolBinding, botbuddyBin, nodeBin, intervalSeconds = DEFAULT_INTERVAL_SECONDS, existingPersonas, tenant, registry, installedAt }) {
|
|
91
96
|
const personas = {};
|
|
92
97
|
for (const persona of Object.values(poolBinding.personas)) {
|
|
93
98
|
personas[persona.email] = Number.isInteger(existingPersonas?.[persona.email]) ? existingPersonas[persona.email] : persona.defaultCount;
|
|
@@ -97,6 +102,7 @@ export function buildInstalledConfig({ rawBinding, poolBinding, botbuddyBin, int
|
|
|
97
102
|
binding: { tenant: rawBinding.tenant, staging_session_pool: rawBinding.staging_session_pool },
|
|
98
103
|
personas,
|
|
99
104
|
botbuddyBin,
|
|
105
|
+
...(nodeBin ? { nodeBin } : {}),
|
|
100
106
|
intervalSeconds,
|
|
101
107
|
// BOT-2648: installed from the tenant's control-plane registration.
|
|
102
108
|
...(tenant ? { tenant, poolId: poolBinding.poolId, source: "control-plane" } : {}),
|
|
@@ -91,7 +91,7 @@ export async function refreshExistingRuntime({ options, home, uid, env, exec, bu
|
|
|
91
91
|
const contentDigest = hash.digest("hex");
|
|
92
92
|
const runtimeName = `runtime-${metadata.version}-${contentDigest}`;
|
|
93
93
|
const runtime = join(root, runtimeName);
|
|
94
|
-
const config = validateInstalledConfig({ ...installed, binding: rawBinding, personas, source: "control-plane", tenant, poolId, registry: { commit_sha: row.commit_sha ?? null, updated_at: row.updated_at ?? null }, runtime: { version: metadata.version, sha256: contentDigest } });
|
|
94
|
+
const config = validateInstalledConfig({ ...installed, binding: rawBinding, personas, nodeBin: installed.nodeBin ?? await fs.realpath(process.execPath), source: "control-plane", tenant, poolId, registry: { commit_sha: row.commit_sha ?? null, updated_at: row.updated_at ?? null }, runtime: { version: metadata.version, sha256: contentDigest } });
|
|
95
95
|
const token = randomUUID();
|
|
96
96
|
const lockPath = join(root, ".runtime-refresh.lock");
|
|
97
97
|
const lock = await fs.open(lockPath, "wx", 0o600);
|
package/src/wait-core.mjs
CHANGED
|
@@ -216,6 +216,15 @@ const VALIDATORS = {
|
|
|
216
216
|
}
|
|
217
217
|
return { runId };
|
|
218
218
|
},
|
|
219
|
+
"staging-pool-refill": (p, raw) => {
|
|
220
|
+
const pair = /^\s*id\s*=\s*([0-9A-Fa-f-]+)\s*$/.exec(raw ?? "");
|
|
221
|
+
if (!pair || Object.keys(p).length !== 1 || p.id !== pair[1]) {
|
|
222
|
+
throw new Error("staging-pool-refill needs exactly one id=<refill job UUID>");
|
|
223
|
+
}
|
|
224
|
+
const jobId = pair[1].toLowerCase();
|
|
225
|
+
if (!CANONICAL_UUID_RE.test(jobId)) throw new Error("staging-pool-refill id must be a canonical UUID");
|
|
226
|
+
return { jobId };
|
|
227
|
+
},
|
|
219
228
|
// BOT-2629: this is deliberately a dedicated condition rather than a raw
|
|
220
229
|
// event escape hatch. The UUID is chosen before dispatch and the exact subject
|
|
221
230
|
// prevents another run on the same idle/re-adopted lease waking this waiter.
|
|
@@ -1295,6 +1304,11 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
|
|
|
1295
1304
|
signal.subject_key === `run:${params.runId}` &&
|
|
1296
1305
|
signal.payload?.run_id === params.runId &&
|
|
1297
1306
|
COMMAND_RUN_TERMINAL_STATUSES.has(signal.payload?.status);
|
|
1307
|
+
case "staging-pool-refill":
|
|
1308
|
+
return signal.signal_type === "staging_pool_refill" &&
|
|
1309
|
+
signal.subject_key === `staging-pool-refill:${params.jobId}` &&
|
|
1310
|
+
signal.payload?.job_id === params.jobId &&
|
|
1311
|
+
["completed", "failed"].includes(signal.payload?.status);
|
|
1298
1312
|
case "stack-run":
|
|
1299
1313
|
return signal.signal_type === "stack_run" && signal.subject_key === `stack-run:${params.id}` &&
|
|
1300
1314
|
signal.payload?.run_id === params.id && Number.isInteger(signal.payload?.exit_code);
|
package/src/wait.mjs
CHANGED
|
@@ -165,6 +165,10 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
|
|
|
165
165
|
condition, and keep this wait's result pending: its
|
|
166
166
|
return is what resumes your task. A run whose worker is
|
|
167
167
|
lost ends owner_lost (bb run inspect <run_id>).
|
|
168
|
+
staging-pool-refill:id=<job UUID> BOT-2894: a requested staging-pool refill reaching
|
|
169
|
+
completed or failed. This is the only condition of its
|
|
170
|
+
wait and is available to each tenant agent that requested
|
|
171
|
+
the deduped job; its terminal receipt is safe to display.
|
|
168
172
|
stack-run:id=<run UUID> one named bb stack run reaching its terminal outcome
|
|
169
173
|
(BOT-2629). Choose the UUID BEFORE dispatch and pass it as
|
|
170
174
|
bb stack run --run-id <UUID>; the receipt carries exit_code,
|
|
@@ -648,6 +652,8 @@ async function registerWait(opts, conditions, deadlineIso) {
|
|
|
648
652
|
// back to an untracked wait would only hide that error behind a timeout.
|
|
649
653
|
"command_run_unavailable",
|
|
650
654
|
"command_run_single_condition_required",
|
|
655
|
+
"staging_pool_refill_unavailable",
|
|
656
|
+
"staging_pool_refill_single_condition_required",
|
|
651
657
|
]);
|
|
652
658
|
if (INVALID_CONDITION_CODES.has(body.error)) {
|
|
653
659
|
const err = new Error(body.detail || body.error);
|