dsh-done-sound 0.1.13 → 0.1.14

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 +89 -53
  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,61 @@ 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
+ const now = Date.now();
424
+
425
+ // Stop the loop only when the session truly recovered: a newer turn
426
+ // closed normally (`completed`). Aborts / blocks / errors are FAILURES
427
+ // from the user's point of view (a dropped model often ends the retry
428
+ // turn as `aborted`) — those must keep retrying until the user stops it.
416
429
  const recovered =
417
430
  liveSnap.endReason !== null &&
418
431
  liveSnap.endTurn > retryState.endTurnAtError &&
419
- liveSnap.endReason !== 'error' &&
420
- liveSnap.endReason !== 'max-tokens';
432
+ liveSnap.endReason === 'completed';
421
433
  if (liveSnap.sessionId !== sid || liveSnap.pendingCount > 0 || recovered) {
422
- cancelModuleRetry();
434
+ retryLog('stop', 'liveSnap=' + String(liveSnap.sessionId) + '/' + (liveSnap.endReason || 'idle') + '/' + (liveSnap.pendingCount || 0));
435
+ cancelModuleRetry('recovered-or-stale');
423
436
  setConfig({ retryCooldownUntil: null });
424
437
  return;
425
438
  }
426
- // The agent is generating again: wait for that turn instead of stacking
427
- // another "继续" on top of it.
439
+
440
+ // The agent is generating again. Wait for that turn to close instead of
441
+ // stacking another "继续" on top of it — UNLESS it has been "streaming"
442
+ // without ever closing for a suspiciously long time. A dropped model can
443
+ // leave the session stuck in running=true with no turn/end event, which
444
+ // would otherwise make the loop wait forever silently (the "only one
445
+ // retry" symptom). In that case treat it as hung and force another send.
428
446
  if (liveSnap.streaming === true) {
429
- scheduleModuleRetry(delayMs);
447
+ if (retryState.streamingSince === null) retryState.streamingSince = now;
448
+ const stuckCap = Math.max(delayMs, 60000);
449
+ if (now - retryState.streamingSince < stuckCap) {
450
+ retryLog('wait', 'streaming ' + String(Math.round((now - retryState.streamingSince) / 1000)) + 's');
451
+ scheduleModuleRetry(delayMs);
452
+ return;
453
+ }
454
+ retryLog('force', 'streaming stuck ' + String(Math.round((now - retryState.streamingSince) / 1000)) + 's — sending anyway');
455
+ retryState.streamingSince = null;
456
+ sendModuleRetryContinue(sid, delayMs);
430
457
  return;
431
458
  }
432
- const retryLater = () => {
433
- if (retryState.active) scheduleModuleRetry(delayMs);
434
- };
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.
459
+
460
+ retryState.streamingSince = null;
461
+ sendModuleRetryContinue(sid, delayMs);
462
+ }
463
+
464
+ /** One attempt: send "继续" and schedule the next window regardless of result. */
465
+ function sendModuleRetryContinue(sid, delayMs) {
441
466
  const prompt = pluginCtx && pluginCtx.connection && pluginCtx.connection.api && pluginCtx.connection.api.sessions
442
467
  ? pluginCtx.connection.api.sessions.prompt
443
468
  : null;
444
- const fail = (why) => {
445
- try {
446
- console.error('[dsh-done-sound] auto-retry could not send "继续":', why);
447
- reportLog('error', ['auto-retry could not send "继续":', why]);
448
- } catch {
449
- // console may be gone during teardown
450
- }
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();
469
+ const retryLater = () => {
470
+ if (retryState.active) scheduleModuleRetry(delayMs);
463
471
  };
464
472
  if (typeof prompt !== 'function') {
465
473
  // Nothing to retry with — stop instead of spinning a useless timer.
466
- cancelModuleRetry();
474
+ cancelModuleRetry('prompt-api-unavailable');
467
475
  setConfig({
468
476
  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
- },
477
+ lastEvent: { at: Date.now(), kind: 'retry', reason: 'retry-failed', play: false, retried: true, retryFailed: true },
477
478
  });
478
479
  return;
479
480
  }
@@ -483,13 +484,14 @@ window.__ModuleLoader__.load({
483
484
  } catch {
484
485
  // keep UTC
485
486
  }
487
+ retryLog('send', '继续 attempt=' + String(retryState.attempt + 1));
486
488
  Promise.resolve(prompt({ sessionId: sid, mode: 'queue', content: [{ type: 'text', text: '继续' }], clientTimeZone: tz }))
487
489
  .then((res) => {
488
490
  const result = res && res.result;
489
491
  if (result && result.ok) {
490
492
  try {
491
493
  console.log('[dsh-done-sound] auto-retry sent "继续" to session', sid);
492
- reportLog('info', ['auto-retry sent "继续" to session', sid]);
494
+ reportLog('info', ['auto-retry sent "继续" to session', sid, 'attempt', retryState.attempt + 1]);
493
495
  } catch {
494
496
  // ignore
495
497
  }
@@ -505,6 +507,9 @@ window.__ModuleLoader__.load({
505
507
  // never let a playback error break the retry flow
506
508
  }
507
509
  retryState.attempt += 1;
510
+ // A send that the host accepted but that revived nothing must still
511
+ // keep the loop alive — the next window sends "继续" again.
512
+ retryState.streamingSince = null;
508
513
  setConfig({
509
514
  lastEvent: {
510
515
  at: Date.now(),
@@ -516,22 +521,52 @@ window.__ModuleLoader__.load({
516
521
  retryAttempt: retryState.attempt,
517
522
  },
518
523
  });
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
524
  retryLater();
522
525
  } else {
523
526
  const err = result && result.error ? result.error.message : JSON.stringify(result);
524
- fail(err || 'prompt not accepted');
527
+ sendRetryFail(err || 'prompt not accepted', delayMs);
525
528
  }
526
529
  })
527
- .catch((err) => fail(err && err.message ? err.message : String(err)));
530
+ .catch((err) => sendRetryFail(err && err.message ? err.message : String(err), delayMs));
531
+ }
532
+
533
+ function sendRetryFail(why, delayMs) {
534
+ try {
535
+ console.error('[dsh-done-sound] auto-retry could not send "继续":', why);
536
+ reportLog('error', ['auto-retry could not send "继续":', why]);
537
+ } catch {
538
+ // console may be gone during teardown
539
+ }
540
+ setConfig({
541
+ lastEvent: {
542
+ at: Date.now(),
543
+ kind: 'retry',
544
+ reason: 'retry-failed',
545
+ play: false,
546
+ retried: true,
547
+ retryFailed: true,
548
+ retryAttempt: retryState.attempt,
549
+ },
550
+ });
551
+ if (retryState.active) scheduleModuleRetry(delayMs);
528
552
  }
553
+
554
+ function retryLog(action, detail) {
555
+ try {
556
+ console.info('[dsh-done-sound] retry:', action, detail || '');
557
+ reportLog('info', ['retry:', action, detail || '']);
558
+ } catch {
559
+ // ignore — logging must never break the loop
560
+ }
561
+ }
562
+
529
563
  function startModuleRetry(sessionId, endTurnAtError, delayMs) {
530
- cancelModuleRetry();
564
+ cancelModuleRetry('re-arm');
531
565
  retryState.active = true;
532
566
  retryState.sessionId = sessionId;
533
567
  retryState.endTurnAtError = endTurnAtError;
534
568
  retryState.attempt = 0;
569
+ retryState.streamingSince = null;
535
570
  scheduleModuleRetry(delayMs);
536
571
  }
537
572
 
@@ -1566,3 +1601,4 @@ if (snap.pendingCount > 0) {
1566
1601
  },
1567
1602
  });
1568
1603
 
1604
+
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.14",
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",