dsh-client-auto-continue 0.7.5 → 0.8.1

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 CHANGED
@@ -27,1129 +27,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
27
27
  var index_exports = {};
28
28
  __export(index_exports, {
29
29
  apply: () => apply,
30
- fillTemplate: () => fillTemplate,
31
30
  inject: () => inject,
32
- pauseSession: () => pauseSession,
33
31
  pausedSessions: () => pausedSessions,
34
32
  readTodayStats: () => readTodayStats,
35
33
  resetTodayStats: () => resetTodayStats,
36
- sessionPauseUntil: () => sessionPauseUntil,
37
34
  unpauseSession: () => unpauseSession
38
35
  });
39
36
  module.exports = __toCommonJS(index_exports);
40
37
 
41
- // src/client/engine.ts
42
- var DEFAULT_CONFIG = {
43
- continueText: "继续",
44
- continueTextMaxTokens: "继续",
45
- guardTools: true,
46
- guardPendingText: "(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)",
47
- guardDoneText: "(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)",
48
- graceMs: 3e3,
49
- cooldownMs: 2e4,
50
- maxConsecutive: 3,
51
- scanOnBoot: true,
52
- scanLimit: 8,
53
- freshMs: 15 * 60 * 1e3,
54
- reconnectScanDelayMs: 5e3,
55
- reconnectBackoffMs: 3e3,
56
- verbose: true,
57
- classify: true,
58
- backoffFactor: 2,
59
- backoffMaxMs: 3e5,
60
- notify: false,
61
- paused: false,
62
- loopGuard: true,
63
- loopShortChars: 40,
64
- loopWindowMs: 3e4,
65
- loopShortCount: 12,
66
- loopRepeatText: 4,
67
- loopToolRepeat: 5,
68
- loopText: "(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)"
69
- };
70
- function numberOr(value, fallback) {
71
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
72
- }
73
- function booleanOr(value, fallback) {
74
- return typeof value === "boolean" ? value : fallback;
75
- }
76
- function resolveConfig(section) {
77
- const value = section ?? {};
78
- const text = typeof value.continueText === "string" && value.continueText.trim() !== "" ? value.continueText : DEFAULT_CONFIG.continueText;
79
- const maxTokensText = typeof value.continueTextMaxTokens === "string" && value.continueTextMaxTokens.trim() !== "" ? value.continueTextMaxTokens : DEFAULT_CONFIG.continueTextMaxTokens;
80
- const guardPendingText = typeof value.guardPendingText === "string" && value.guardPendingText.trim() !== "" ? value.guardPendingText : DEFAULT_CONFIG.guardPendingText;
81
- const guardDoneText = typeof value.guardDoneText === "string" && value.guardDoneText.trim() !== "" ? value.guardDoneText : DEFAULT_CONFIG.guardDoneText;
82
- return {
83
- continueText: text,
84
- continueTextMaxTokens: maxTokensText,
85
- guardTools: booleanOr(value.guardTools, DEFAULT_CONFIG.guardTools),
86
- guardPendingText,
87
- guardDoneText,
88
- graceMs: numberOr(value.graceMs, DEFAULT_CONFIG.graceMs),
89
- cooldownMs: numberOr(value.cooldownMs, DEFAULT_CONFIG.cooldownMs),
90
- maxConsecutive: Math.max(1, numberOr(value.maxConsecutive, DEFAULT_CONFIG.maxConsecutive)),
91
- scanOnBoot: booleanOr(value.scanOnBoot, DEFAULT_CONFIG.scanOnBoot),
92
- scanLimit: Math.max(1, numberOr(value.scanLimit, DEFAULT_CONFIG.scanLimit)),
93
- freshMs: numberOr(value.freshMs, DEFAULT_CONFIG.freshMs),
94
- reconnectScanDelayMs: numberOr(value.reconnectScanDelayMs, DEFAULT_CONFIG.reconnectScanDelayMs),
95
- reconnectBackoffMs: numberOr(value.reconnectBackoffMs, DEFAULT_CONFIG.reconnectBackoffMs),
96
- verbose: booleanOr(value.verbose, DEFAULT_CONFIG.verbose),
97
- classify: booleanOr(value.classify, DEFAULT_CONFIG.classify),
98
- backoffFactor: Math.max(1, numberOr(value.backoffFactor, DEFAULT_CONFIG.backoffFactor)),
99
- backoffMaxMs: numberOr(value.backoffMaxMs, DEFAULT_CONFIG.backoffMaxMs),
100
- notify: booleanOr(value.notify, DEFAULT_CONFIG.notify),
101
- paused: booleanOr(value.paused, DEFAULT_CONFIG.paused),
102
- loopGuard: booleanOr(value.loopGuard, DEFAULT_CONFIG.loopGuard),
103
- loopShortChars: Math.max(1, numberOr(value.loopShortChars, DEFAULT_CONFIG.loopShortChars)),
104
- loopWindowMs: Math.max(1e3, numberOr(value.loopWindowMs, DEFAULT_CONFIG.loopWindowMs)),
105
- loopShortCount: Math.max(2, numberOr(value.loopShortCount, DEFAULT_CONFIG.loopShortCount)),
106
- loopRepeatText: Math.max(2, numberOr(value.loopRepeatText, DEFAULT_CONFIG.loopRepeatText)),
107
- loopToolRepeat: Math.max(2, numberOr(value.loopToolRepeat, DEFAULT_CONFIG.loopToolRepeat)),
108
- loopText: typeof value.loopText === "string" && value.loopText.trim() !== "" ? value.loopText : DEFAULT_CONFIG.loopText
109
- };
110
- }
111
- function isNonHumanReason(kind) {
112
- return kind === "error" || kind === "interrupted" || kind === "max-tokens";
113
- }
114
- function isTransientFailure(failure) {
115
- const haystack = `${failure.code} ${failure.message}`.toLowerCase();
116
- const status = failure.status;
117
- if (status !== void 0 && (status === 401 || status === 403)) return false;
118
- const permanent = /auth|unauthor|forbidden|credential|api[_-]?key|permission/i.test(haystack) || /insufficient.*(balance|quota)|billing|payment|quota.*exceeded.*(?!retry)/i.test(haystack) || /model.*not[_-]?found|unknown[_-]?model|model[_-]?not[_-]?found|not.*support.*model/i.test(haystack) || /context.*(length|limit|overflow|exceed)|token.*limit|max.*context/i.test(haystack) || /invalid[_-]?request|bad[_-]?request/i.test(haystack);
119
- return !permanent;
120
- }
121
- function isTransientAgentError(message) {
122
- return /network|timeout|timed ?out|econn|etimedout|socket|5\d\d|\b429\b|upstream|temporar/i.test(message);
123
- }
124
- function notify(title, body, options) {
125
- try {
126
- const N = globalThis.Notification;
127
- if (typeof N === "undefined") return;
128
- const permission = N.permission;
129
- const create = () => {
130
- const instance = new N(title, {
131
- body,
132
- ...options?.actions !== void 0 && options.actions.length > 0 ? { actions: options.actions } : {}
133
- });
134
- const target = instance;
135
- target.onclick = () => {
136
- try {
137
- globalThis.focus?.();
138
- } catch {
139
- }
140
- };
141
- if (options?.onAction !== void 0) {
142
- target.onaction = (event) => options.onAction?.(event.action);
143
- }
144
- };
145
- if (permission === "granted") {
146
- create();
147
- } else if (permission === "default") {
148
- void N.requestPermission?.().then((result) => {
149
- if (result === "granted") create();
150
- }).catch(() => {
151
- });
152
- }
153
- } catch {
154
- }
155
- }
156
- function formatElapsed(ms) {
157
- if (ms === void 0 || !Number.isFinite(ms) || ms < 0) return "";
158
- if (ms < 1e3) return `${Math.round(ms)}ms`;
159
- const s = Math.round(ms / 1e3);
160
- if (s < 60) return `${s}s`;
161
- return `${Math.floor(s / 60)}m${s % 60 > 0 ? `${s % 60}s` : ""}`;
162
- }
163
- function fillTemplate(template, ctx) {
164
- return template.replace(/\{code\}/g, ctx.facts?.code ?? "").replace(/\{message\}/g, ctx.facts?.message ?? "").replace(/\{status\}/g, ctx.facts?.status !== void 0 ? String(ctx.facts.status) : "").replace(/\{tool\}/g, ctx.tool ?? "").replace(/\{turn\}/g, ctx.turn !== void 0 ? String(ctx.turn) : "").replace(/\{errorCount\}/g, ctx.errorCount !== void 0 ? String(ctx.errorCount) : "").replace(/\{sessionTitle\}/g, ctx.sessionTitle ?? "").replace(/\{elapsed\}/g, formatElapsed(ctx.elapsedMs)).replace(/\{result\}/g, ctx.result ?? "");
165
- }
166
- var TOOL_RESULT_CAP = 160;
167
- function extractText(blocks, cap) {
168
- let out = "";
169
- const walk = (value) => {
170
- if (out.length >= cap) return;
171
- if (Array.isArray(value)) {
172
- for (const item of value) walk(item);
173
- return;
174
- }
175
- if (typeof value !== "object" || value === null) return;
176
- const record = value;
177
- if (record["type"] === "text" && typeof record["text"] === "string") {
178
- out += record["text"];
179
- return;
180
- }
181
- for (const child of Object.values(record)) walk(child);
182
- };
183
- walk(blocks);
184
- return out.slice(0, cap);
185
- }
186
- function toolResultFacts(data) {
187
- const failed = data.error !== void 0 || data.message?.content?.[0]?.isError === true;
188
- return { ok: !failed, excerpt: extractText(data.message?.content?.[0]?.content, TOOL_RESULT_CAP) };
189
- }
190
- function effectiveCooldown(consecutive, base, factor, max) {
191
- const multiplier = Math.pow(factor, consecutive);
192
- return Math.min(Math.max(base, base * multiplier), Math.max(base, max));
193
- }
194
- function sleep(ms) {
195
- return new Promise((resolve) => setTimeout(resolve, ms));
196
- }
197
- function clientTimeZone() {
198
- try {
199
- return Intl.DateTimeFormat().resolvedOptions().timeZone || void 0;
200
- } catch {
201
- return void 0;
202
- }
203
- }
204
- var lockPrefix = "dsh-auto-continue:";
205
- var lockKey = (sessionId) => `${lockPrefix}lock:${sessionId}`;
206
- var stampKey = (sessionId) => `${lockPrefix}last:${sessionId}`;
207
- var countKey = (sessionId) => `${lockPrefix}count:${sessionId}`;
208
- function readLastSent(sessionId) {
209
- try {
210
- const raw = localStorage.getItem(stampKey(sessionId));
211
- if (raw === null) return { at: 0, text: "" };
212
- const parsed = JSON.parse(raw);
213
- if (typeof parsed === "object" && parsed !== null && typeof parsed.text === "string") {
214
- return { at: Number(parsed.at) || 0, text: parsed.text };
215
- }
216
- return { at: Number(raw) || 0, text: "" };
217
- } catch {
218
- return { at: 0, text: "" };
219
- }
220
- }
221
- function readLastSend(sessionId) {
222
- return readLastSent(sessionId).at;
223
- }
224
- function writeLastSend(sessionId, at, text) {
225
- try {
226
- localStorage.setItem(stampKey(sessionId), JSON.stringify({ at, text }));
227
- } catch {
228
- }
229
- }
230
- var SEND_COUNT_WINDOW_MS = 10 * 60 * 1e3;
231
- function readSendCount(sessionId) {
232
- try {
233
- const raw = localStorage.getItem(countKey(sessionId));
234
- if (raw === null) return { at: 0, count: 0 };
235
- const parsed = JSON.parse(raw);
236
- if (typeof parsed === "object" && parsed !== null && typeof parsed.count === "number") {
237
- const at = Number(parsed.at) || 0;
238
- if (Date.now() - at > SEND_COUNT_WINDOW_MS) return { at: 0, count: 0 };
239
- return { at, count: parsed.count };
240
- }
241
- } catch {
242
- }
243
- return { at: 0, count: 0 };
244
- }
245
- function bumpSendCount(sessionId) {
246
- try {
247
- const current2 = readSendCount(sessionId);
248
- localStorage.setItem(
249
- countKey(sessionId),
250
- JSON.stringify({ at: Date.now(), count: current2.count + 1 })
251
- );
252
- } catch {
253
- }
254
- }
255
- function clearSendCount(sessionId) {
256
- try {
257
- localStorage.removeItem(countKey(sessionId));
258
- } catch {
259
- }
260
- }
261
- async function withSendLock(sessionId, body) {
262
- const nav = globalThis.navigator;
263
- if (nav?.locks !== void 0) {
264
- await nav.locks.request(`dsh-auto-continue:send:${sessionId}`, body);
265
- return;
266
- }
267
- if (!claimSend(sessionId)) return;
268
- try {
269
- await body();
270
- } finally {
271
- releaseSend(sessionId);
272
- }
273
- }
274
- function claimSend(sessionId) {
275
- try {
276
- const token = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
277
- localStorage.setItem(lockKey(sessionId), token);
278
- return localStorage.getItem(lockKey(sessionId)) === token;
279
- } catch {
280
- return true;
281
- }
282
- }
283
- function releaseSend(sessionId) {
284
- try {
285
- localStorage.removeItem(lockKey(sessionId));
286
- } catch {
287
- }
288
- }
289
- var pauseKey = (sessionId) => `${lockPrefix}pause:${sessionId}`;
290
- function pauseSession(sessionId, ms) {
291
- try {
292
- localStorage.setItem(pauseKey(sessionId), String(Date.now() + ms));
293
- } catch {
294
- }
295
- }
296
- function unpauseSession(sessionId) {
297
- try {
298
- localStorage.removeItem(pauseKey(sessionId));
299
- } catch {
300
- }
301
- }
302
- function sessionPauseUntil(sessionId) {
303
- try {
304
- return Number(localStorage.getItem(pauseKey(sessionId)) ?? 0) || 0;
305
- } catch {
306
- return 0;
307
- }
308
- }
309
- function pausedSessions() {
310
- const out = [];
311
- const now = Date.now();
312
- try {
313
- for (let i = 0; i < localStorage.length; i += 1) {
314
- const key = localStorage.key(i);
315
- if (key === null || !key.startsWith(`${lockPrefix}pause:`)) continue;
316
- const sessionId = key.slice(lockPrefix.length + "pause:".length);
317
- const until = Number(localStorage.getItem(key) ?? 0) || 0;
318
- if (until > now) out.push({ sessionId, until });
319
- else localStorage.removeItem(key);
320
- }
321
- } catch {
322
- }
323
- return out;
324
- }
325
- var statsKey = `${lockPrefix}stats`;
326
- var STATS_MAX_DAYS = 90;
327
- function todayKey() {
328
- const d = /* @__PURE__ */ new Date();
329
- const mm = String(d.getMonth() + 1).padStart(2, "0");
330
- const dd = String(d.getDate()).padStart(2, "0");
331
- return `${d.getFullYear()}-${mm}-${dd}`;
332
- }
333
- function readStats() {
334
- try {
335
- const raw = localStorage.getItem(statsKey);
336
- if (raw === null) return [];
337
- const parsed = JSON.parse(raw);
338
- if (!Array.isArray(parsed)) return [];
339
- return parsed.filter(
340
- (item) => typeof item === "object" && item !== null && typeof item.date === "string"
341
- );
342
- } catch {
343
- return [];
344
- }
345
- }
346
- function writeStats(list) {
347
- try {
348
- localStorage.setItem(statsKey, JSON.stringify(list));
349
- } catch {
350
- }
351
- }
352
- function bumpStat(delta) {
353
- const list = readStats();
354
- let day = list.find((item) => item.date === todayKey());
355
- if (day === void 0) {
356
- day = { date: todayKey(), sent: 0, skipped: 0, recovered: 0, failed: 0, gaveUp: 0, looped: 0, byCode: {} };
357
- list.unshift(day);
358
- }
359
- if (delta.sent !== void 0) day.sent += delta.sent;
360
- if (delta.skipped !== void 0) day.skipped += delta.skipped;
361
- if (delta.recovered !== void 0) day.recovered += delta.recovered;
362
- if (delta.failed !== void 0) day.failed += delta.failed;
363
- if (delta.gaveUp !== void 0) day.gaveUp += delta.gaveUp;
364
- if (delta.looped !== void 0) day.looped += delta.looped;
365
- if (delta.code !== void 0) day.byCode[delta.code] = (day.byCode[delta.code] ?? 0) + 1;
366
- writeStats(list.slice(0, STATS_MAX_DAYS));
367
- }
368
- function readTodayStats() {
369
- const today = todayKey();
370
- const found = readStats().find((item) => item.date === today);
371
- return found ?? { date: today, sent: 0, skipped: 0, recovered: 0, failed: 0, gaveUp: 0, looped: 0, byCode: {} };
372
- }
373
- function resetTodayStats() {
374
- writeStats(readStats().filter((item) => item.date !== todayKey()));
375
- }
376
- var freshState = () => ({
377
- consecutive: 0,
378
- lastAutoAt: 0,
379
- lastAttemptAt: 0,
380
- lastSentText: "",
381
- pendingTimer: void 0,
382
- running: void 0,
383
- queued: 0,
384
- subagent: false,
385
- lastFailure: void 0,
386
- lastFailureAt: 0,
387
- lastTool: void 0,
388
- lastToolResult: void 0,
389
- lastTurn: void 0,
390
- pendingRecoveryAt: 0,
391
- shortRun: 0,
392
- lastShortAt: 0,
393
- lastAssistantText: "",
394
- sameTextRun: 0,
395
- toolRun: void 0,
396
- loopFired: false,
397
- loopCancelled: false,
398
- loopRetryTimer: void 0
399
- });
400
- var RECOVERY_WINDOW_MS = 10 * 60 * 1e3;
401
- var ECHO_WINDOW_MS = 10 * 60 * 1e3;
402
- function isOurEcho(state, sessionId, event) {
403
- if (event.type !== "user/message") return false;
404
- const message = event.data;
405
- if (message.source.kind !== "user") return false;
406
- const last = readLastSent(sessionId);
407
- if (last.at === 0 || last.text === "") return false;
408
- if (Date.now() - last.at > ECHO_WINDOW_MS) return false;
409
- const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
410
- return text === last.text;
411
- }
412
- async function pumpStream(open, onFrame, onReconnect, getBackoff, log, signal) {
413
- let backoff = getBackoff();
414
- while (!signal.aborted) {
415
- let connected = false;
416
- try {
417
- for await (const envelope of open(signal)) {
418
- connected = true;
419
- onFrame(envelope.payload);
420
- }
421
- if (signal.aborted) return;
422
- } catch (error) {
423
- if (signal.aborted) return;
424
- log(`stream error: ${error instanceof Error ? error.message : String(error)}`);
425
- }
426
- if (!connected) {
427
- await sleep(backoff);
428
- backoff = Math.min(backoff * 2, 15e3);
429
- continue;
430
- }
431
- backoff = getBackoff();
432
- onReconnect();
433
- await sleep(backoff);
434
- }
435
- }
436
- var AutoContinueRunner = class {
437
- /**
438
- * @param api - shared wire client (ctx.connection.api).
439
- * @param getConfig - read the current resolved configuration (settings scope).
440
- */
441
- constructor(api, getConfig) {
442
- this.api = api;
443
- this.getConfig = getConfig;
444
- this.states = /* @__PURE__ */ new Map();
445
- this.muxAbort = new AbortController();
446
- this.hostAbort = new AbortController();
447
- this.disposed = false;
448
- this.reconnectScans = 0;
449
- /** 会话标题缓存(来自 session.list 投影, {sessionTitle} 占位符用)。 */
450
- this.titles = /* @__PURE__ */ new Map();
451
- const config = this.getConfig();
452
- void this.runMux();
453
- void this.runHost();
454
- if (config.scanOnBoot) {
455
- void this.bootScanLoop();
456
- }
457
- this.log(
458
- `已启动(文本="${config.continueText}", 宽限 ${config.graceMs}ms, 冷却 ${config.cooldownMs}ms, 最多连续 ${config.maxConsecutive} 次)`
459
- );
460
- }
461
- log(message) {
462
- if (this.getConfig().verbose) console.info(`[auto-continue] ${message}`);
463
- }
464
- dispose() {
465
- this.disposed = true;
466
- this.muxAbort.abort();
467
- this.hostAbort.abort();
468
- for (const state of this.states.values()) {
469
- if (state.pendingTimer !== void 0) clearTimeout(state.pendingTimer);
470
- if (state.loopRetryTimer !== void 0) clearTimeout(state.loopRetryTimer);
471
- }
472
- this.states.clear();
473
- }
474
- state(sessionId) {
475
- let state = this.states.get(sessionId);
476
- if (state === void 0) {
477
- state = freshState();
478
- this.states.set(sessionId, state);
479
- }
480
- return state;
481
- }
482
- runMux() {
483
- return pumpStream(
484
- (signal) => this.api.events.mux({}, signal),
485
- (payload) => this.onMuxFrame(payload),
486
- () => this.scheduleReconnectScan(),
487
- () => this.getConfig().reconnectBackoffMs,
488
- (m) => this.log(m),
489
- this.muxAbort.signal
490
- );
491
- }
492
- runHost() {
493
- return pumpStream(
494
- (signal) => this.api.events.host({}, signal),
495
- (payload) => this.onHostFrame(payload),
496
- () => this.scheduleReconnectScan(),
497
- () => this.getConfig().reconnectBackoffMs,
498
- (m) => this.log(m),
499
- this.hostAbort.signal
500
- );
501
- }
502
- // ---------- mux 帧 ----------
503
- onMuxFrame(frame) {
504
- switch (frame.type) {
505
- case "session/event":
506
- if (frame.event.type === "tool/call") {
507
- const name = frame.event.data.name;
508
- if (typeof name === "string") {
509
- const state = this.state(frame.sessionId);
510
- state.lastTool = name;
511
- state.lastToolResult = "pending";
512
- state.shortRun = 0;
513
- const key = `${name}
514
- ${frame.event.data.arguments}`;
515
- if (state.toolRun?.key === key) {
516
- state.toolRun.waiting = true;
517
- } else {
518
- state.toolRun = { key, count: 1, lastResult: void 0, waiting: false };
519
- }
520
- }
521
- } else if (frame.event.type === "tool/result") {
522
- const state = this.state(frame.sessionId);
523
- if (state.lastToolResult === "pending") {
524
- const facts = toolResultFacts(frame.event.data);
525
- state.lastToolResult = facts;
526
- const run = state.toolRun;
527
- if (run !== void 0 && run.waiting) {
528
- run.waiting = false;
529
- if (run.lastResult !== void 0 && run.lastResult === facts.excerpt) {
530
- run.count += 1;
531
- this.checkLoop(frame.sessionId, state);
532
- } else {
533
- run.lastResult = facts.excerpt;
534
- run.count = 1;
535
- }
536
- } else if (run !== void 0 && !run.waiting) {
537
- run.lastResult = facts.excerpt;
538
- }
539
- }
540
- } else if (frame.event.type === "assistant/message") {
541
- const state = this.state(frame.sessionId);
542
- this.onAssistantMessage(frame.sessionId, state, frame.event);
543
- }
544
- this.onSessionEvent(frame.sessionId, frame.event);
545
- break;
546
- case "session/queue":
547
- this.state(frame.sessionId).queued = frame.items.length;
548
- if (frame.items.length > 0) this.cancelPending(frame.sessionId, "出现排队消息");
549
- break;
550
- case "stream/error":
551
- this.log(`mux stream/error: ${frame.error.code} ${frame.error.message}`);
552
- break;
553
- default:
554
- break;
555
- }
556
- }
557
- /** 从 assistant/message 事件提取纯文本。 */
558
- assistantText(event) {
559
- const content = event.data.message.content;
560
- if (!Array.isArray(content)) return "";
561
- return content.filter((part) => part.type === "text").map((part) => part.text).join("");
562
- }
563
- /**
564
- * loop guard 信号 1(空转): 时间窗内连续短句且期间无工具调用。
565
- * 短句 = 模型消息文本短于 loopShortChars; 长句、工具调用、或短句间隔超过
566
- * loopWindowMs(正常思考的短文本散布在长时间里)都会重置计数。
567
- */
568
- onAssistantMessage(sessionId, state, event) {
569
- if (!this.getConfig().loopGuard) return;
570
- const text = this.assistantText(event);
571
- const trimmed = text.trim();
572
- if (trimmed !== "" && trimmed === state.lastAssistantText) {
573
- state.sameTextRun += 1;
574
- } else {
575
- state.lastAssistantText = trimmed;
576
- state.sameTextRun = 1;
577
- }
578
- if (trimmed.length < this.getConfig().loopShortChars) {
579
- const now = Date.now();
580
- if (now - state.lastShortAt > this.getConfig().loopWindowMs) {
581
- state.shortRun = 0;
582
- }
583
- state.shortRun += 1;
584
- state.lastShortAt = now;
585
- } else {
586
- state.shortRun = 0;
587
- state.lastShortAt = 0;
588
- }
589
- this.checkLoop(sessionId, state);
590
- }
591
- /** 两个循环信号的公共检查; 命中且本回合未打断过则打断。 */
592
- checkLoop(sessionId, state) {
593
- if (!this.getConfig().loopGuard) return;
594
- if (state.loopFired) return;
595
- if (!state.running) return;
596
- const config = this.getConfig();
597
- if (state.sameTextRun >= config.loopRepeatText) {
598
- this.log(`检测到空转循环 ${sessionId}: 连续 ${state.sameTextRun} 条相同消息`);
599
- void this.interruptLoop(sessionId, state);
600
- } else if (state.shortRun >= config.loopShortCount) {
601
- this.log(`检测到空转循环 ${sessionId}: 连续 ${state.shortRun} 条短句且无工具调用`);
602
- void this.interruptLoop(sessionId, state);
603
- } else if (state.toolRun !== void 0 && state.toolRun.count >= config.loopToolRepeat) {
604
- const toolName = state.toolRun.key.split("\n")[0] ?? "?";
605
- this.log(`检测到工具死循环 ${sessionId}: 「${toolName}」连续 ${state.toolRun.count} 次(同参数同结果)`);
606
- void this.interruptLoop(sessionId, state);
607
- }
608
- }
609
- /**
610
- * 打断运行中的回合: cancel(带来源标记)+ 进冷却。
611
- * 随后的 turn/end aborted 会因 loopCancelled 走「可恢复中断」路径,
612
- * 用 loopText 重启回合——不会与用户手动停止混淆。
613
- */
614
- async interruptLoop(sessionId, state) {
615
- if (state.loopFired) return;
616
- if (Date.now() - state.lastAttemptAt < this.cooldownFor(state)) {
617
- this.log(`跳过循环打断 ${sessionId}: 处于冷却期`);
618
- return;
619
- }
620
- state.loopFired = true;
621
- state.loopCancelled = true;
622
- state.lastAttemptAt = Date.now();
623
- bumpStat({ looped: 1 });
624
- try {
625
- const response = await this.api.sessions.cancel({ sessionId });
626
- this.log(
627
- `已打断循环 ${sessionId}: ${response.result.ok ? "cancel 已受理" : "cancel 被拒绝"}`
628
- );
629
- } catch (error) {
630
- this.log(`打断循环失败 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
631
- state.loopCancelled = false;
632
- }
633
- }
634
- onSessionEvent(sessionId, event) {
635
- const state = this.state(sessionId);
636
- switch (event.type) {
637
- case "turn/start":
638
- state.running = true;
639
- state.lastTool = void 0;
640
- state.lastToolResult = void 0;
641
- state.shortRun = 0;
642
- state.lastShortAt = 0;
643
- state.lastAssistantText = "";
644
- state.sameTextRun = 0;
645
- state.toolRun = void 0;
646
- state.loopFired = false;
647
- state.loopCancelled = false;
648
- if (state.loopRetryTimer !== void 0) {
649
- clearTimeout(state.loopRetryTimer);
650
- state.loopRetryTimer = void 0;
651
- }
652
- this.cancelPending(sessionId, "宿主自行开启新回合");
653
- break;
654
- case "turn/end": {
655
- state.running = false;
656
- this.cancelPending(sessionId, "收到新的 turn/end");
657
- const reason = event.data.reason;
658
- if (reason.kind === "completed") {
659
- state.consecutive = 0;
660
- state.lastFailure = void 0;
661
- clearSendCount(sessionId);
662
- this.noteRecovery(sessionId, "completed");
663
- } else if (reason.kind === "aborted") {
664
- if (state.loopCancelled) {
665
- state.loopCancelled = false;
666
- state.loopFired = false;
667
- state.pendingRecoveryAt = 0;
668
- state.shortRun = 0;
669
- state.lastShortAt = 0;
670
- state.lastAssistantText = "";
671
- state.sameTextRun = 0;
672
- state.toolRun = void 0;
673
- const cooldown = this.cooldownFor(state);
674
- const remaining = cooldown - (Date.now() - state.lastAttemptAt);
675
- if (remaining > 0) {
676
- if (state.loopRetryTimer !== void 0) clearTimeout(state.loopRetryTimer);
677
- state.loopRetryTimer = setTimeout(() => {
678
- state.loopRetryTimer = void 0;
679
- this.schedule(sessionId, "loop:aborted");
680
- }, remaining);
681
- this.log(`loop 重启延迟 ${remaining}ms(冷却期) ${sessionId}`);
682
- } else {
683
- this.schedule(sessionId, "loop:aborted");
684
- }
685
- } else {
686
- state.consecutive = 0;
687
- state.pendingRecoveryAt = 0;
688
- clearSendCount(sessionId);
689
- }
690
- } else if (reason.kind === "blocked") {
691
- } else if (reason.kind === "interrupted") {
692
- state.consecutive = 0;
693
- state.pendingRecoveryAt = 0;
694
- } else if (reason.kind === "error") {
695
- const error = reason.error;
696
- state.lastFailure = {
697
- code: typeof error.code === "string" ? error.code : "UNKNOWN",
698
- message: typeof error.message === "string" ? error.message : String(error),
699
- ...typeof error.status === "number" ? { status: error.status } : {}
700
- };
701
- state.lastTurn = event.data.turn;
702
- state.lastFailureAt = Date.now();
703
- this.noteRecovery(sessionId, "error");
704
- this.onTurnFailure(sessionId, "turn/end:error", state.lastFailure);
705
- } else if (reason.kind === "max-tokens") {
706
- state.lastFailureAt = Date.now();
707
- this.noteRecovery(sessionId, "error");
708
- this.schedule(sessionId, "turn/end:max-tokens");
709
- }
710
- break;
711
- }
712
- case "user/message":
713
- if (isOurEcho(state, sessionId, event)) break;
714
- if (event.data.source.kind === "user") {
715
- state.consecutive = 0;
716
- clearSendCount(sessionId);
717
- this.cancelPending(sessionId, "用户手动发送消息");
718
- }
719
- break;
720
- default:
721
- break;
722
- }
723
- }
724
- // ---------- host 帧 ----------
725
- onHostFrame(frame) {
726
- switch (frame.type) {
727
- case "host/session-status":
728
- this.state(frame.sessionId).running = frame.running;
729
- if (frame.running) this.cancelPending(frame.sessionId, "宿主报告会话开始运行");
730
- break;
731
- case "host/session-added":
732
- this.state(frame.sessionId).subagent = frame.parentSessionId !== void 0;
733
- break;
734
- case "host/agent-error":
735
- if (this.state(frame.sessionId).subagent) break;
736
- this.log(`host/agent-error(${frame.sessionId}): ${frame.message}`);
737
- if (!isTransientAgentError(frame.message)) {
738
- this.log(`跳过 ${frame.sessionId}: 永久性 agent 错误 — ${frame.message}`);
739
- bumpStat({ skipped: 1 });
740
- if (this.getConfig().notify) {
741
- notify(
742
- "dsh-auto-continue: 未自动继续",
743
- `${frame.sessionId}: 永久性 agent 错误 ${frame.message.slice(0, 120)}`,
744
- this.notifyOptions(frame.sessionId)
745
- );
746
- }
747
- break;
748
- }
749
- this.schedule(frame.sessionId, "host/agent-error");
750
- break;
751
- case "host/session-removed":
752
- this.cancelPending(frame.sessionId, "会话已移除");
753
- this.states.delete(frame.sessionId);
754
- break;
755
- default:
756
- break;
757
- }
758
- }
759
- // ---------- 调度 ----------
760
- /** 回合失败入口: 先做错误分类, 永久性失败跳过并通知, 临时性失败走正常调度。 */
761
- onTurnFailure(sessionId, reason, failure) {
762
- const config = this.getConfig();
763
- if (config.classify && !isTransientFailure(failure)) {
764
- const summary = `${failure.code}${failure.status !== void 0 ? ` (HTTP ${failure.status})` : ""}`;
765
- this.log(`跳过 ${sessionId}(${reason}): 永久性失败 ${summary} — ${failure.message}`);
766
- bumpStat({ skipped: 1, code: failure.code });
767
- if (config.notify) {
768
- notify(
769
- "dsh-auto-continue: 未自动继续",
770
- `${sessionId}: 永久性错误 ${summary},需要人工处理`,
771
- this.notifyOptions(sessionId)
772
- );
773
- }
774
- return;
775
- }
776
- this.schedule(sessionId, reason);
777
- }
778
- /** 通知操作按钮与回调(「立即续跑」/「暂停该会话 1 小时」)。 */
779
- notifyOptions(sessionId) {
780
- return {
781
- actions: [
782
- { action: "resume", title: "立即续跑" },
783
- { action: "pause1h", title: "暂停该会话 1 小时" }
784
- ],
785
- onAction: (action) => this.onNotifyAction(sessionId, action)
786
- };
787
- }
788
- onNotifyAction(sessionId, action) {
789
- if (action === "resume") {
790
- this.log(`通知按钮: 立即续跑 ${sessionId}`);
791
- void this.resumeNow(sessionId);
792
- } else if (action === "pause1h") {
793
- this.log(`通知按钮: 暂停 ${sessionId} 1 小时`);
794
- pauseSession(sessionId, 60 * 60 * 1e3);
795
- this.cancelPending(sessionId, "通知按钮暂停该会话");
796
- }
797
- }
798
- /** 恢复结果记账: 自动发送后窗口内的回合结束, 判定恢复成功或失败。 */
799
- noteRecovery(sessionId, outcome) {
800
- const state = this.state(sessionId);
801
- if (state.pendingRecoveryAt === 0) return;
802
- if (Date.now() - state.pendingRecoveryAt > RECOVERY_WINDOW_MS) {
803
- state.pendingRecoveryAt = 0;
804
- return;
805
- }
806
- state.pendingRecoveryAt = 0;
807
- bumpStat(outcome === "completed" ? { recovered: 1 } : { failed: 1 });
808
- this.log(`恢复结果(${sessionId}): ${outcome === "completed" ? "成功" : "失败"}`);
809
- }
810
- /** 立即为该会话发送一次自动继续(无视冷却与连续上限; 由通知按钮触发)。 */
811
- async resumeNow(sessionId) {
812
- if (this.disposed) return;
813
- const state = this.state(sessionId);
814
- if (state.subagent) return;
815
- if (state.pendingTimer !== void 0) {
816
- clearTimeout(state.pendingTimer);
817
- state.pendingTimer = void 0;
818
- }
819
- await this.fire(sessionId, "manual:notification", true);
820
- }
821
- /** 本会话当前生效的冷却间隔(自适应退避)。 */
822
- cooldownFor(state) {
823
- const config = this.getConfig();
824
- return effectiveCooldown(
825
- state.consecutive,
826
- config.cooldownMs,
827
- config.backoffFactor,
828
- config.backoffMaxMs
829
- );
830
- }
831
- schedule(sessionId, reason) {
832
- const state = this.state(sessionId);
833
- const config = this.getConfig();
834
- if (state.subagent) return;
835
- if (config.paused) {
836
- this.log(`跳过 ${sessionId}(${reason}): 全局暂停中`);
837
- return;
838
- }
839
- if (Date.now() < sessionPauseUntil(sessionId)) {
840
- this.log(`跳过 ${sessionId}(${reason}): 会话暂停中`);
841
- return;
842
- }
843
- if (state.pendingTimer !== void 0) return;
844
- if (Date.now() - state.lastAttemptAt < this.cooldownFor(state)) return;
845
- if (state.consecutive >= config.maxConsecutive) {
846
- this.log(
847
- `跳过 ${sessionId}(${reason}): 已连续自动继续 ${state.consecutive} 次, 等待用户介入或成功回合`
848
- );
849
- return;
850
- }
851
- if (state.queued > 0) return;
852
- const timer = setTimeout(() => {
853
- if (state.pendingTimer !== timer) return;
854
- state.pendingTimer = void 0;
855
- void this.fire(sessionId, reason);
856
- }, config.graceMs);
857
- state.pendingTimer = timer;
858
- const template = reason.startsWith("loop:") ? config.loopText : reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
859
- this.log(
860
- `检测到非人为中断 ${sessionId}(${reason}), ${config.graceMs}ms 后自动发送「${template}」`
861
- );
862
- }
863
- cancelPending(sessionId, why) {
864
- const state = this.state(sessionId);
865
- if (state.pendingTimer === void 0) return;
866
- clearTimeout(state.pendingTimer);
867
- state.pendingTimer = void 0;
868
- this.log(`取消 ${sessionId} 的自动继续(${why})`);
869
- }
870
- async fire(sessionId, reason, force = false) {
871
- if (this.disposed) return;
872
- const state = this.state(sessionId);
873
- const config = this.getConfig();
874
- if (state.running === void 0) {
875
- const running = await this.runningViaList(sessionId);
876
- if (running === void 0 || running) {
877
- this.log(`跳过 ${sessionId}: 无法确认空闲(${running === void 0 ? "未知" : "运行中"})`);
878
- return;
879
- }
880
- } else if (state.running) {
881
- this.log(`跳过 ${sessionId}: 会话仍在运行`);
882
- return;
883
- }
884
- if (state.queued > 0) {
885
- this.log(`跳过 ${sessionId}: 已有排队消息`);
886
- return;
887
- }
888
- if (!force && readSendCount(sessionId).count >= config.maxConsecutive) {
889
- this.log(`跳过 ${sessionId}: 发送计数已达上限 ${config.maxConsecutive}, 等待用户介入或成功回合`);
890
- return;
891
- }
892
- const template = reason.startsWith("loop:") ? config.loopText : reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
893
- let sessionTitle;
894
- if (template.includes("{sessionTitle}")) {
895
- sessionTitle = this.titles.get(sessionId);
896
- if (sessionTitle === void 0) {
897
- const info = await this.fetchSessionInfo(sessionId);
898
- sessionTitle = info?.title;
899
- }
900
- }
901
- const text = this.buildContinueText(config, state, template, sessionTitle);
902
- const zone = clientTimeZone();
903
- await withSendLock(sessionId, async () => {
904
- if (this.disposed) return;
905
- if (state.queued > 0) {
906
- this.log(`跳过 ${sessionId}: 已有排队消息`);
907
- return;
908
- }
909
- if (!force && Date.now() - readLastSend(sessionId) < this.cooldownFor(state)) {
910
- this.log(`跳过 ${sessionId}: 其他标签页刚发送过`);
911
- return;
912
- }
913
- if (!force && readSendCount(sessionId).count >= config.maxConsecutive) {
914
- this.log(`跳过 ${sessionId}: 发送计数已达上限 ${config.maxConsecutive}, 等待用户介入或成功回合`);
915
- return;
916
- }
917
- if (!force && await this.hostHasPendingSameText(sessionId, text)) {
918
- this.log(`跳过 ${sessionId}: 宿主队列里已有相同文本消息在排队`);
919
- return;
920
- }
921
- state.lastAttemptAt = Date.now();
922
- try {
923
- const response = await this.api.sessions.prompt({
924
- sessionId,
925
- mode: "queue",
926
- content: [{ type: "text", text }],
927
- ...zone === void 0 ? {} : { clientTimeZone: zone }
928
- });
929
- if (response.result.ok) {
930
- const now = Date.now();
931
- state.consecutive += 1;
932
- state.lastAutoAt = now;
933
- state.lastSentText = text;
934
- state.pendingRecoveryAt = now;
935
- writeLastSend(sessionId, now, text);
936
- bumpSendCount(sessionId);
937
- bumpStat({ sent: 1, ...state.lastFailure !== void 0 ? { code: state.lastFailure.code } : {} });
938
- this.log(`已自动发送「${text}」到 ${sessionId}(${reason}), 第 ${state.consecutive} 次连续`);
939
- if (config.notify) {
940
- notify(
941
- "dsh-auto-continue: 已自动继续",
942
- `${sessionId}: 已发送「${text}」(第 ${state.consecutive} 次连续)`,
943
- this.notifyOptions(sessionId)
944
- );
945
- }
946
- if (state.consecutive >= config.maxConsecutive) {
947
- bumpStat({ gaveUp: 1 });
948
- this.log(`达到连续上限 ${config.maxConsecutive} 次, 停止自动继续 ${sessionId}`);
949
- if (config.notify) {
950
- notify(
951
- "dsh-auto-continue: 已停止自动继续",
952
- `${sessionId}: 连续失败 ${state.consecutive} 次, 需要人工介入`,
953
- this.notifyOptions(sessionId)
954
- );
955
- }
956
- }
957
- } else {
958
- this.log(
959
- `发送失败 ${sessionId}: ${response.result.error.code} ${response.result.error.message}`
960
- );
961
- }
962
- } catch (error) {
963
- this.log(`发送异常 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
964
- }
965
- });
966
- }
967
- /**
968
- * 组装本次续跑消息: 模板填充 + 幂等护栏。
969
- * 护栏依据上一步工具调用的执行状态附加指引, 防止重跑副作用操作:
970
- * - 结果未确认(可能已部分执行)→ 提示先确认状态、不要重复执行
971
- * - 已确认成功 → 提示已完成、不要重复执行
972
- * - 已失败 → 不加护栏(重试工具本来就是目的)
973
- */
974
- buildContinueText(config, state, template, sessionTitle) {
975
- let text = fillTemplate(template, {
976
- facts: state.lastFailure,
977
- tool: state.lastTool,
978
- turn: state.lastTurn,
979
- errorCount: state.consecutive + 1,
980
- sessionTitle,
981
- elapsedMs: state.lastFailureAt > 0 ? Date.now() - state.lastFailureAt : void 0
982
- });
983
- if (!config.guardTools) return text;
984
- const guard = this.currentGuard(state);
985
- if (guard.kind === "pending") {
986
- text += ` ${fillTemplate(config.guardPendingText, { tool: guard.tool, result: guard.result })}`;
987
- } else if (guard.kind === "done") {
988
- text += ` ${fillTemplate(config.guardDoneText, { tool: guard.tool, result: guard.result })}`;
989
- }
990
- return text;
991
- }
992
- /** 上一步工具调用的护栏状态(实时路径, 由 mux 帧维护)。 */
993
- currentGuard(state) {
994
- if (state.lastTool === void 0 || state.lastToolResult === void 0) return { kind: "none" };
995
- if (state.lastToolResult === "pending") return { kind: "pending", tool: state.lastTool };
996
- if (state.lastToolResult.ok) {
997
- return { kind: "done", tool: state.lastTool, result: state.lastToolResult.excerpt };
998
- }
999
- return { kind: "failed", tool: state.lastTool };
1000
- }
1001
- /**
1002
- * 宿主权威兜底: 历史里最后一条事件是否就是同一文本的 user 消息。
1003
- * 是 = 它还在排队未被处理, 不应再叠加发送; 否(回合结束等其他事件)= 放行。
1004
- * 查询失败时返回 false(放行, 本地防线仍在)。
1005
- */
1006
- async hostHasPendingSameText(sessionId, text) {
1007
- try {
1008
- const response = await this.api.sessions.history({ sessionId, maxMessages: 10 });
1009
- if (!response.result.ok) return false;
1010
- const events = response.result.value.events;
1011
- const last = events[events.length - 1]?.event;
1012
- if (last === void 0 || last.type !== "user/message") return false;
1013
- if (last.data.source?.kind !== "user") return false;
1014
- const lastText = (last.data.content ?? []).filter((part) => part.type === "text").map((part) => part.text).join("");
1015
- return lastText === text;
1016
- } catch {
1017
- return false;
1018
- }
1019
- }
1020
- /** 查一次 session.list, 顺带缓存该会话的标题。 */
1021
- async fetchSessionInfo(sessionId) {
1022
- try {
1023
- const response = await this.api.sessions.list({});
1024
- if (!response.result.ok) return void 0;
1025
- const item = response.result.value.items.find(
1026
- (summary) => summary.sessionId === sessionId
1027
- );
1028
- if (item === void 0) return void 0;
1029
- const title = item.projections?.values?.title;
1030
- if (typeof title === "string" && title !== "") this.titles.set(sessionId, title);
1031
- return { running: item.running, title: typeof title === "string" ? title : void 0 };
1032
- } catch {
1033
- return void 0;
1034
- }
1035
- }
1036
- async runningViaList(sessionId) {
1037
- const info = await this.fetchSessionInfo(sessionId);
1038
- return info?.running;
1039
- }
1040
- // ---------- 启动/重连扫描 ----------
1041
- scheduleReconnectScan() {
1042
- this.reconnectScans += 1;
1043
- const scan = this.reconnectScans;
1044
- setTimeout(() => {
1045
- if (scan !== this.reconnectScans || this.disposed) return;
1046
- void this.scanLoop(6, this.getConfig().reconnectScanDelayMs);
1047
- }, this.getConfig().reconnectScanDelayMs);
1048
- }
1049
- async bootScanLoop() {
1050
- await this.scanLoop(Infinity, 3e3);
1051
- }
1052
- /** 反复尝试扫描, 直到成功(宿主就绪)或达到次数上限。 */
1053
- async scanLoop(attempts, delayMs) {
1054
- for (let attempt = 0; attempt < attempts && !this.disposed; attempt += 1) {
1055
- try {
1056
- if (await this.scanInterrupted()) return;
1057
- } catch (error) {
1058
- if (this.disposed) return;
1059
- if (attempt % 10 === 0) {
1060
- this.log(
1061
- `扫描失败(${attempt + 1}/${attempts === Infinity ? "∞" : attempts}): ${error instanceof Error ? error.message : String(error)}`
1062
- );
1063
- }
1064
- }
1065
- if (attempt + 1 < attempts) await sleep(delayMs);
1066
- }
1067
- }
1068
- /**
1069
- * 扫描最近中断过的会话: 最后回合以非人为原因结束, 且其后没有新回合或用户消息。
1070
- * @returns 是否成功完成一次扫描(宿主就绪)。
1071
- */
1072
- async scanInterrupted() {
1073
- const config = this.getConfig();
1074
- if (config.paused) return true;
1075
- const response = await this.api.sessions.list({});
1076
- if (!response.result.ok) return false;
1077
- const items = response.result.value.items;
1078
- for (const summary of items) {
1079
- const title = summary.projections?.values?.title;
1080
- if (typeof title === "string" && title !== "") this.titles.set(summary.sessionId, title);
1081
- }
1082
- const candidates = items.filter((summary) => !summary.running && summary.parentSessionId === void 0).slice(0, config.scanLimit);
1083
- const now = Date.now();
1084
- for (const summary of candidates) {
1085
- if (this.disposed) return true;
1086
- const state = this.state(summary.sessionId);
1087
- if (state.pendingTimer !== void 0) continue;
1088
- if (state.consecutive >= config.maxConsecutive) continue;
1089
- if (now - state.lastAttemptAt < this.cooldownFor(state)) continue;
1090
- if (now < sessionPauseUntil(summary.sessionId)) continue;
1091
- let events;
1092
- try {
1093
- const page = await this.api.sessions.history({
1094
- sessionId: summary.sessionId,
1095
- maxMessages: 30
1096
- });
1097
- if (!page.result.ok) continue;
1098
- events = page.result.value.events;
1099
- } catch {
1100
- continue;
1101
- }
1102
- let lastEnd;
1103
- for (let i = events.length - 1; i >= 0; i -= 1) {
1104
- const event = events[i]?.event;
1105
- if (event !== void 0 && event.type === "turn/end") {
1106
- lastEnd = event;
1107
- break;
1108
- }
1109
- }
1110
- if (lastEnd === void 0) continue;
1111
- const reason = lastEnd.data.reason;
1112
- if (!isNonHumanReason(reason.kind)) continue;
1113
- if (lastEnd.time < now - config.freshMs) continue;
1114
- let superseded = false;
1115
- for (const entry of events) {
1116
- const event = entry.event;
1117
- if (event.seq <= lastEnd.seq) continue;
1118
- if (event.type === "turn/start") superseded = true;
1119
- if (event.type === "user/message" && event.data.source.kind === "user") superseded = true;
1120
- if (superseded) break;
1121
- }
1122
- if (superseded) continue;
1123
- this.applyGuardFromEvents(state, events, lastEnd.seq);
1124
- this.log(`扫描发现中断 ${summary.sessionId}(turn/end:${reason.kind}), 安排自动继续`);
1125
- this.schedule(summary.sessionId, `scan:turn/end:${reason.kind}`);
1126
- }
1127
- return true;
1128
- }
1129
- /** 从历史事件恢复上一步工具调用状态(扫描路径的幂等护栏)。 */
1130
- applyGuardFromEvents(state, events, untilSeq) {
1131
- state.lastTool = void 0;
1132
- state.lastToolResult = void 0;
1133
- let call;
1134
- for (const entry of events) {
1135
- const event = entry.event;
1136
- if (event.seq >= untilSeq) continue;
1137
- if (event.type === "tool/call") call = event;
1138
- }
1139
- if (call === void 0) return;
1140
- state.lastTool = call.data.name;
1141
- state.lastToolResult = "pending";
1142
- for (const entry of events) {
1143
- const event = entry.event;
1144
- if (event.seq <= call.seq || event.seq >= untilSeq) continue;
1145
- if (event.type === "tool/result") {
1146
- state.lastToolResult = toolResultFacts(event.data);
1147
- break;
1148
- }
1149
- }
1150
- }
1151
- };
1152
-
1153
38
  // src/client/locales.ts
1154
39
  var zh = {
1155
40
  "card.title": "自动继续",
@@ -1178,10 +63,6 @@ var zh = {
1178
63
  "field.scanLimitHint": "最多检查多少个最近更新的会话(不含运行中与子代理会话)。",
1179
64
  "field.freshMs": "扫描时间窗 (ms)",
1180
65
  "field.freshMsHint": "扫描只处理该时间窗内的中断。",
1181
- "field.reconnectScanDelayMs": "重连扫描延迟 (ms)",
1182
- "field.reconnectScanDelayMsHint": "重连后等待宿主完成恢复再扫描。",
1183
- "field.reconnectBackoffMs": "重连退避 (ms)",
1184
- "field.reconnectBackoffMsHint": "事件流断开后的重连间隔。",
1185
66
  "field.verbose": "详细日志",
1186
67
  "field.verboseHint": "在浏览器控制台输出 [auto-continue] 日志。",
1187
68
  "field.classify": "错误分类",
@@ -1263,10 +144,6 @@ var en = {
1263
144
  "field.scanLimitHint": "How many most-recently-updated sessions to check (running / subagent sessions excluded).",
1264
145
  "field.freshMs": "Scan window (ms)",
1265
146
  "field.freshMsHint": "Only interruptions inside this window are considered.",
1266
- "field.reconnectScanDelayMs": "Reconnect scan delay (ms)",
1267
- "field.reconnectScanDelayMsHint": "Wait for the host to finish recovering before scanning after a reconnect.",
1268
- "field.reconnectBackoffMs": "Reconnect backoff (ms)",
1269
- "field.reconnectBackoffMsHint": "Interval between event-stream reconnect attempts.",
1270
147
  "field.verbose": "Verbose logs",
1271
148
  "field.verboseHint": "Log [auto-continue] lines to the browser console.",
1272
149
  "field.classify": "Classify errors",
@@ -1326,6 +203,165 @@ var en = {
1326
203
  var import_react = require("react");
1327
204
  var import_client = require("@deepseek-ai/dsh-client-runtime/client");
1328
205
 
206
+ // src/shared/core.ts
207
+ var DEFAULT_CONFIG = {
208
+ continueText: "继续",
209
+ continueTextMaxTokens: "继续",
210
+ guardTools: true,
211
+ guardPendingText: "(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)",
212
+ guardDoneText: "(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)",
213
+ graceMs: 3e3,
214
+ cooldownMs: 2e4,
215
+ maxConsecutive: 3,
216
+ scanOnBoot: true,
217
+ scanLimit: 8,
218
+ freshMs: 15 * 60 * 1e3,
219
+ verbose: true,
220
+ classify: true,
221
+ backoffFactor: 2,
222
+ backoffMaxMs: 3e5,
223
+ notify: false,
224
+ paused: false,
225
+ loopGuard: true,
226
+ loopShortChars: 40,
227
+ loopWindowMs: 3e4,
228
+ loopShortCount: 12,
229
+ loopRepeatText: 4,
230
+ loopToolRepeat: 5,
231
+ loopText: "(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)"
232
+ };
233
+ var RECOVERY_WINDOW_MS = 10 * 60 * 1e3;
234
+ var ECHO_WINDOW_MS = 10 * 60 * 1e3;
235
+
236
+ // src/client/bridge.ts
237
+ var EMPTY_STATS = {
238
+ date: "",
239
+ sent: 0,
240
+ skipped: 0,
241
+ recovered: 0,
242
+ failed: 0,
243
+ gaveUp: 0,
244
+ looped: 0,
245
+ byCode: {}
246
+ };
247
+ var state = { stats: EMPTY_STATS, paused: [] };
248
+ var listeners = /* @__PURE__ */ new Set();
249
+ function pausedSessions() {
250
+ return state.paused;
251
+ }
252
+ function readTodayStats() {
253
+ return state.stats;
254
+ }
255
+ function resetTodayStats() {
256
+ void postAction({ action: "reset-stats" });
257
+ }
258
+ function unpauseSession(sessionId) {
259
+ void postAction({ action: "unpause", sessionId });
260
+ }
261
+ function subscribeBridge(listener) {
262
+ listeners.add(listener);
263
+ return () => {
264
+ listeners.delete(listener);
265
+ };
266
+ }
267
+ async function postAction(payload) {
268
+ try {
269
+ await fetch("/api/auto-continue-action", {
270
+ method: "POST",
271
+ headers: { "content-type": "application/json" },
272
+ body: JSON.stringify(payload)
273
+ });
274
+ } catch {
275
+ }
276
+ }
277
+ function handleEvent(event) {
278
+ if (event.type === "state") {
279
+ state = {
280
+ stats: event.stats ?? EMPTY_STATS,
281
+ paused: event.paused ?? []
282
+ };
283
+ for (const listener of listeners) listener();
284
+ } else if (event.type === "notice" && event.notice !== void 0) {
285
+ showNotification(event.notice);
286
+ }
287
+ }
288
+ function showNotification(notice) {
289
+ try {
290
+ const N = globalThis.Notification;
291
+ if (typeof N === "undefined") return;
292
+ const permission = N.permission;
293
+ const create = () => {
294
+ const instance = new N(notice.title, {
295
+ body: notice.body,
296
+ ...notice.actions.length > 0 ? { actions: notice.actions } : {}
297
+ });
298
+ const target = instance;
299
+ target.onclick = () => {
300
+ try {
301
+ globalThis.focus?.();
302
+ } catch {
303
+ }
304
+ };
305
+ target.onaction = (event) => {
306
+ if (notice.sessionId !== void 0) {
307
+ void postAction({ action: event.action, sessionId: notice.sessionId });
308
+ }
309
+ };
310
+ };
311
+ if (permission === "granted") {
312
+ create();
313
+ } else if (permission === "default") {
314
+ void N.requestPermission?.().then((result) => {
315
+ if (result === "granted") create();
316
+ }).catch(() => {
317
+ });
318
+ }
319
+ } catch {
320
+ }
321
+ }
322
+ function startBridge() {
323
+ let stopped = false;
324
+ let controller;
325
+ const loop = async () => {
326
+ while (!stopped) {
327
+ controller = new AbortController();
328
+ try {
329
+ const response = await fetch("/api/auto-continue-bridge", { signal: controller.signal });
330
+ if (!response.ok || response.body === null) throw new Error(`bridge HTTP ${response.status}`);
331
+ const reader = response.body.getReader();
332
+ const decoder = new TextDecoder();
333
+ let buffer = "";
334
+ for (; ; ) {
335
+ const { done, value } = await reader.read();
336
+ if (done) break;
337
+ buffer += decoder.decode(value, { stream: true });
338
+ let idx = buffer.indexOf("\n\n");
339
+ while (idx !== -1) {
340
+ const chunk = buffer.slice(0, idx);
341
+ buffer = buffer.slice(idx + 2);
342
+ for (const line of chunk.split("\n")) {
343
+ if (line.startsWith("data: ")) {
344
+ try {
345
+ handleEvent(JSON.parse(line.slice(6)));
346
+ } catch {
347
+ }
348
+ }
349
+ }
350
+ idx = buffer.indexOf("\n\n");
351
+ }
352
+ }
353
+ } catch {
354
+ }
355
+ if (!stopped) await new Promise((resolve) => setTimeout(resolve, 3e3));
356
+ }
357
+ };
358
+ void loop();
359
+ return () => {
360
+ stopped = true;
361
+ controller?.abort();
362
+ };
363
+ }
364
+
1329
365
  // src/client/settings-form.ts
1330
366
  function numberField(field, min = 0) {
1331
367
  return {
@@ -1696,8 +732,6 @@ var AutoContinueSettingsCardController = class {
1696
732
  booleanField("scanOnBoot"),
1697
733
  numberField("scanLimit", 1),
1698
734
  numberField("freshMs", 0),
1699
- numberField("reconnectScanDelayMs", 0),
1700
- numberField("reconnectBackoffMs", 0),
1701
735
  booleanField("verbose"),
1702
736
  booleanField("classify"),
1703
737
  numberField("backoffFactor", 1),
@@ -1728,8 +762,6 @@ var AutoContinueSettingsCardController = class {
1728
762
  scanOnBoot: this.form.field("scanOnBoot"),
1729
763
  scanLimit: this.form.field("scanLimit"),
1730
764
  freshMs: this.form.field("freshMs"),
1731
- reconnectScanDelayMs: this.form.field("reconnectScanDelayMs"),
1732
- reconnectBackoffMs: this.form.field("reconnectBackoffMs"),
1733
765
  verbose: this.form.field("verbose"),
1734
766
  classify: this.form.field("classify"),
1735
767
  backoffFactor: this.form.field("backoffFactor"),
@@ -1754,10 +786,10 @@ var AutoContinueSettingsCardController = class {
1754
786
  };
1755
787
  function SettingsCard(props) {
1756
788
  const [open, setOpen] = (0, import_react.useState)(false);
1757
- const { state } = props;
1758
- if (!state.available) return null;
789
+ const { state: state2 } = props;
790
+ if (!state2.available) return null;
1759
791
  const title = props.t(props.titleKey);
1760
- const blocked = !state.dirty || state.invalid || state.saving;
792
+ const blocked = !state2.dirty || state2.invalid || state2.saving;
1761
793
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: open ? "dshAcCard dshAcCardOpen" : "dshAcCard", children: [
1762
794
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1763
795
  "button",
@@ -1773,27 +805,27 @@ function SettingsCard(props) {
1773
805
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshAcName", children: title }),
1774
806
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshAcDescription", children: props.t(props.descriptionKey) })
1775
807
  ] }),
1776
- state.dirty ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshAcPending", title: props.t("chrome.unsaved"), children: props.t("chrome.unsaved") }) : null,
808
+ state2.dirty ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshAcPending", title: props.t("chrome.unsaved"), children: props.t("chrome.unsaved") }) : null,
1777
809
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: open ? "dshAcChevron dshAcChevronOpen" : "dshAcChevron", children: "▾" })
1778
810
  ]
1779
811
  }
1780
812
  ),
1781
813
  open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshAcBody", children: [
1782
- !state.writable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshAcReadOnly", role: "status", children: props.t("chrome.readOnly") }) : null,
814
+ !state2.writable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshAcReadOnly", role: "status", children: props.t("chrome.readOnly") }) : null,
1783
815
  props.children,
1784
816
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshAcFooter", children: [
1785
- state.failed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshAcFailed", role: "status", children: props.t("chrome.saveFailed") }) : null,
817
+ state2.failed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshAcFailed", role: "status", children: props.t("chrome.saveFailed") }) : null,
1786
818
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1787
819
  "button",
1788
820
  {
1789
821
  type: "button",
1790
822
  className: "dshAcDiscard",
1791
- disabled: !state.dirty || state.saving,
823
+ disabled: !state2.dirty || state2.saving,
1792
824
  onClick: props.onDiscard,
1793
825
  children: props.t("chrome.discard")
1794
826
  }
1795
827
  ),
1796
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dshAcSave", disabled: blocked, onClick: props.onSave, children: props.t(!state.saving ? "chrome.save" : "chrome.saving") })
828
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dshAcSave", disabled: blocked, onClick: props.onSave, children: props.t(!state2.saving ? "chrome.save" : "chrome.saving") })
1797
829
  ] })
1798
830
  ] }) : null
1799
831
  ] });
@@ -1855,8 +887,12 @@ function LivePanels(props) {
1855
887
  const { t } = props;
1856
888
  const [, refresh] = (0, import_react.useState)(0);
1857
889
  (0, import_react.useEffect)(() => {
890
+ const unsubscribe = subscribeBridge(() => refresh((value) => value + 1));
1858
891
  const timer = setInterval(() => refresh((value) => value + 1), 5e3);
1859
- return () => clearInterval(timer);
892
+ return () => {
893
+ unsubscribe();
894
+ clearInterval(timer);
895
+ };
1860
896
  }, []);
1861
897
  const stats = readTodayStats();
1862
898
  const hasStats = stats.sent + stats.skipped + stats.recovered + stats.failed + stats.gaveUp + stats.looped > 0;
@@ -1963,8 +999,8 @@ function LivePanels(props) {
1963
999
  }
1964
1000
  function AutoContinueSettingsCard(props) {
1965
1001
  const { t } = props;
1966
- const state = props.useAutoContinueSettingsCard((snapshot) => snapshot);
1967
- const disabled = !state.writable;
1002
+ const state2 = props.useAutoContinueSettingsCard((snapshot) => snapshot);
1003
+ const disabled = !state2.writable;
1968
1004
  const shared = { t, disabled };
1969
1005
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1970
1006
  SettingsCard,
@@ -1972,7 +1008,7 @@ function AutoContinueSettingsCard(props) {
1972
1008
  t,
1973
1009
  titleKey: "card.title",
1974
1010
  descriptionKey: "card.description",
1975
- state,
1011
+ state: state2,
1976
1012
  onSave: props.save,
1977
1013
  onDiscard: props.discard,
1978
1014
  children: [
@@ -1983,7 +1019,7 @@ function AutoContinueSettingsCard(props) {
1983
1019
  label: t("field.paused"),
1984
1020
  hint: t("field.pausedHint"),
1985
1021
  ...shared,
1986
- ...state.paused,
1022
+ ...state2.paused,
1987
1023
  onEdit: (text) => props.edit("paused", text),
1988
1024
  onReset: () => props.resetField("paused")
1989
1025
  }
@@ -1995,7 +1031,7 @@ function AutoContinueSettingsCard(props) {
1995
1031
  label: t("field.continueText"),
1996
1032
  hint: t("field.continueTextHint"),
1997
1033
  ...shared,
1998
- ...state.continueText,
1034
+ ...state2.continueText,
1999
1035
  onEdit: (text) => props.edit("continueText", text),
2000
1036
  placeholder: DEFAULT_CONFIG.continueText,
2001
1037
  onReset: () => props.resetField("continueText")
@@ -2008,7 +1044,7 @@ function AutoContinueSettingsCard(props) {
2008
1044
  label: t("field.continueTextMaxTokens"),
2009
1045
  hint: t("field.continueTextMaxTokensHint"),
2010
1046
  ...shared,
2011
- ...state.continueTextMaxTokens,
1047
+ ...state2.continueTextMaxTokens,
2012
1048
  onEdit: (text) => props.edit("continueTextMaxTokens", text),
2013
1049
  placeholder: DEFAULT_CONFIG.continueTextMaxTokens,
2014
1050
  onReset: () => props.resetField("continueTextMaxTokens")
@@ -2021,7 +1057,7 @@ function AutoContinueSettingsCard(props) {
2021
1057
  label: t("field.guardTools"),
2022
1058
  hint: t("field.guardToolsHint"),
2023
1059
  ...shared,
2024
- ...state.guardTools,
1060
+ ...state2.guardTools,
2025
1061
  onEdit: (text) => props.edit("guardTools", text),
2026
1062
  onReset: () => props.resetField("guardTools")
2027
1063
  }
@@ -2033,7 +1069,7 @@ function AutoContinueSettingsCard(props) {
2033
1069
  label: t("field.guardPendingText"),
2034
1070
  hint: t("field.guardPendingTextHint"),
2035
1071
  ...shared,
2036
- ...state.guardPendingText,
1072
+ ...state2.guardPendingText,
2037
1073
  onEdit: (text) => props.edit("guardPendingText", text),
2038
1074
  placeholder: DEFAULT_CONFIG.guardPendingText,
2039
1075
  onReset: () => props.resetField("guardPendingText")
@@ -2046,7 +1082,7 @@ function AutoContinueSettingsCard(props) {
2046
1082
  label: t("field.guardDoneText"),
2047
1083
  hint: t("field.guardDoneTextHint"),
2048
1084
  ...shared,
2049
- ...state.guardDoneText,
1085
+ ...state2.guardDoneText,
2050
1086
  onEdit: (text) => props.edit("guardDoneText", text),
2051
1087
  placeholder: DEFAULT_CONFIG.guardDoneText,
2052
1088
  onReset: () => props.resetField("guardDoneText")
@@ -2060,7 +1096,7 @@ function AutoContinueSettingsCard(props) {
2060
1096
  hint: t("field.graceMsHint"),
2061
1097
  numeric: true,
2062
1098
  ...shared,
2063
- ...state.graceMs,
1099
+ ...state2.graceMs,
2064
1100
  onEdit: (text) => props.edit("graceMs", text),
2065
1101
  onReset: () => props.resetField("graceMs")
2066
1102
  }
@@ -2073,7 +1109,7 @@ function AutoContinueSettingsCard(props) {
2073
1109
  hint: t("field.cooldownMsHint"),
2074
1110
  numeric: true,
2075
1111
  ...shared,
2076
- ...state.cooldownMs,
1112
+ ...state2.cooldownMs,
2077
1113
  onEdit: (text) => props.edit("cooldownMs", text),
2078
1114
  onReset: () => props.resetField("cooldownMs")
2079
1115
  }
@@ -2086,7 +1122,7 @@ function AutoContinueSettingsCard(props) {
2086
1122
  hint: t("field.maxConsecutiveHint"),
2087
1123
  numeric: true,
2088
1124
  ...shared,
2089
- ...state.maxConsecutive,
1125
+ ...state2.maxConsecutive,
2090
1126
  onEdit: (text) => props.edit("maxConsecutive", text),
2091
1127
  onReset: () => props.resetField("maxConsecutive")
2092
1128
  }
@@ -2098,7 +1134,7 @@ function AutoContinueSettingsCard(props) {
2098
1134
  label: t("field.scanOnBoot"),
2099
1135
  hint: t("field.scanOnBootHint"),
2100
1136
  ...shared,
2101
- ...state.scanOnBoot,
1137
+ ...state2.scanOnBoot,
2102
1138
  onEdit: (text) => props.edit("scanOnBoot", text),
2103
1139
  onReset: () => props.resetField("scanOnBoot")
2104
1140
  }
@@ -2111,7 +1147,7 @@ function AutoContinueSettingsCard(props) {
2111
1147
  hint: t("field.scanLimitHint"),
2112
1148
  numeric: true,
2113
1149
  ...shared,
2114
- ...state.scanLimit,
1150
+ ...state2.scanLimit,
2115
1151
  onEdit: (text) => props.edit("scanLimit", text),
2116
1152
  onReset: () => props.resetField("scanLimit")
2117
1153
  }
@@ -2124,37 +1160,11 @@ function AutoContinueSettingsCard(props) {
2124
1160
  hint: t("field.freshMsHint"),
2125
1161
  numeric: true,
2126
1162
  ...shared,
2127
- ...state.freshMs,
1163
+ ...state2.freshMs,
2128
1164
  onEdit: (text) => props.edit("freshMs", text),
2129
1165
  onReset: () => props.resetField("freshMs")
2130
1166
  }
2131
1167
  ),
2132
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2133
- ValueField,
2134
- {
2135
- id: "auto-continue-reconnect-scan-delay",
2136
- label: t("field.reconnectScanDelayMs"),
2137
- hint: t("field.reconnectScanDelayMsHint"),
2138
- numeric: true,
2139
- ...shared,
2140
- ...state.reconnectScanDelayMs,
2141
- onEdit: (text) => props.edit("reconnectScanDelayMs", text),
2142
- onReset: () => props.resetField("reconnectScanDelayMs")
2143
- }
2144
- ),
2145
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2146
- ValueField,
2147
- {
2148
- id: "auto-continue-reconnect-backoff",
2149
- label: t("field.reconnectBackoffMs"),
2150
- hint: t("field.reconnectBackoffMsHint"),
2151
- numeric: true,
2152
- ...shared,
2153
- ...state.reconnectBackoffMs,
2154
- onEdit: (text) => props.edit("reconnectBackoffMs", text),
2155
- onReset: () => props.resetField("reconnectBackoffMs")
2156
- }
2157
- ),
2158
1168
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2159
1169
  BooleanField,
2160
1170
  {
@@ -2162,7 +1172,7 @@ function AutoContinueSettingsCard(props) {
2162
1172
  label: t("field.verbose"),
2163
1173
  hint: t("field.verboseHint"),
2164
1174
  ...shared,
2165
- ...state.verbose,
1175
+ ...state2.verbose,
2166
1176
  onEdit: (text) => props.edit("verbose", text),
2167
1177
  onReset: () => props.resetField("verbose")
2168
1178
  }
@@ -2174,7 +1184,7 @@ function AutoContinueSettingsCard(props) {
2174
1184
  label: t("field.classify"),
2175
1185
  hint: t("field.classifyHint"),
2176
1186
  ...shared,
2177
- ...state.classify,
1187
+ ...state2.classify,
2178
1188
  onEdit: (text) => props.edit("classify", text),
2179
1189
  onReset: () => props.resetField("classify")
2180
1190
  }
@@ -2187,7 +1197,7 @@ function AutoContinueSettingsCard(props) {
2187
1197
  hint: t("field.backoffFactorHint"),
2188
1198
  numeric: true,
2189
1199
  ...shared,
2190
- ...state.backoffFactor,
1200
+ ...state2.backoffFactor,
2191
1201
  onEdit: (text) => props.edit("backoffFactor", text),
2192
1202
  onReset: () => props.resetField("backoffFactor")
2193
1203
  }
@@ -2200,7 +1210,7 @@ function AutoContinueSettingsCard(props) {
2200
1210
  hint: t("field.backoffMaxMsHint"),
2201
1211
  numeric: true,
2202
1212
  ...shared,
2203
- ...state.backoffMaxMs,
1213
+ ...state2.backoffMaxMs,
2204
1214
  onEdit: (text) => props.edit("backoffMaxMs", text),
2205
1215
  onReset: () => props.resetField("backoffMaxMs")
2206
1216
  }
@@ -2212,7 +1222,7 @@ function AutoContinueSettingsCard(props) {
2212
1222
  label: t("field.notify"),
2213
1223
  hint: t("field.notifyHint"),
2214
1224
  ...shared,
2215
- ...state.notify,
1225
+ ...state2.notify,
2216
1226
  onEdit: (text) => props.edit("notify", text),
2217
1227
  onReset: () => props.resetField("notify")
2218
1228
  }
@@ -2224,7 +1234,7 @@ function AutoContinueSettingsCard(props) {
2224
1234
  label: t("field.loopGuard"),
2225
1235
  hint: t("field.loopGuardHint"),
2226
1236
  ...shared,
2227
- ...state.loopGuard,
1237
+ ...state2.loopGuard,
2228
1238
  onEdit: (text) => props.edit("loopGuard", text),
2229
1239
  onReset: () => props.resetField("loopGuard")
2230
1240
  }
@@ -2237,7 +1247,7 @@ function AutoContinueSettingsCard(props) {
2237
1247
  hint: t("field.loopShortCharsHint"),
2238
1248
  numeric: true,
2239
1249
  ...shared,
2240
- ...state.loopShortChars,
1250
+ ...state2.loopShortChars,
2241
1251
  onEdit: (text) => props.edit("loopShortChars", text),
2242
1252
  onReset: () => props.resetField("loopShortChars")
2243
1253
  }
@@ -2250,7 +1260,7 @@ function AutoContinueSettingsCard(props) {
2250
1260
  hint: t("field.loopWindowMsHint"),
2251
1261
  numeric: true,
2252
1262
  ...shared,
2253
- ...state.loopWindowMs,
1263
+ ...state2.loopWindowMs,
2254
1264
  onEdit: (text) => props.edit("loopWindowMs", text),
2255
1265
  onReset: () => props.resetField("loopWindowMs")
2256
1266
  }
@@ -2263,7 +1273,7 @@ function AutoContinueSettingsCard(props) {
2263
1273
  hint: t("field.loopShortCountHint"),
2264
1274
  numeric: true,
2265
1275
  ...shared,
2266
- ...state.loopShortCount,
1276
+ ...state2.loopShortCount,
2267
1277
  onEdit: (text) => props.edit("loopShortCount", text),
2268
1278
  onReset: () => props.resetField("loopShortCount")
2269
1279
  }
@@ -2276,7 +1286,7 @@ function AutoContinueSettingsCard(props) {
2276
1286
  hint: t("field.loopToolRepeatHint"),
2277
1287
  numeric: true,
2278
1288
  ...shared,
2279
- ...state.loopToolRepeat,
1289
+ ...state2.loopToolRepeat,
2280
1290
  onEdit: (text) => props.edit("loopToolRepeat", text),
2281
1291
  onReset: () => props.resetField("loopToolRepeat")
2282
1292
  }
@@ -2289,7 +1299,7 @@ function AutoContinueSettingsCard(props) {
2289
1299
  hint: t("field.loopRepeatTextHint"),
2290
1300
  numeric: true,
2291
1301
  ...shared,
2292
- ...state.loopRepeatText,
1302
+ ...state2.loopRepeatText,
2293
1303
  onEdit: (text) => props.edit("loopRepeatText", text),
2294
1304
  onReset: () => props.resetField("loopRepeatText")
2295
1305
  }
@@ -2301,7 +1311,7 @@ function AutoContinueSettingsCard(props) {
2301
1311
  label: t("field.loopText"),
2302
1312
  hint: t("field.loopTextHint"),
2303
1313
  ...shared,
2304
- ...state.loopText,
1314
+ ...state2.loopText,
2305
1315
  onEdit: (text) => props.edit("loopText", text),
2306
1316
  placeholder: DEFAULT_CONFIG.loopText,
2307
1317
  onReset: () => props.resetField("loopText")
@@ -2316,13 +1326,11 @@ function AutoContinueSettingsCard(props) {
2316
1326
  // src/client/index.ts
2317
1327
  var NS = "auto-continue";
2318
1328
  var SETTINGS_NS = "auto-continue";
2319
- var inject = ["slots", "locale", "connection", "settingsScope"];
2320
- var current = null;
1329
+ var inject = ["slots", "locale", "settingsScope"];
2321
1330
  function apply(ctx) {
2322
1331
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "auto-continue: dictionaries");
1332
+ ctx.effect(() => startBridge(), "auto-continue: host bridge");
2323
1333
  const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS });
2324
- current?.dispose();
2325
- current = new AutoContinueRunner(ctx.connection.api, () => resolveConfig(scope.getSnapshot().value));
2326
1334
  const controller = new AutoContinueSettingsCardController(scope);
2327
1335
  ctx.slots.inject(
2328
1336
  "settings.plugin.item",