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.
Files changed (2) hide show
  1. package/lib/client.js +111 -59
  2. 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
- // Stop the loop once the situation it was armed for is gone: another
414
- // session, a human-approval wait, or a newer turn that closed normally
415
- // (i.e. the retry worked).
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 !== 'error' &&
420
- liveSnap.endReason !== 'max-tokens';
431
+ liveSnap.endReason === 'completed';
421
432
  if (liveSnap.sessionId !== sid || liveSnap.pendingCount > 0 || recovered) {
422
- cancelModuleRetry();
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
- // The agent is generating again: wait for that turn instead of stacking
427
- // another "继续" on top of it.
428
- if (liveSnap.streaming === true) {
429
- scheduleModuleRetry(delayMs);
430
- return;
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
- // Real send path: the browser→host RPC client (same route the composer's
436
- // "send" uses). ctx.connection is provided by @deepseek-ai/dsh-client-connection;
437
- // its api.sessions.prompt carries an RpcResponse whose `.result` is {ok} |
438
- // {ok:false,error}. Failures are surfaced (console + settings-card row) —
439
- // the old remote.sessions guess was silently skipped because no such
440
- // namespace exists on the client context.
441
- const prompt = pluginCtx && pluginCtx.connection && pluginCtx.connection.api && pluginCtx.connection.api.sessions
442
- ? pluginCtx.connection.api.sessions.prompt
443
- : null;
444
- const fail = (why) => {
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
- console.error('[dsh-done-sound] auto-retry could not send "继续":', why);
447
- reportLog('error', ['auto-retry could not send "继续":', why]);
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
- // console may be gone during teardown
479
+ // ignore
450
480
  }
451
- setConfig({
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
- Promise.resolve(prompt({ sessionId: sid, mode: 'queue', content: [{ type: 'text', text: '继续' }], clientTimeZone: tz }))
497
+ retryLog('send', '继续 attempt=' + String(retryState.attempt + 1));
498
+ Promise.resolve(prompt([{ type: 'text', text: '继续' }], 'queue'))
487
499
  .then((res) => {
488
- const result = res && res.result;
489
- if (result && result.ok) {
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
- fail(err || 'prompt not accepted');
539
+ sendRetryFail(err || 'prompt not accepted', delayMs);
525
540
  }
526
541
  })
527
- .catch((err) => fail(err && err.message ? err.message : String(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 !== 'error' && snap.endReason !== 'max-tokens';
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
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-done-sound",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Play a user-chosen sound whenever a conversation finishes in the DeepSeek Harness web GUI",
5
5
  "license": "MIT",
6
6
  "type": "module",