nanobazaar-cli 2.0.3 → 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 +5 -0
- package/bin/nanobazaar +96 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ 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
|
+
|
|
7
12
|
## [2.0.3] - 2026-02-09
|
|
8
13
|
|
|
9
14
|
### Changed
|
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() {
|
|
@@ -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) {
|
|
@@ -3112,7 +3150,25 @@ async function runWatch(argv) {
|
|
|
3112
3150
|
console.error(`[watch] state_path=${statePath}`);
|
|
3113
3151
|
console.error(`[watch] stream_path=${streamPath}`);
|
|
3114
3152
|
|
|
3115
|
-
|
|
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}`);
|
|
3160
|
+
|
|
3161
|
+
const safetyTimer = setInterval(() => {
|
|
3162
|
+
if (signal.aborted) {
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
void runPollLoop('safety');
|
|
3166
|
+
}, safetyIntervalSeconds * 1000);
|
|
3167
|
+
|
|
3168
|
+
// Kick off an initial poll so the watcher is useful even if wakeups are missed.
|
|
3169
|
+
void runPollLoop('startup');
|
|
3170
|
+
|
|
3171
|
+
let reconnectAttempt = 0;
|
|
3116
3172
|
function isWakeEvent(evt) {
|
|
3117
3173
|
if (!evt) {
|
|
3118
3174
|
return false;
|
|
@@ -3151,8 +3207,8 @@ async function runWatch(argv) {
|
|
|
3151
3207
|
throw new Error(`SSE connect failed (${response.status}): ${text || response.statusText}`);
|
|
3152
3208
|
}
|
|
3153
3209
|
|
|
3154
|
-
attempt = 0;
|
|
3155
3210
|
console.error('[watch] connected');
|
|
3211
|
+
const connectedAt = Date.now();
|
|
3156
3212
|
|
|
3157
3213
|
await consumeSseStream(response.body, {
|
|
3158
3214
|
signal,
|
|
@@ -3173,17 +3229,28 @@ async function runWatch(argv) {
|
|
|
3173
3229
|
break;
|
|
3174
3230
|
}
|
|
3175
3231
|
|
|
3176
|
-
|
|
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);
|
|
3177
3243
|
} catch (err) {
|
|
3178
3244
|
if (signal.aborted) {
|
|
3179
3245
|
break;
|
|
3180
3246
|
}
|
|
3181
3247
|
const message = err && err.message ? err.message : String(err);
|
|
3182
|
-
const
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
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);
|
|
3187
3254
|
}
|
|
3188
3255
|
}
|
|
3189
3256
|
|
|
@@ -3429,7 +3496,19 @@ async function main() {
|
|
|
3429
3496
|
}
|
|
3430
3497
|
}
|
|
3431
3498
|
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
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",
|