nanobazaar-cli 2.0.2 → 2.0.4

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/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to `nanobazaar-cli` are documented in this file.
4
4
 
5
5
  This project follows Semantic Versioning.
6
6
 
7
+ ## [2.0.4] - 2026-02-11
8
+
9
+ ### Fixed
10
+ - `nanobazaar watch` now reconnects SSE loops with backoff + jitter and includes health logs.
11
+
12
+ ## [2.0.3] - 2026-02-09
13
+
14
+ ### Changed
15
+ - `nanobazaar watch` now wakes only on relay wake events; removed the safety interval.
16
+
7
17
  ## [2.0.2] - 2026-02-08
8
18
 
9
19
  ### Fixed
package/bin/nanobazaar CHANGED
@@ -32,6 +32,11 @@ const CONFIG_BASE_DIR = XDG_CONFIG_HOME || path.join(HOME_DIR, '.config');
32
32
  const STATE_DEFAULT = path.join(CONFIG_BASE_DIR, 'nanobazaar', 'nanobazaar.json');
33
33
  const STATE_LOCK_RETRY_MS = 50;
34
34
  const STATE_LOCK_TIMEOUT_MS = 5000;
35
+ const WATCH_RECONNECT_BASE_MS = 1000;
36
+ const WATCH_RECONNECT_FACTOR = 2;
37
+ const WATCH_RECONNECT_CAP_MS = 60000;
38
+ const WATCH_RECONNECT_JITTER_RATIO = 0.2;
39
+ const WATCH_RECONNECT_STABLE_WINDOW_MS = 30000;
35
40
  let STATE_LOCK_SLEEP = null;
36
41
 
37
42
  function requireFetch() {
@@ -1275,10 +1280,10 @@ Commands:
1275
1280
  Poll events and optionally ack
1276
1281
  poll ack --up-to-event-id <id>
1277
1282
  Advance the server-side poll cursor (used for 410 resync)
1278
- watch [--stream-path /v0/stream] [--safety-wake-interval <seconds>]
1283
+ watch [--stream-path /v0/stream]
1279
1284
  [--state-path <path>] [--openclaw-bin <bin>] [--event-text <text>]
1280
1285
  [--mode now|next] [--no-openclaw]
1281
- Maintain SSE connection; wake OpenClaw on relay wake events + on a safety interval.
1286
+ Maintain SSE connection; wake OpenClaw on relay wake events.
1282
1287
  Does not poll or ack (OpenClaw should run /nanobazaar poll).
1283
1288
 
1284
1289
  Global flags:
@@ -2905,11 +2910,44 @@ function appendEvents(state, events) {
2905
2910
  }
2906
2911
 
2907
2912
  function backoffDelayMs(attempt, opts) {
2908
- const baseMs = opts && opts.baseMs ? opts.baseMs : 500;
2909
- const maxMs = opts && opts.maxMs ? opts.maxMs : 30000;
2910
- const exp = Math.min(maxMs, baseMs * (2 ** Math.max(0, attempt)));
2911
- const jittered = exp * (0.5 + Math.random());
2912
- return Math.min(maxMs, Math.max(baseMs, Math.floor(jittered)));
2913
+ const baseMs = opts && opts.baseMs ? opts.baseMs : WATCH_RECONNECT_BASE_MS;
2914
+ const factor = opts && opts.factor ? opts.factor : WATCH_RECONNECT_FACTOR;
2915
+ const capMs = opts && (opts.capMs || opts.maxMs) ? (opts.capMs || opts.maxMs) : WATCH_RECONNECT_CAP_MS;
2916
+ const jitterRatio = opts && typeof opts.jitterRatio === 'number'
2917
+ ? Math.min(1, Math.max(0, opts.jitterRatio))
2918
+ : WATCH_RECONNECT_JITTER_RATIO;
2919
+ const randomFn = opts && typeof opts.random === 'function' ? opts.random : Math.random;
2920
+ const safeAttempt = Math.max(0, Math.floor(attempt));
2921
+ const exponent = Math.min(capMs, baseMs * (factor ** safeAttempt));
2922
+ const jitterOffset = jitterRatio === 0 ? 0 : ((randomFn() * 2) - 1) * jitterRatio;
2923
+ const jittered = exponent * (1 + jitterOffset);
2924
+ return Math.min(capMs, Math.max(baseMs, Math.round(jittered)));
2925
+ }
2926
+
2927
+ function planReconnect(attempt, opts) {
2928
+ const connectedDurationMs = opts && typeof opts.connectedDurationMs === 'number'
2929
+ ? Math.max(0, Math.floor(opts.connectedDurationMs))
2930
+ : null;
2931
+ const stableWindowMs = opts && opts.stableWindowMs ? opts.stableWindowMs : WATCH_RECONNECT_STABLE_WINDOW_MS;
2932
+ const reason = opts && opts.reason ? String(opts.reason) : 'unknown';
2933
+ let effectiveAttempt = Math.max(0, Math.floor(attempt));
2934
+ let reset = false;
2935
+
2936
+ if (connectedDurationMs !== null && connectedDurationMs >= stableWindowMs && effectiveAttempt > 0) {
2937
+ effectiveAttempt = 0;
2938
+ reset = true;
2939
+ }
2940
+
2941
+ const delayMs = backoffDelayMs(effectiveAttempt, opts && opts.backoff);
2942
+ return {
2943
+ attemptNumber: effectiveAttempt + 1,
2944
+ nextAttempt: effectiveAttempt + 1,
2945
+ delayMs,
2946
+ reason,
2947
+ reset,
2948
+ connectedDurationMs,
2949
+ stableWindowMs,
2950
+ };
2913
2951
  }
2914
2952
 
2915
2953
  function sleep(ms, signal) {
@@ -3035,11 +3073,9 @@ async function runWatch(argv) {
3035
3073
  throw new Error('watch no longer polls. Remove polling flags (--no-ack/--no-fetch-payloads/--limit/--types/--print-polls/--output) and retry.');
3036
3074
  }
3037
3075
 
3038
- const safetyIntervalSeconds = flags.safetyWakeInterval
3039
- ? parsePositiveInt(flags.safetyWakeInterval, '--safety-wake-interval')
3040
- : flags.safetyPollInterval
3041
- ? parsePositiveInt(flags.safetyPollInterval, '--safety-poll-interval')
3042
- : 180;
3076
+ if (flags.safetyWakeInterval !== undefined || flags.safetyPollInterval !== undefined) {
3077
+ throw new Error('Safety wake interval has been removed. Remove --safety-wake-interval/--safety-poll-interval and retry.');
3078
+ }
3043
3079
 
3044
3080
  const openclawBin = String(flags.openclawBin || 'openclaw');
3045
3081
  const mode = String(flags.mode || 'now');
@@ -3050,7 +3086,6 @@ async function runWatch(argv) {
3050
3086
  relay_url: config.relay_url,
3051
3087
  state_path: statePath,
3052
3088
  stream_path: streamPath,
3053
- safety_wake_interval_seconds: safetyIntervalSeconds,
3054
3089
  openclaw_enabled: openclawEnabled,
3055
3090
  });
3056
3091
 
@@ -3091,7 +3126,7 @@ async function runWatch(argv) {
3091
3126
  const label = queuedReason || reason;
3092
3127
  queuedReason = null;
3093
3128
 
3094
- // Best-effort: waking OpenClaw can be retried on the next wake/safety interval.
3129
+ // Best-effort: waking OpenClaw can be retried on the next wake event.
3095
3130
  const didWake = triggerOpenclawWake(label);
3096
3131
  console.error(`[watch] wake ${didWake ? 'ok' : 'skipped'} (${label})`);
3097
3132
  }
@@ -3114,19 +3149,26 @@ async function runWatch(argv) {
3114
3149
  console.error(`[watch] relay=${config.relay_url}`);
3115
3150
  console.error(`[watch] state_path=${statePath}`);
3116
3151
  console.error(`[watch] stream_path=${streamPath}`);
3117
- console.error(`[watch] safety_wake_interval_seconds=${safetyIntervalSeconds}`);
3152
+
3153
+ console.error(`[watch] streams=${streams.join(',')}`);
3154
+ console.error(`[watch] safety_poll_interval_seconds=${safetyIntervalSeconds}`);
3155
+ console.error(`[watch] reconnect_backoff_base_ms=${WATCH_RECONNECT_BASE_MS}`);
3156
+ console.error(`[watch] reconnect_backoff_factor=${WATCH_RECONNECT_FACTOR}`);
3157
+ console.error(`[watch] reconnect_backoff_cap_ms=${WATCH_RECONNECT_CAP_MS}`);
3158
+ console.error(`[watch] reconnect_backoff_jitter_ratio=${WATCH_RECONNECT_JITTER_RATIO}`);
3159
+ console.error(`[watch] reconnect_stable_window_ms=${WATCH_RECONNECT_STABLE_WINDOW_MS}`);
3118
3160
 
3119
3161
  const safetyTimer = setInterval(() => {
3120
3162
  if (signal.aborted) {
3121
3163
  return;
3122
3164
  }
3123
- void runWakeLoop('safety');
3165
+ void runPollLoop('safety');
3124
3166
  }, safetyIntervalSeconds * 1000);
3125
3167
 
3126
- // Kick off an initial wake so the agent can poll even if wakeups are missed.
3127
- void runWakeLoop('startup');
3168
+ // Kick off an initial poll so the watcher is useful even if wakeups are missed.
3169
+ void runPollLoop('startup');
3128
3170
 
3129
- let attempt = 0;
3171
+ let reconnectAttempt = 0;
3130
3172
  function isWakeEvent(evt) {
3131
3173
  if (!evt) {
3132
3174
  return false;
@@ -3165,8 +3207,8 @@ async function runWatch(argv) {
3165
3207
  throw new Error(`SSE connect failed (${response.status}): ${text || response.statusText}`);
3166
3208
  }
3167
3209
 
3168
- attempt = 0;
3169
3210
  console.error('[watch] connected');
3211
+ const connectedAt = Date.now();
3170
3212
 
3171
3213
  await consumeSseStream(response.body, {
3172
3214
  signal,
@@ -3187,21 +3229,31 @@ async function runWatch(argv) {
3187
3229
  break;
3188
3230
  }
3189
3231
 
3190
- console.error('[watch] disconnected');
3232
+ const disconnectedDurationMs = Math.max(0, Date.now() - connectedAt);
3233
+ const disconnectPlan = planReconnect(reconnectAttempt, {
3234
+ reason: 'SSE stream disconnected',
3235
+ connectedDurationMs: disconnectedDurationMs,
3236
+ });
3237
+ reconnectAttempt = disconnectPlan.nextAttempt;
3238
+ if (disconnectPlan.reset) {
3239
+ console.error(`[watch] reconnect backoff reset after stable connection (${disconnectPlan.connectedDurationMs}ms >= ${disconnectPlan.stableWindowMs}ms)`);
3240
+ }
3241
+ console.error(`[watch] reconnect health: attempt=${disconnectPlan.attemptNumber} delay_ms=${disconnectPlan.delayMs} reason=${disconnectPlan.reason} connected_ms=${disconnectPlan.connectedDurationMs}`);
3242
+ await sleep(disconnectPlan.delayMs, signal);
3191
3243
  } catch (err) {
3192
3244
  if (signal.aborted) {
3193
3245
  break;
3194
3246
  }
3195
3247
  const message = err && err.message ? err.message : String(err);
3196
- const delayMs = backoffDelayMs(attempt);
3197
- attempt += 1;
3198
- console.error(`[watch] sse error: ${message}`);
3199
- console.error(`[watch] reconnecting in ${Math.round(delayMs / 1000)}s`);
3200
- await sleep(delayMs, signal);
3248
+ const errorPlan = planReconnect(reconnectAttempt, {
3249
+ reason: message,
3250
+ });
3251
+ reconnectAttempt = errorPlan.nextAttempt;
3252
+ console.error(`[watch] reconnect health: attempt=${errorPlan.attemptNumber} delay_ms=${errorPlan.delayMs} reason=${errorPlan.reason} connected_ms=unknown`);
3253
+ await sleep(errorPlan.delayMs, signal);
3201
3254
  }
3202
3255
  }
3203
3256
 
3204
- clearInterval(safetyTimer);
3205
3257
  }
3206
3258
 
3207
3259
  const DISPATCH_BOOL_FLAGS = new Set([
@@ -3444,7 +3496,19 @@ async function main() {
3444
3496
  }
3445
3497
  }
3446
3498
 
3447
- main().catch((err) => {
3448
- console.error(err.message || err);
3449
- process.exit(1);
3450
- });
3499
+ if (require.main === module) {
3500
+ main().catch((err) => {
3501
+ console.error(err.message || err);
3502
+ process.exit(1);
3503
+ });
3504
+ } else {
3505
+ module.exports = {
3506
+ backoffDelayMs,
3507
+ planReconnect,
3508
+ WATCH_RECONNECT_BASE_MS,
3509
+ WATCH_RECONNECT_FACTOR,
3510
+ WATCH_RECONNECT_CAP_MS,
3511
+ WATCH_RECONNECT_JITTER_RATIO,
3512
+ WATCH_RECONNECT_STABLE_WINDOW_MS,
3513
+ };
3514
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanobazaar-cli",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "NanoBazaar CLI for the NanoBazaar Relay and OpenClaw skill.",
5
5
  "homepage": "https://github.com/nanobazaar/nanobazaar/tree/main/packages/nanobazaar-cli#readme",
6
6
  "license": "UNLICENSED",