dsh-client-auto-continue 0.10.0 → 0.10.2

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/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  <a href="https://github.com/HsiangNianian/dsh-auto-continue/stargazers"><img src="https://img.shields.io/github/stars/HsiangNianian/dsh-auto-continue?logo=github&label=Stars" alt="GitHub stars"></a>
18
18
  <a href="https://github.com/HsiangNianian/dsh-auto-continue/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-65a30d?style=flat" alt="MIT license"></a>
19
19
  <a href="https://awesome-dsh-plugin.com"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="awesome · DSH plugin"></a>
20
+ <a href="https://www.dsh.so/artifact/dsh-auto-continue/"><img src="https://www.dsh.so/badge/install/dsh-auto-continue.svg" alt="dsh.so install"></a>
20
21
  <br>
21
22
  <img src="https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=fff" alt="TypeScript">
22
23
  <img src="https://img.shields.io/badge/esbuild-FFCF00?style=flat&logo=esbuild&logoColor=000" alt="esbuild">
package/README.zh.md CHANGED
@@ -17,6 +17,7 @@
17
17
  <a href="https://github.com/HsiangNianian/dsh-auto-continue/stargazers"><img src="https://img.shields.io/github/stars/HsiangNianian/dsh-auto-continue?logo=github&label=Stars" alt="GitHub stars"></a>
18
18
  <a href="https://github.com/HsiangNianian/dsh-auto-continue/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-65a30d?style=flat" alt="MIT license"></a>
19
19
  <a href="https://awesome-dsh-plugin.com"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="awesome · DSH plugin"></a>
20
+ <a href="https://www.dsh.so/artifact/dsh-auto-continue/"><img src="https://www.dsh.so/badge/install/dsh-auto-continue.svg" alt="dsh.so install"></a>
20
21
  <br>
21
22
  <img src="https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=fff" alt="TypeScript">
22
23
  <img src="https://img.shields.io/badge/esbuild-FFCF00?style=flat&logo=esbuild&logoColor=000" alt="esbuild">
package/lib/index.js CHANGED
@@ -317,7 +317,10 @@ var AutoContinueRunner = class {
317
317
  this.noticeListeners = /* @__PURE__ */ new Set();
318
318
  this.stateListeners = /* @__PURE__ */ new Set();
319
319
  this.disposed = false;
320
- ctx.on("session/event", (session, event) => this.onHostEvent(session, event));
320
+ this.disposeSessionEvents = ctx.on(
321
+ "session/event",
322
+ (session, event) => this.onHostEvent(session, event)
323
+ );
321
324
  const config = this.getConfig();
322
325
  if (config.scanOnBoot) {
323
326
  void this.bootScanLoop();
@@ -379,7 +382,9 @@ var AutoContinueRunner = class {
379
382
  this.emitState();
380
383
  }
381
384
  dispose() {
385
+ if (this.disposed) return;
382
386
  this.disposed = true;
387
+ this.disposeSessionEvents();
383
388
  for (const state of this.states.values()) {
384
389
  if (state.pendingTimer !== void 0) clearTimeout(state.pendingTimer);
385
390
  if (state.loopRetryTimer !== void 0) clearTimeout(state.loopRetryTimer);
@@ -560,7 +565,11 @@ ${event.data.arguments}`;
560
565
  if (state.loopRetryTimer !== void 0) clearTimeout(state.loopRetryTimer);
561
566
  state.loopRetryTimer = setTimeout(() => {
562
567
  state.loopRetryTimer = void 0;
563
- this.schedule(sessionId, "loop:aborted");
568
+ try {
569
+ this.schedule(sessionId, "loop:aborted");
570
+ } catch (error) {
571
+ console.error(`[auto-continue] loop 重启异常 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
572
+ }
564
573
  }, remaining);
565
574
  this.log(`loop 重启延迟 ${remaining}ms(冷却期) ${sessionId}`);
566
575
  } else {
@@ -691,7 +700,11 @@ ${event.data.arguments}`;
691
700
  clearTimeout(state.pendingTimer);
692
701
  state.pendingTimer = void 0;
693
702
  }
694
- await this.fire(sessionId, "manual:notification", true);
703
+ try {
704
+ await this.fire(sessionId, "manual:notification", true);
705
+ } catch (error) {
706
+ console.error(`[auto-continue] 手动续跑异常 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
707
+ }
695
708
  }
696
709
  /** 本会话当前生效的冷却间隔(自适应退避)。 */
697
710
  cooldownFor(state) {
@@ -726,7 +739,11 @@ ${event.data.arguments}`;
726
739
  const timer = setTimeout(() => {
727
740
  if (state.pendingTimer !== timer) return;
728
741
  state.pendingTimer = void 0;
729
- void this.fire(sessionId, reason);
742
+ try {
743
+ void this.fire(sessionId, reason);
744
+ } catch (error) {
745
+ console.error(`[auto-continue] 定时发送异常 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
746
+ }
730
747
  }, config.graceMs);
731
748
  state.pendingTimer = timer;
732
749
  const template = reason.startsWith("loop:") ? config.loopText : reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
@@ -992,11 +1009,14 @@ function apply(ctx) {
992
1009
  applies: "live"
993
1010
  });
994
1011
  });
1012
+ let runnerRef;
995
1013
  ctx.inject(["settings", "agents", "webServer"], (engineCtx) => {
1014
+ if (runnerRef !== void 0) runnerRef.dispose();
996
1015
  const runner = new AutoContinueRunner(
997
1016
  engineCtx,
998
1017
  () => resolveConfig(engineCtx.settings.get(settingsNamespace(AUTO_CONTINUE_NS)))
999
1018
  );
1019
+ runnerRef = runner;
1000
1020
  const sseClients = /* @__PURE__ */ new Set();
1001
1021
  const pushToAll = (data) => {
1002
1022
  for (const send of sseClients) {
@@ -1071,6 +1091,11 @@ function apply(ctx) {
1071
1091
  }
1072
1092
  });
1073
1093
  });
1094
+ ctx.effect(() => () => {
1095
+ const runner = runnerRef;
1096
+ runnerRef = void 0;
1097
+ if (runner !== void 0) runner.dispose();
1098
+ });
1074
1099
  }
1075
1100
  export {
1076
1101
  AUTO_CONTINUE_NS,
@@ -36,6 +36,7 @@ export declare class AutoContinueRunner {
36
36
  private readonly notices;
37
37
  private readonly noticeListeners;
38
38
  private readonly stateListeners;
39
+ private readonly disposeSessionEvents;
39
40
  private disposed;
40
41
  /**
41
42
  * @param ctx - host plugin context (agents registry, session events, settings).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-client-auto-continue",
3
3
  "description": "DSH Web UI plugin: automatically sends a localized continue prompt when a request is interrupted by network errors or other non-human causes",
4
- "version": "0.10.0",
4
+ "version": "0.10.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "build": "node build.mjs && tsc -p tsconfig.build.json",
47
47
  "watch": "node build.mjs --watch",
48
48
  "typecheck": "tsc --noEmit",
49
- "test": "node tests/simulate-host.mjs && node tests/simulate-client-loader.mjs",
49
+ "test": "node tests/simulate-host.mjs && node tests/simulate-client-loader.mjs && node tests/prepare-github-package.mjs",
50
50
  "prepack": "npm run build"
51
51
  },
52
52
  "keywords": [
@@ -0,0 +1,39 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ export const SOURCE_PACKAGE_NAME = 'dsh-client-auto-continue';
6
+ export const GITHUB_PACKAGE_NAME = '@hsiangnianian/dsh-auto-continue';
7
+
8
+ const sourceEntry = `name: '${SOURCE_PACKAGE_NAME}'`;
9
+ const githubEntry = `name: '${GITHUB_PACKAGE_NAME}'`;
10
+
11
+ /** Rewrite the publish-only package identity and its DSH loader row together. */
12
+ export function prepareGitHubPackage(root) {
13
+ const manifestPath = join(root, 'package.json');
14
+ const patchPath = join(root, 'cordis.patch.yml');
15
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
16
+ const patch = readFileSync(patchPath, 'utf8');
17
+
18
+ if (manifest.name !== SOURCE_PACKAGE_NAME) {
19
+ throw new Error(`Expected package name ${SOURCE_PACKAGE_NAME}, received ${String(manifest.name)}`);
20
+ }
21
+
22
+ const entryCount = patch.split(sourceEntry).length - 1;
23
+ if (entryCount !== 1) {
24
+ throw new Error(`Cordis patch must reference ${SOURCE_PACKAGE_NAME} exactly once`);
25
+ }
26
+
27
+ manifest.name = GITHUB_PACKAGE_NAME;
28
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
29
+ writeFileSync(patchPath, patch.replace(sourceEntry, githubEntry));
30
+
31
+ return { sourceName: SOURCE_PACKAGE_NAME, packageName: GITHUB_PACKAGE_NAME };
32
+ }
33
+
34
+ const scriptPath = fileURLToPath(import.meta.url);
35
+ if (process.argv[1] && resolve(process.argv[1]) === scriptPath) {
36
+ const root = process.argv[2] ? resolve(process.argv[2]) : resolve(dirname(scriptPath), '..');
37
+ const result = prepareGitHubPackage(root);
38
+ console.log(`Prepared ${result.packageName} from ${result.sourceName}`);
39
+ }
@@ -143,6 +143,7 @@ export class AutoContinueRunner {
143
143
  private readonly notices: HostNotice[] = [];
144
144
  private readonly noticeListeners = new Set<() => void>();
145
145
  private readonly stateListeners = new Set<() => void>();
146
+ private readonly disposeSessionEvents: () => void;
146
147
  private disposed = false;
147
148
 
148
149
  /**
@@ -154,7 +155,9 @@ export class AutoContinueRunner {
154
155
  private readonly getConfig: () => AutoContinueConfig,
155
156
  ) {
156
157
  // 单实例事件源: 宿主进程内的会话事件 firehose, 天然覆盖所有会话。
157
- ctx.on('session/event', (session, event) => this.onHostEvent(session, event));
158
+ this.disposeSessionEvents = ctx.on('session/event', (session, event) =>
159
+ this.onHostEvent(session, event),
160
+ );
158
161
  const config = this.getConfig();
159
162
  if (config.scanOnBoot) {
160
163
  void this.bootScanLoop();
@@ -226,7 +229,9 @@ export class AutoContinueRunner {
226
229
  }
227
230
 
228
231
  dispose(): void {
232
+ if (this.disposed) return;
229
233
  this.disposed = true;
234
+ this.disposeSessionEvents();
230
235
  for (const state of this.states.values()) {
231
236
  if (state.pendingTimer !== undefined) clearTimeout(state.pendingTimer);
232
237
  if (state.loopRetryTimer !== undefined) clearTimeout(state.loopRetryTimer);
@@ -434,7 +439,11 @@ export class AutoContinueRunner {
434
439
  if (state.loopRetryTimer !== undefined) clearTimeout(state.loopRetryTimer);
435
440
  state.loopRetryTimer = setTimeout(() => {
436
441
  state.loopRetryTimer = undefined;
437
- this.schedule(sessionId, 'loop:aborted');
442
+ try {
443
+ this.schedule(sessionId, 'loop:aborted');
444
+ } catch (error) {
445
+ console.error(`[auto-continue] loop 重启异常 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
446
+ }
438
447
  }, remaining);
439
448
  this.log(`loop 重启延迟 ${remaining}ms(冷却期) ${sessionId}`);
440
449
  } else {
@@ -591,7 +600,11 @@ export class AutoContinueRunner {
591
600
  clearTimeout(state.pendingTimer);
592
601
  state.pendingTimer = undefined;
593
602
  }
594
- await this.fire(sessionId, 'manual:notification', true);
603
+ try {
604
+ await this.fire(sessionId, 'manual:notification', true);
605
+ } catch (error) {
606
+ console.error(`[auto-continue] 手动续跑异常 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
607
+ }
595
608
  }
596
609
 
597
610
  /** 本会话当前生效的冷却间隔(自适应退避)。 */
@@ -628,7 +641,12 @@ export class AutoContinueRunner {
628
641
  const timer = setTimeout(() => {
629
642
  if (state.pendingTimer !== timer) return;
630
643
  state.pendingTimer = undefined;
631
- void this.fire(sessionId, reason);
644
+ // 保险丝: 定时器回调内任何异常( inactive context)都不得成为未捕获异常炸掉进程。
645
+ try {
646
+ void this.fire(sessionId, reason);
647
+ } catch (error) {
648
+ console.error(`[auto-continue] 定时发送异常 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
649
+ }
632
650
  }, config.graceMs);
633
651
  state.pendingTimer = timer;
634
652
  const template = reason.startsWith('loop:')
package/src/index.ts CHANGED
@@ -98,11 +98,19 @@ export function apply(ctx: Context): void {
98
98
  });
99
99
  });
100
100
 
101
+ // 引擎引用: inject 回调可能重入(依赖组合变化), 顶层 effect 在 fiber 卸载时必跑。
102
+ // dispose 绑定必须挂在 apply 的顶层 ctx 上——挂 inject 派生 ctx 的 effect 在 config HMR
103
+ // 替换行时不会执行, 悬空定时器会撞上 inactive context 炸掉整个进程。
104
+ let runnerRef: AutoContinueRunner | undefined;
105
+
101
106
  // 单实例引擎: host 进程内监听会话事件, 所有标签页共享同一个引擎。
102
107
  ctx.inject(['settings', 'agents', 'webServer'], (engineCtx) => {
108
+ // 回调重入时先清理旧引擎, 避免定时器与监听器叠加。
109
+ if (runnerRef !== undefined) runnerRef.dispose();
103
110
  const runner = new AutoContinueRunner(engineCtx, () =>
104
111
  resolveConfig(engineCtx.settings.get(settingsNamespace(AUTO_CONTINUE_NS)) as AutoContinueSettings | undefined),
105
112
  );
113
+ runnerRef = runner;
106
114
 
107
115
  // 状态桥: browser 侧订阅通知与运行时状态(SSE)。
108
116
  const sseClients = new Set<(data: string) => void>();
@@ -178,4 +186,12 @@ export function apply(ctx: Context): void {
178
186
  },
179
187
  });
180
188
  });
189
+
190
+ // 顶层生命周期绑定: fiber 卸载时清理引擎(定时器/监听器)。
191
+ // 挂在 apply 的 ctx 上保证 Cordis 一定会调用, 防止 inactive context 崩溃。
192
+ ctx.effect(() => () => {
193
+ const runner = runnerRef;
194
+ runnerRef = undefined;
195
+ if (runner !== undefined) runner.dispose();
196
+ });
181
197
  }