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 +10 -0
- package/bin/nanobazaar +95 -31
- package/package.json +1 -1
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]
|
|
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
|
|
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 :
|
|
2909
|
-
const
|
|
2910
|
-
const
|
|
2911
|
-
const
|
|
2912
|
-
|
|
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
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
3165
|
+
void runPollLoop('safety');
|
|
3124
3166
|
}, safetyIntervalSeconds * 1000);
|
|
3125
3167
|
|
|
3126
|
-
// Kick off an initial
|
|
3127
|
-
void
|
|
3168
|
+
// Kick off an initial poll so the watcher is useful even if wakeups are missed.
|
|
3169
|
+
void runPollLoop('startup');
|
|
3128
3170
|
|
|
3129
|
-
let
|
|
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
|
-
|
|
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
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
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
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
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.
|
|
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",
|