dsh-done-sound 0.1.13 → 0.1.15
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/lib/client.js +111 -59
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -370,6 +370,7 @@ window.__ModuleLoader__.load({
|
|
|
370
370
|
deadline: 0,
|
|
371
371
|
timer: null,
|
|
372
372
|
delayMs: 60000,
|
|
373
|
+
streamingSince: null, // when the loop first saw the session stuck streaming
|
|
373
374
|
};
|
|
374
375
|
// Latest session view, refreshed by the detector on every snapshot change,
|
|
375
376
|
// so this module-level timer can decide without reaching into React state.
|
|
@@ -380,13 +381,22 @@ window.__ModuleLoader__.load({
|
|
|
380
381
|
retryState.timer = null;
|
|
381
382
|
}
|
|
382
383
|
}
|
|
383
|
-
function cancelModuleRetry() {
|
|
384
|
+
function cancelModuleRetry(why) {
|
|
385
|
+
if (retryState.active || retryState.timer !== null) {
|
|
386
|
+
try {
|
|
387
|
+
console.info('[dsh-done-sound] retry: cancel', why || 'stop');
|
|
388
|
+
reportLog('info', ['retry: cancel', why || 'stop']);
|
|
389
|
+
} catch {
|
|
390
|
+
// ignore
|
|
391
|
+
}
|
|
392
|
+
}
|
|
384
393
|
clearRetryTimer();
|
|
385
394
|
retryState.active = false;
|
|
386
395
|
retryState.sessionId = null;
|
|
387
396
|
retryState.endTurnAtError = 0;
|
|
388
397
|
retryState.attempt = 0;
|
|
389
398
|
retryState.deadline = 0;
|
|
399
|
+
retryState.streamingSince = null;
|
|
390
400
|
}
|
|
391
401
|
/** Queue the next attempt and publish the countdown deadline. */
|
|
392
402
|
function scheduleModuleRetry(delayMs) {
|
|
@@ -410,70 +420,71 @@ window.__ModuleLoader__.load({
|
|
|
410
420
|
if (!retryState.active) return;
|
|
411
421
|
const sid = retryState.sessionId;
|
|
412
422
|
const delayMs = retryState.delayMs;
|
|
413
|
-
|
|
414
|
-
//
|
|
415
|
-
// (
|
|
423
|
+
|
|
424
|
+
// Stop the loop only when the session truly recovered: a newer turn
|
|
425
|
+
// closed normally (`completed`). Aborts / blocks / errors are FAILURES
|
|
426
|
+
// from the user's point of view (a dropped model often ends the retry
|
|
427
|
+
// turn as `aborted`) — those must keep retrying until the user stops it.
|
|
416
428
|
const recovered =
|
|
417
429
|
liveSnap.endReason !== null &&
|
|
418
430
|
liveSnap.endTurn > retryState.endTurnAtError &&
|
|
419
|
-
liveSnap.endReason
|
|
420
|
-
liveSnap.endReason !== 'max-tokens';
|
|
431
|
+
liveSnap.endReason === 'completed';
|
|
421
432
|
if (liveSnap.sessionId !== sid || liveSnap.pendingCount > 0 || recovered) {
|
|
422
|
-
|
|
433
|
+
retryLog('stop', 'liveSnap=' + String(liveSnap.sessionId) + '/' + (liveSnap.endReason || 'idle') + '/' + (liveSnap.pendingCount || 0));
|
|
434
|
+
cancelModuleRetry('recovered-or-stale');
|
|
423
435
|
setConfig({ retryCooldownUntil: null });
|
|
424
436
|
return;
|
|
425
437
|
}
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
438
|
+
|
|
439
|
+
// Every retry window sends "继续" — no waiting on a "streaming" state.
|
|
440
|
+
// A dropped model leaves the session stuck in running=true (streaming)
|
|
441
|
+
// with no turn/end event, so waiting on it made the countdown end without
|
|
442
|
+
// ever sending. The loop only stops on true recovery, a human-approval
|
|
443
|
+
// wait, a session switch, a config toggle, or the user pressing 终止重试.
|
|
444
|
+
retryLog('send', 'window fired attempt=' + String(retryState.attempt + 1));
|
|
445
|
+
sendModuleRetryContinue(sid, delayMs);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** One attempt: send "继续" and schedule the next window regardless of result. */
|
|
449
|
+
function sendModuleRetryContinue(sid, delayMs) {
|
|
432
450
|
const retryLater = () => {
|
|
433
451
|
if (retryState.active) scheduleModuleRetry(delayMs);
|
|
434
452
|
};
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
453
|
+
// DSH 0.1.5-rc.2 exposes the sessions service as `ctx.sessions`:
|
|
454
|
+
// `sessions.binding(sessionId).session.prompt(...)`. The older
|
|
455
|
+
// `connection.api.sessions.prompt` shape no longer exists there, which is
|
|
456
|
+
// why retries stopped after arming (prompt-api-unavailable). Support both
|
|
457
|
+
// so one bundle serves rc.1 and rc.2 hosts.
|
|
458
|
+
let session = null;
|
|
459
|
+
let prompt = null;
|
|
460
|
+
try {
|
|
461
|
+
const sessions = pluginCtx && pluginCtx.sessions;
|
|
462
|
+
if (sessions && typeof sessions.binding === 'function') {
|
|
463
|
+
const binding = sessions.binding(sid);
|
|
464
|
+
if (binding && binding.session && typeof binding.session.prompt === 'function') {
|
|
465
|
+
session = binding.session;
|
|
466
|
+
prompt = (content, mode) => session.prompt(content, mode);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
// fall through to legacy path
|
|
471
|
+
}
|
|
472
|
+
if (typeof prompt !== 'function') {
|
|
445
473
|
try {
|
|
446
|
-
|
|
447
|
-
|
|
474
|
+
const legacy = pluginCtx && pluginCtx.connection && pluginCtx.connection.api && pluginCtx.connection.api.sessions;
|
|
475
|
+
if (legacy && typeof legacy.prompt === 'function') {
|
|
476
|
+
prompt = (content, mode) => legacy.prompt({ sessionId: sid, mode, content });
|
|
477
|
+
}
|
|
448
478
|
} catch {
|
|
449
|
-
//
|
|
479
|
+
// ignore
|
|
450
480
|
}
|
|
451
|
-
|
|
452
|
-
lastEvent: {
|
|
453
|
-
at: Date.now(),
|
|
454
|
-
kind: 'retry',
|
|
455
|
-
reason: 'retry-failed',
|
|
456
|
-
play: false,
|
|
457
|
-
retried: true,
|
|
458
|
-
retryFailed: true,
|
|
459
|
-
retryAttempt: retryState.attempt,
|
|
460
|
-
},
|
|
461
|
-
});
|
|
462
|
-
retryLater();
|
|
463
|
-
};
|
|
481
|
+
}
|
|
464
482
|
if (typeof prompt !== 'function') {
|
|
465
483
|
// Nothing to retry with — stop instead of spinning a useless timer.
|
|
466
|
-
cancelModuleRetry();
|
|
484
|
+
cancelModuleRetry('prompt-api-unavailable');
|
|
467
485
|
setConfig({
|
|
468
486
|
retryCooldownUntil: null,
|
|
469
|
-
lastEvent: {
|
|
470
|
-
at: Date.now(),
|
|
471
|
-
kind: 'retry',
|
|
472
|
-
reason: 'retry-failed',
|
|
473
|
-
play: false,
|
|
474
|
-
retried: true,
|
|
475
|
-
retryFailed: true,
|
|
476
|
-
},
|
|
487
|
+
lastEvent: { at: Date.now(), kind: 'retry', reason: 'retry-failed', play: false, retried: true, retryFailed: true },
|
|
477
488
|
});
|
|
478
489
|
return;
|
|
479
490
|
}
|
|
@@ -483,13 +494,16 @@ window.__ModuleLoader__.load({
|
|
|
483
494
|
} catch {
|
|
484
495
|
// keep UTC
|
|
485
496
|
}
|
|
486
|
-
|
|
497
|
+
retryLog('send', '继续 attempt=' + String(retryState.attempt + 1));
|
|
498
|
+
Promise.resolve(prompt([{ type: 'text', text: '继续' }], 'queue'))
|
|
487
499
|
.then((res) => {
|
|
488
|
-
|
|
489
|
-
|
|
500
|
+
// rc.2 sessions prompt resolves to RemoteResult ({ok} | {ok:false,
|
|
501
|
+
// error}); the legacy connection API resolves to {result: {...}}.
|
|
502
|
+
const result = res && res.result ? res.result : res;
|
|
503
|
+
if (result && result.ok === true) {
|
|
490
504
|
try {
|
|
491
505
|
console.log('[dsh-done-sound] auto-retry sent "继续" to session', sid);
|
|
492
|
-
reportLog('info', ['auto-retry sent "继续" to session', sid]);
|
|
506
|
+
reportLog('info', ['auto-retry sent "继续" to session', sid, 'attempt', retryState.attempt + 1]);
|
|
493
507
|
} catch {
|
|
494
508
|
// ignore
|
|
495
509
|
}
|
|
@@ -505,6 +519,9 @@ window.__ModuleLoader__.load({
|
|
|
505
519
|
// never let a playback error break the retry flow
|
|
506
520
|
}
|
|
507
521
|
retryState.attempt += 1;
|
|
522
|
+
// A send that the host accepted but that revived nothing must still
|
|
523
|
+
// keep the loop alive — the next window sends "继续" again.
|
|
524
|
+
retryState.streamingSince = null;
|
|
508
525
|
setConfig({
|
|
509
526
|
lastEvent: {
|
|
510
527
|
at: Date.now(),
|
|
@@ -516,22 +533,52 @@ window.__ModuleLoader__.load({
|
|
|
516
533
|
retryAttempt: retryState.attempt,
|
|
517
534
|
},
|
|
518
535
|
});
|
|
519
|
-
// Keep the loop alive: if this attempt did not revive the session,
|
|
520
|
-
// the next tick sends "继续" again until the user stops it.
|
|
521
536
|
retryLater();
|
|
522
537
|
} else {
|
|
523
538
|
const err = result && result.error ? result.error.message : JSON.stringify(result);
|
|
524
|
-
|
|
539
|
+
sendRetryFail(err || 'prompt not accepted', delayMs);
|
|
525
540
|
}
|
|
526
541
|
})
|
|
527
|
-
.catch((err) =>
|
|
542
|
+
.catch((err) => sendRetryFail(err && err.message ? err.message : String(err), delayMs));
|
|
528
543
|
}
|
|
544
|
+
|
|
545
|
+
function sendRetryFail(why, delayMs) {
|
|
546
|
+
try {
|
|
547
|
+
console.error('[dsh-done-sound] auto-retry could not send "继续":', why);
|
|
548
|
+
reportLog('error', ['auto-retry could not send "继续":', why]);
|
|
549
|
+
} catch {
|
|
550
|
+
// console may be gone during teardown
|
|
551
|
+
}
|
|
552
|
+
setConfig({
|
|
553
|
+
lastEvent: {
|
|
554
|
+
at: Date.now(),
|
|
555
|
+
kind: 'retry',
|
|
556
|
+
reason: 'retry-failed',
|
|
557
|
+
play: false,
|
|
558
|
+
retried: true,
|
|
559
|
+
retryFailed: true,
|
|
560
|
+
retryAttempt: retryState.attempt,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
if (retryState.active) scheduleModuleRetry(delayMs);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function retryLog(action, detail) {
|
|
567
|
+
try {
|
|
568
|
+
console.info('[dsh-done-sound] retry:', action, detail || '');
|
|
569
|
+
reportLog('info', ['retry:', action, detail || '']);
|
|
570
|
+
} catch {
|
|
571
|
+
// ignore — logging must never break the loop
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
529
575
|
function startModuleRetry(sessionId, endTurnAtError, delayMs) {
|
|
530
|
-
cancelModuleRetry();
|
|
576
|
+
cancelModuleRetry('re-arm');
|
|
531
577
|
retryState.active = true;
|
|
532
578
|
retryState.sessionId = sessionId;
|
|
533
579
|
retryState.endTurnAtError = endTurnAtError;
|
|
534
580
|
retryState.attempt = 0;
|
|
581
|
+
retryState.streamingSince = null;
|
|
535
582
|
scheduleModuleRetry(delayMs);
|
|
536
583
|
}
|
|
537
584
|
|
|
@@ -1352,8 +1399,12 @@ window.__ModuleLoader__.load({
|
|
|
1352
1399
|
liveSnap.endTurn = snap.endTurn;
|
|
1353
1400
|
liveSnap.endReason = snap.endReason;
|
|
1354
1401
|
if (retryState.active) {
|
|
1402
|
+
// Only a newer turn that closed with `completed` counts as a real
|
|
1403
|
+
// recovery. `aborted`/`blocked` (a dropped model often ends its
|
|
1404
|
+
// retry turn that way) and `error`/`max-tokens` are FAILURES that
|
|
1405
|
+
// must keep the retry loop running until the user stops it.
|
|
1355
1406
|
const newerClosed = snap.endReason !== null && snap.endTurn > retryState.endTurnAtError;
|
|
1356
|
-
const recovered = newerClosed && snap.endReason
|
|
1407
|
+
const recovered = newerClosed && snap.endReason === 'completed';
|
|
1357
1408
|
const stop =
|
|
1358
1409
|
snap.sessionId !== retryState.sessionId ||
|
|
1359
1410
|
snap.pendingCount > 0 ||
|
|
@@ -1549,7 +1600,7 @@ if (snap.pendingCount > 0) {
|
|
|
1549
1600
|
}
|
|
1550
1601
|
|
|
1551
1602
|
exports.name = 'dsh-done-sound';
|
|
1552
|
-
exports.inject = ['slots', 'remote', 'remote.commands', 'connection'];
|
|
1603
|
+
exports.inject = ['slots', 'sessions', 'remote', 'remote.commands', 'connection'];
|
|
1553
1604
|
exports.apply = apply;
|
|
1554
1605
|
// Test seam: `test/retry.test.mjs` loads this bundle with a React stub and
|
|
1555
1606
|
// drives the module-level retry loop directly, so the "keeps retrying until
|
|
@@ -1566,3 +1617,4 @@ if (snap.pendingCount > 0) {
|
|
|
1566
1617
|
},
|
|
1567
1618
|
});
|
|
1568
1619
|
|
|
1620
|
+
|