@botbuddy/cli 1.63.26 → 1.63.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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.26",
3
- "source_commit": "832178e0e921663ead7288d2f629e8a1a06e9e4a",
4
- "source_sha256": "42fc35ac242296494ceacfe345030b4c1cf12cd63d240b1b6313065d73dfc1bb",
2
+ "release_version": "1.63.28",
3
+ "source_commit": "60e4aacb7efe75b27dd73e2481352be8f5a4e478",
4
+ "source_sha256": "d979ad49e64dd5af2d8ba6a993682466747eee6b9502eb647d3814ef860ce7c1",
5
5
  "target": "darwin-arm64",
6
- "sha256": "a011707d70ecd7aaea9dc73960bf2ec8e542efd570fb1f13ff4befb8d8c40e88"
6
+ "sha256": "1c9c4958b9c65847cdb856728c9f6f644e10094029ddd767ecbc8fae5194ed20"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.26",
3
- "source_commit": "832178e0e921663ead7288d2f629e8a1a06e9e4a",
4
- "source_sha256": "42fc35ac242296494ceacfe345030b4c1cf12cd63d240b1b6313065d73dfc1bb",
2
+ "release_version": "1.63.28",
3
+ "source_commit": "60e4aacb7efe75b27dd73e2481352be8f5a4e478",
4
+ "source_sha256": "d979ad49e64dd5af2d8ba6a993682466747eee6b9502eb647d3814ef860ce7c1",
5
5
  "target": "linux-x64",
6
- "sha256": "765fec331e8eca8dd14823603aeaa0ff780db7369489e286923d73d8447f2f88"
6
+ "sha256": "c6dccb7605296c3c7b12ec9598397a7ede559a0640a1a969a5da2c4149860e42"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.63.26",
4
- "source_commit": "832178e0e921663ead7288d2f629e8a1a06e9e4a",
5
- "source_sha256": "42fc35ac242296494ceacfe345030b4c1cf12cd63d240b1b6313065d73dfc1bb",
3
+ "release_version": "1.63.28",
4
+ "source_commit": "60e4aacb7efe75b27dd73e2481352be8f5a4e478",
5
+ "source_sha256": "d979ad49e64dd5af2d8ba6a993682466747eee6b9502eb647d3814ef860ce7c1",
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": "a011707d70ecd7aaea9dc73960bf2ec8e542efd570fb1f13ff4befb8d8c40e88",
11
+ "sha256": "1c9c4958b9c65847cdb856728c9f6f644e10094029ddd767ecbc8fae5194ed20",
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": "765fec331e8eca8dd14823603aeaa0ff780db7369489e286923d73d8447f2f88",
18
+ "sha256": "c6dccb7605296c3c7b12ec9598397a7ede559a0640a1a969a5da2c4149860e42",
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.26",
3
+ "version": "1.63.28",
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
@@ -362,9 +362,16 @@ ${bold("REMOTE LEASES")} (BOT-2142, stack run)
362
362
  4 REQUESTER_ORIGIN_EXPLICIT_CONFLICT an unpinned stable --callback-port run in a repo whose
363
363
  supabase/config.toml sets its own [auth] external_url (no lease requested)
364
364
  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)
365
+ answer from the stack's own network before the child (lease reaped);
366
+ a typed relay refusal keeps its own code (BOT-2790): CALLBACK_ALIAS_DRIFT,
367
+ CALLBACK_PRODUCER_MISMATCH, CALLBACK_BIND_REFUSED, CALLBACK_ENDPOINT_REFUSED, …
366
368
  6 CALLBACK_TRANSPORT_LOST / CALLBACK_AUTHORIZATION_LOST the native route or its current
367
- authorization closed; the child is stopped and the lease reaped
369
+ authorization closed; the child is stopped and the lease reaped.
370
+ callback_cause names why (BOT-2828): relay CALLBACK_RUNTIME_DRIFT,
371
+ CALLBACK_VALIDATION_UNAVAILABLE (Docker unanswered 20 s), CALLBACK_PRODUCER_ENDED,
372
+ CALLBACK_GENERATION_EXPIRED, CALLBACK_RENEWAL_REFUSED; requester CALLBACK_SSH_LOST,
373
+ CALLBACK_RELAY_EXITED, CALLBACK_GATEWAY_EXPIRED, CALLBACK_RENEWAL_ACK_TIMEOUT,
374
+ CALLBACK_STATUS_UNREADABLE, CALLBACK_PROTOCOL_ERROR, CALLBACK_AUTHORIZATION_LOST
368
375
  5 CALLBACK_PORT_BUSY --callback-port is already listening on the serving host (another run or
369
376
  service), the host refused this run's -R because another run took the
370
377
  port first, or another listener shares the port; this run holds no -R —
@@ -4826,6 +4833,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4826
4833
  const transient = !observed?.auth && (observed?.status == null || observed.status >= 500 || observed.status === 408 || observed.status === 429 || observed.error === "invalid_json");
4827
4834
  throw Object.assign(new Error(transient ? "current callback status unreadable" : "current callback authorization refused"), transient ? { transient: true } : { code: "CALLBACK_AUTHORIZATION_LOST" });
4828
4835
  }
4836
+ // BOT-2790 AC-6 (BOT-2828): a backend RPC_ERROR (e.g. a statement timeout) is an unanswered read.
4837
+ if (observed.data?.success !== true && observed.data?.code === "RPC_ERROR") throw Object.assign(new Error("current callback status unreadable (RPC_ERROR)"), { transient: true });
4829
4838
  if (observed.data?.success !== true) throw Object.assign(new Error("current callback authorization unavailable"), { code: "CALLBACK_AUTHORIZATION_LOST" });
4830
4839
  return observed.data; },
4831
4840
  });
@@ -4917,7 +4926,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4917
4926
  }
4918
4927
  if (callbackLost || nativeCallback && !nativeCallback.isLive()) {
4919
4928
  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 };
4929
+ const callbackCause = callbackLost?.code ?? "CALLBACK_RELAY_EXITED";
4930
+ 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
4931
  }
4922
4932
  if (tunnelLost) {
4923
4933
  // The forwards died between being verified and the child starting.
@@ -4984,7 +4994,9 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4984
4994
  error: "the requester origin closed while the child was running, so the child was stopped and the lease released" };
4985
4995
  }
4986
4996
  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" };
4997
+ // BOT-2828: the receipt names the hop and cause that closed the route, not only the class.
4998
+ const callbackCause = callbackLost.code ?? (callbackLost.reason === "callback_authorization_lost" ? "CALLBACK_AUTHORIZATION_LOST" : "CALLBACK_RELAY_EXITED");
4999
+ 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
5000
  }
4989
5001
  if (tunnelLost && !receivedSignal) {
4990
5002
  return tunnelLostResult(tunnelLost, { leaseId, cleanup, childExitCode: childResult.code, fenced: fencing, ...(parkIncident ? { incident: parkIncident } : {}) });
@@ -5092,6 +5104,8 @@ export function runReceiptFields(result) {
5092
5104
  transport: result.execution?.transport ?? "none",
5093
5105
  // BOT-2775: present only when the callback was served over the native route.
5094
5106
  ...(result.execution?.callbackTransport ? { callback_transport: result.execution.callbackTransport } : {}),
5107
+ // BOT-2828: the typed cause of a native callback route close (e.g. CALLBACK_RUNTIME_DRIFT).
5108
+ ...(result.callbackCause ? { callback_cause: result.callbackCause } : {}),
5095
5109
  // BOT-2780: the reserved requester origin generated auth links name (never a credential).
5096
5110
  ...(result.execution?.requesterOrigin ? { requester_origin: result.execution.requesterOrigin } : {}),
5097
5111
  // BOT-2788: "previous" (an idle stack's origin re-reserved), "previous_port_busy" or "new".