@botbuddy/cli 1.63.27 → 1.63.29

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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.27",
3
- "source_commit": "7b8d56823a4e1d2935a9c436de5cc8738f333dfa",
4
- "source_sha256": "69351ba7fad33c5ce76d7bd76ed782988b44eccec4809bb93b0d2d26d109d641",
2
+ "release_version": "1.63.29",
3
+ "source_commit": "732c555dd44141d581f96959ed590007b3ec09de",
4
+ "source_sha256": "c2f0fc90169b4580f2d8918fb1100b853e0663a65fe470dc399c2a33787d7639",
5
5
  "target": "darwin-arm64",
6
- "sha256": "9b000a9b8103c438f0f81859538db21844375a5219ddf423e7266e4d9c33dbca"
6
+ "sha256": "05fd954131e610edd04ff1ee4ee4a0e37047f3ab56bbb0d4fd5fc3cee82bcd86"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.27",
3
- "source_commit": "7b8d56823a4e1d2935a9c436de5cc8738f333dfa",
4
- "source_sha256": "69351ba7fad33c5ce76d7bd76ed782988b44eccec4809bb93b0d2d26d109d641",
2
+ "release_version": "1.63.29",
3
+ "source_commit": "732c555dd44141d581f96959ed590007b3ec09de",
4
+ "source_sha256": "c2f0fc90169b4580f2d8918fb1100b853e0663a65fe470dc399c2a33787d7639",
5
5
  "target": "linux-x64",
6
- "sha256": "ed2e4faa813d9c0905703f790a3744d4250d3a85add6dd5ce7efb6498e6a76ad"
6
+ "sha256": "1b3d42884f57dd70d3946137b704009d42ee027e2ca060b2e4df70325c52c5b7"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.63.27",
4
- "source_commit": "7b8d56823a4e1d2935a9c436de5cc8738f333dfa",
5
- "source_sha256": "69351ba7fad33c5ce76d7bd76ed782988b44eccec4809bb93b0d2d26d109d641",
3
+ "release_version": "1.63.29",
4
+ "source_commit": "732c555dd44141d581f96959ed590007b3ec09de",
5
+ "source_sha256": "c2f0fc90169b4580f2d8918fb1100b853e0663a65fe470dc399c2a33787d7639",
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": "9b000a9b8103c438f0f81859538db21844375a5219ddf423e7266e4d9c33dbca",
11
+ "sha256": "05fd954131e610edd04ff1ee4ee4a0e37047f3ab56bbb0d4fd5fc3cee82bcd86",
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": "ed2e4faa813d9c0905703f790a3744d4250d3a85add6dd5ce7efb6498e6a76ad",
18
+ "sha256": "1b3d42884f57dd70d3946137b704009d42ee027e2ca060b2e4df70325c52c5b7",
19
19
  "mode": "0755"
20
20
  }
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.63.27",
3
+ "version": "1.63.29",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,8 @@ const version=/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
19
19
  const fields=['protocol','binary_path','artifact_sha256','config_path','service_uid','runtime_version','runtime_git_sha','runtime_target','host_key','machine_id','engine_id','lease_id','job_id','attempt','tenant_id','project_id','callback_port','producer_pid','producer_start_ticks','producer_boot_id'];
20
20
  const fail=(code,message)=>{throw Object.assign(Error(message),{code});};
21
21
  const quote=value=>"'"+String(value).replaceAll("'","'\\''")+"'";
22
+ // BOT-2828: a relay-reported cause is one fixed upper-case code, never free text.
23
+ const typedCause=value=>typeof value==='string'&&/^CALLBACK_[A-Z_]{1,64}$/.test(value)?value:null;
22
24
  const canonicalMachine=value=>typeof value==='string'&&/^(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.test(value)?value.replaceAll('-','').toLowerCase():null;
23
25
 
24
26
  export function validatedCallbackDescriptor(value){
@@ -86,10 +88,17 @@ export async function openNativeCallbackTransport({current,leaseId,hostKey,callb
86
88
  const command=`set -eu; relayPid=; cleanup(){ trap - EXIT HUP INT TERM; if [ -n "$relayPid" ]; then kill -TERM "$relayPid" 2>/dev/null || :; wait "$relayPid" 2>/dev/null || :; fi; rm -f -- ${quote(socket)}; rmdir -- ${quote(directory)}; }; trap cleanup EXIT HUP INT TERM; ${quote(d.binary_path)} --callback-relay --config ${quote(d.config_path)} <&0 & relayPid=$!; status=0; wait "$relayPid" || status=$?; relayPid=; exit "$status"`;
87
89
  child=spawnImpl(sshBin,[...SSH_FORWARD_OPTIONS.filter(arg=>arg!=='-N'),'-o','StreamLocalBindMask=0177','-R',socket+':127.0.0.1:'+gateway.port,'--',target,command],{env,stdio:['pipe','pipe','pipe']});
88
90
  child.stdin.on('error',()=>{});child.stderr.on('data',()=>{});
89
- let spawnError=false;child.once('error',()=>{spawnError=true});terminal=new Promise(resolve=>child.once('close',(code,signal)=>resolve({code,signal,reason:spawnError?'callback_process_error':'callback_process_exit'})));
90
- terminal.then(exit=>{if(!closed){reportExit(exit);void close().catch(()=>reportExit({code:null,reason:'callback_cleanup_uncertain'}))}});
91
+ let spawnError=false;child.once('error',()=>{spawnError=true});// BOT-2828: a relay that ends without a typed close frame is classified by its exit: ssh's own
92
+ // failure (255) or a signal is the SSH hop, any other exit is the relay process itself.
93
+ terminal=new Promise(resolve=>child.once('close',(exitCode,signal)=>resolve({reason:spawnError?'callback_process_error':'callback_process_exit',code:spawnError||exitCode===255||signal?'CALLBACK_SSH_LOST':'CALLBACK_RELAY_EXITED',exit_code:exitCode,signal})));
94
+ terminal.then(exit=>{if(!closed){reportExit(exit);void close().catch(()=>reportExit({reason:'callback_cleanup_uncertain',code:'CALLBACK_CLEANUP_UNCERTAIN'}))}});
91
95
  const lines=createInterface({input:child.stdout});
92
- lines.on('line',line=>{try{const event=JSON.parse(line);if(event.refused===true&&event.code==='CALLBACK_CONSUMING_NAMESPACE_UNREACHABLE'&&pendingAck){const waiter=pendingAck;pendingAck=undefined;const refusal=Error('consuming namespace callback probe failed');refusal.code='CALLBACK_CONSUMING_NAMESPACE_UNREACHABLE';waiter.reject(refusal);}else if(pendingReady){const waiter=pendingReady;pendingReady=undefined;waiter.resolve(event);}else if(pendingAck&&event.renewed===true&&event.route_generation===routeGeneration&&Number.isFinite(Date.parse(event.expires_at))){const waiter=pendingAck;pendingAck=undefined;waiter.resolve(event);}else throw Error('unexpected callback frame');}catch{reportExit({code:null,reason:'callback_protocol_error'});void close().catch(()=>{})}});
96
+ lines.on('line',line=>{try{const event=JSON.parse(line);
97
+ // BOT-2828: the relay names why it closed a live route ({closed:true,code}).
98
+ if(event?.closed===true&&typedCause(event.code)&&!pendingReady){if(!closed){reportExit({reason:'callback_relay_closed',code:event.code});void close().catch(()=>{})}return;}
99
+ // BOT-2790 AC-5: a typed readiness (or first-renewal) refusal keeps its exact code.
100
+ if(event?.refused===true&&(pendingReady||pendingAck)){const waiter=pendingReady??pendingAck;pendingReady=pendingAck=undefined;const refusal=Error('native callback relay refused readiness');refusal.code=typedCause(event.code)??'CALLBACK_NOT_READY';waiter.reject(refusal);return;}
101
+ if(pendingReady){const waiter=pendingReady;pendingReady=undefined;waiter.resolve(event);}else if(pendingAck&&event.renewed===true&&event.route_generation===routeGeneration&&Number.isFinite(Date.parse(event.expires_at))){const waiter=pendingAck;pendingAck=undefined;waiter.resolve(event);}else throw Error('unexpected callback frame');}catch{if(!closed){reportExit({reason:'callback_protocol_error',code:'CALLBACK_PROTOCOL_ERROR'});void close().catch(()=>{})}}});
93
102
  terminal.then(()=>{lines.close();pendingReady?.reject(Error('callback process ended'));pendingAck?.reject(Error('callback process ended'));});
94
103
  const ready=await new Promise((resolve,reject)=>{
95
104
  const timeout=setTimeout(()=>reject(Object.assign(Error('callback readiness deadline'),{code:'CALLBACK_NOT_READY'})),20_000);timeout.unref();
@@ -97,7 +106,7 @@ export async function openNativeCallbackTransport({current,leaseId,hostKey,callb
97
106
  child.stdin.write(JSON.stringify({binding,capability,socket_path:socket,readiness_nonce:readinessNonce})+'\n');
98
107
  });
99
108
  const callbackUrl=callbackReadiness(ready,callbackPort);
100
- gateway.onClose(()=>{if(!closed){reportExit({code:null,reason:'callback_gateway_closed'});void close().catch(()=>{})}});
109
+ gateway.onClose(()=>{if(!closed){reportExit({reason:'callback_gateway_closed',code:'CALLBACK_GATEWAY_EXPIRED'});void close().catch(()=>{})}});
101
110
  let renewedAt=null;
102
111
  const refresh=async(first=false)=>{
103
112
  if(closed||refreshing)return;refreshing=true;
@@ -108,15 +117,16 @@ export async function openNativeCallbackTransport({current,leaseId,hostKey,callb
108
117
  // A failed status READ is not an authorization answer: keep the live generation (relay and
109
118
  // gateway still expire it at 30 s) and retry on the next tick while inside the grace window.
110
119
  if(!first&&error?.transient===true&&renewedAt!==null&&performance.now()-renewedAt<transientGraceMs){const retry=setTimeout(()=>{void refresh()},transientRetryMs);retry.unref();return;}
120
+ if(error?.transient===true&&!error.code&&!first)error.code='CALLBACK_STATUS_UNREADABLE';
111
121
  throw error;
112
122
  }
113
123
  if(closed)return;const renewed=currentCallbackBinding(status,options);if(JSON.stringify(renewed)!==snapshot)fail('CALLBACK_AUTHORIZATION_LOST','callback context changed');
114
124
  const sentAt=performance.now();
115
- await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(Error('callback renewal acknowledgement deadline')),first?firstAckMs:renewAckMs);timeout.unref();pendingAck={resolve:value=>{clearTimeout(timeout);resolve(value)},reject:error=>{clearTimeout(timeout);reject(error)}};child.stdin.write(JSON.stringify({binding:renewed,renew:true})+'\n');});
125
+ await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(Object.assign(Error('callback renewal acknowledgement deadline'),first?{}:{code:'CALLBACK_RENEWAL_ACK_TIMEOUT',transport:true})),first?firstAckMs:renewAckMs);timeout.unref();pendingAck={resolve:value=>{clearTimeout(timeout);resolve(value)},reject:error=>{clearTimeout(timeout);reject(error)}};child.stdin.write(JSON.stringify({binding:renewed,renew:true})+'\n');});
116
126
  if(closed)return;renewedAt=sentAt;gateway.refresh(Object.freeze({snapshot}),30_000-(performance.now()-sentAt));
117
127
  }
118
128
  // The first renewal carries the consuming-namespace probe: its uncoded failure is readiness.
119
- catch(error){if(!closed){reportExit({code:null,reason:'callback_authorization_lost',...(error.code?{code:error.code}:first?{code:'CALLBACK_NOT_READY'}:{})});void close().catch(()=>{});}}
129
+ catch(error){if(!closed){reportExit({reason:error?.transport===true?'callback_renewal_unacknowledged':'callback_authorization_lost',code:error?.code??(first?'CALLBACK_NOT_READY':'CALLBACK_AUTHORIZATION_LOST')});void close().catch(()=>{});}}
120
130
  finally{refreshing=false}
121
131
  };
122
132
  await refresh(true);if(closed||!gateway.isLive())fail(exitResult?.code??'CALLBACK_NOT_READY','callback route not ready after readiness');
package/src/stack.mjs CHANGED
@@ -351,6 +351,8 @@ ${bold("REMOTE LEASES")} (BOT-2142, stack run)
351
351
  5 HOST_NO_SSH_TARGET the (pinned) remote host has no ssh_target — operator: set container_hosts.ssh_target
352
352
  5 HOST_HELPER_OUTDATED the (pinned) remote host's Helper predates protocol 4 — upgrade that Helper
353
353
  5 NO_HELPER_HOST no co-located host admits and no owned host qualifies as remote
354
+ 5 NO_COLOCATED_HELPER_HOST --local-only (or stack up / --local-exec): this machine has no active Docker
355
+ host (none enrolled, or draining/maintenance/retired) — run without --local-only
354
356
  4 STACK_EXECUTION_MODE_MISMATCH the slot already holds a lease in another mode/host — stack done it first
355
357
  (the four codes above create NO lease)
356
358
  5 BUNDLE_UPLOAD_FAILED the signed bundle upload failed (the lease is released first)
@@ -362,9 +364,16 @@ ${bold("REMOTE LEASES")} (BOT-2142, stack run)
362
364
  4 REQUESTER_ORIGIN_EXPLICIT_CONFLICT an unpinned stable --callback-port run in a repo whose
363
365
  supabase/config.toml sets its own [auth] external_url (no lease requested)
364
366
  5 CALLBACK_NOT_READY / CALLBACK_CONSUMING_NAMESPACE_UNREACHABLE the native route did not
365
- answer from the stack's own network before the child (lease reaped)
367
+ answer from the stack's own network before the child (lease reaped);
368
+ a typed relay refusal keeps its own code (BOT-2790): CALLBACK_ALIAS_DRIFT,
369
+ CALLBACK_PRODUCER_MISMATCH, CALLBACK_BIND_REFUSED, CALLBACK_ENDPOINT_REFUSED, …
366
370
  6 CALLBACK_TRANSPORT_LOST / CALLBACK_AUTHORIZATION_LOST the native route or its current
367
- authorization closed; the child is stopped and the lease reaped
371
+ authorization closed; the child is stopped and the lease reaped.
372
+ callback_cause names why (BOT-2828): relay CALLBACK_RUNTIME_DRIFT,
373
+ CALLBACK_VALIDATION_UNAVAILABLE (Docker unanswered 20 s), CALLBACK_PRODUCER_ENDED,
374
+ CALLBACK_GENERATION_EXPIRED, CALLBACK_RENEWAL_REFUSED; requester CALLBACK_SSH_LOST,
375
+ CALLBACK_RELAY_EXITED, CALLBACK_GATEWAY_EXPIRED, CALLBACK_RENEWAL_ACK_TIMEOUT,
376
+ CALLBACK_STATUS_UNREADABLE, CALLBACK_PROTOCOL_ERROR, CALLBACK_AUTHORIZATION_LOST
368
377
  5 CALLBACK_PORT_BUSY --callback-port is already listening on the serving host (another run or
369
378
  service), the host refused this run's -R because another run took the
370
379
  port first, or another listener shares the port; this run holds no -R —
@@ -1363,6 +1372,13 @@ export const REMOTE_REFUSAL_GUIDANCE = Object.freeze({
1363
1372
  NO_HELPER_HOST: "no host can serve this lease: no co-located Helper admits it and no other host you own qualifies as remote " +
1364
1373
  "(an owned, device-consented, beacon-fresh Helper on protocol 4 with an operator-set ssh_target). No lease was created.",
1365
1374
  HOST_NOT_OWNED: "the pinned host is not a device-consented Helper host your owner owns. No lease was created.",
1375
+ // BOT-2845: a co-located request (`stack up`, `--local-exec`, `stack run --local-only`) from a
1376
+ // machine that no longer hosts Docker. The server considers only ACTIVE, device-consented,
1377
+ // owner-bound hosts for this hardware id and sends no message with this code.
1378
+ NO_COLOCATED_HELPER_HOST: "this machine has no active, device-consented Docker host your owner owns (none is enrolled for " +
1379
+ "this hardware id, or its host is draining, in maintenance or retired), so a co-located lease (`stack up`, `--local-exec`, " +
1380
+ "`stack run --local-only`) cannot be placed here. No lease was created. Run `bb stack run` without --local-only " +
1381
+ "(execution auto) or with --remote, so another host you own serves the stack.",
1366
1382
  HOST_STALE: "the pinned remote host's Helper beacon is stale. No lease was created; retry once it beacons, or drop --host.",
1367
1383
  HOST_NOT_REMOTE: "--remote pinned this machine's own host; drop --host or --remote. No lease was created.",
1368
1384
  BUNDLE_REQUIRED: "the server picked a remote host but this request carried no bundle digest. No lease was created; " +
@@ -4826,6 +4842,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4826
4842
  const transient = !observed?.auth && (observed?.status == null || observed.status >= 500 || observed.status === 408 || observed.status === 429 || observed.error === "invalid_json");
4827
4843
  throw Object.assign(new Error(transient ? "current callback status unreadable" : "current callback authorization refused"), transient ? { transient: true } : { code: "CALLBACK_AUTHORIZATION_LOST" });
4828
4844
  }
4845
+ // BOT-2790 AC-6 (BOT-2828): a backend RPC_ERROR (e.g. a statement timeout) is an unanswered read.
4846
+ if (observed.data?.success !== true && observed.data?.code === "RPC_ERROR") throw Object.assign(new Error("current callback status unreadable (RPC_ERROR)"), { transient: true });
4829
4847
  if (observed.data?.success !== true) throw Object.assign(new Error("current callback authorization unavailable"), { code: "CALLBACK_AUTHORIZATION_LOST" });
4830
4848
  return observed.data; },
4831
4849
  });
@@ -4917,7 +4935,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4917
4935
  }
4918
4936
  if (callbackLost || nativeCallback && !nativeCallback.isLive()) {
4919
4937
  await closeTunnel(); const cleanup = await requestCleanup();
4920
- return { exitCode: cleanup.ok && !callbackCleanupError ? EXIT.LEASE_FAILED : EXIT.CLEANUP_FAILED, outcome: "error", code: "CALLBACK_TRANSPORT_LOST", leaseId, error: "callback transport closed before child start", cleanup };
4938
+ const callbackCause = callbackLost?.code ?? "CALLBACK_RELAY_EXITED";
4939
+ return { exitCode: cleanup.ok && !callbackCleanupError ? EXIT.LEASE_FAILED : EXIT.CLEANUP_FAILED, outcome: "error", code: "CALLBACK_TRANSPORT_LOST", callbackCause, leaseId, error: `callback transport closed before child start (${callbackCause})`, cleanup };
4921
4940
  }
4922
4941
  if (tunnelLost) {
4923
4942
  // The forwards died between being verified and the child starting.
@@ -4984,7 +5003,9 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4984
5003
  error: "the requester origin closed while the child was running, so the child was stopped and the lease released" };
4985
5004
  }
4986
5005
  if (callbackLost && !receivedSignal) {
4987
- return { exitCode: cleanup.ok && !callbackCleanupError ? EXIT.LEASE_FAILED : EXIT.CLEANUP_FAILED, outcome: "failed", code: callbackLost.reason === "callback_authorization_lost" ? "CALLBACK_AUTHORIZATION_LOST" : "CALLBACK_TRANSPORT_LOST", leaseId, childExitCode: childResult.code, cleanup, fenced: true, error: "callback transport authorization or runtime context was lost; child stopped" };
5006
+ // BOT-2828: the receipt names the hop and cause that closed the route, not only the class.
5007
+ const callbackCause = callbackLost.code ?? (callbackLost.reason === "callback_authorization_lost" ? "CALLBACK_AUTHORIZATION_LOST" : "CALLBACK_RELAY_EXITED");
5008
+ return { exitCode: cleanup.ok && !callbackCleanupError ? EXIT.LEASE_FAILED : EXIT.CLEANUP_FAILED, outcome: "failed", code: callbackLost.reason === "callback_authorization_lost" ? "CALLBACK_AUTHORIZATION_LOST" : "CALLBACK_TRANSPORT_LOST", callbackCause, leaseId, childExitCode: childResult.code, cleanup, fenced: true, error: `callback transport authorization or runtime context was lost (${callbackCause}); child stopped` };
4988
5009
  }
4989
5010
  if (tunnelLost && !receivedSignal) {
4990
5011
  return tunnelLostResult(tunnelLost, { leaseId, cleanup, childExitCode: childResult.code, fenced: fencing, ...(parkIncident ? { incident: parkIncident } : {}) });
@@ -5092,6 +5113,8 @@ export function runReceiptFields(result) {
5092
5113
  transport: result.execution?.transport ?? "none",
5093
5114
  // BOT-2775: present only when the callback was served over the native route.
5094
5115
  ...(result.execution?.callbackTransport ? { callback_transport: result.execution.callbackTransport } : {}),
5116
+ // BOT-2828: the typed cause of a native callback route close (e.g. CALLBACK_RUNTIME_DRIFT).
5117
+ ...(result.callbackCause ? { callback_cause: result.callbackCause } : {}),
5095
5118
  // BOT-2780: the reserved requester origin generated auth links name (never a credential).
5096
5119
  ...(result.execution?.requesterOrigin ? { requester_origin: result.execution.requesterOrigin } : {}),
5097
5120
  // BOT-2788: "previous" (an idle stack's origin re-reserved), "previous_port_busy" or "new".