dsh-client-auto-continue 0.11.2 → 0.11.4
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.map +2 -2
- package/lib/index.js +457 -119
- package/lib/types/host/engine.d.ts +3 -3
- package/lib/types/shared/core.d.ts +83 -27
- package/package.json +1 -1
- package/src/host/engine.ts +189 -119
- package/src/shared/core.ts +436 -45
package/lib/index.js
CHANGED
|
@@ -203,6 +203,46 @@ function fillTemplate(template, ctx) {
|
|
|
203
203
|
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 ?? "");
|
|
204
204
|
}
|
|
205
205
|
var TOOL_RESULT_CAP = 160;
|
|
206
|
+
function stableFingerprint(value) {
|
|
207
|
+
let first = 2166136261;
|
|
208
|
+
let second = 2654435769;
|
|
209
|
+
let length = 0;
|
|
210
|
+
const feed = (text) => {
|
|
211
|
+
length += text.length;
|
|
212
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
213
|
+
const code = text.charCodeAt(i);
|
|
214
|
+
first = Math.imul(first ^ code, 16777619) >>> 0;
|
|
215
|
+
second = Math.imul(second ^ code, 2246822507) >>> 0;
|
|
216
|
+
second = (second ^ second >>> 13) >>> 0;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const walk = (part) => {
|
|
220
|
+
if (part === null) {
|
|
221
|
+
feed("null");
|
|
222
|
+
} else if (Array.isArray(part)) {
|
|
223
|
+
feed("[");
|
|
224
|
+
for (const item of part) {
|
|
225
|
+
walk(item);
|
|
226
|
+
feed(",");
|
|
227
|
+
}
|
|
228
|
+
feed("]");
|
|
229
|
+
} else if (typeof part === "object") {
|
|
230
|
+
feed("{");
|
|
231
|
+
const record = part;
|
|
232
|
+
for (const key of Object.keys(record).sort()) {
|
|
233
|
+
feed(JSON.stringify(key));
|
|
234
|
+
feed(":");
|
|
235
|
+
walk(record[key]);
|
|
236
|
+
feed(",");
|
|
237
|
+
}
|
|
238
|
+
feed("}");
|
|
239
|
+
} else {
|
|
240
|
+
feed(`${typeof part}:${JSON.stringify(part) ?? String(part)}`);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
walk(value);
|
|
244
|
+
return `${first.toString(16).padStart(8, "0")}${second.toString(16).padStart(8, "0")}:${length}`;
|
|
245
|
+
}
|
|
206
246
|
function extractText(blocks, cap) {
|
|
207
247
|
let out = "";
|
|
208
248
|
const walk = (value) => {
|
|
@@ -222,10 +262,262 @@ function extractText(blocks, cap) {
|
|
|
222
262
|
walk(blocks);
|
|
223
263
|
return out.slice(0, cap);
|
|
224
264
|
}
|
|
265
|
+
function toolCorrelationKey(data, callId) {
|
|
266
|
+
if (callId === void 0 || typeof data.turn !== "number" || typeof data.step !== "number") {
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
return JSON.stringify([data.turn, data.step, callId]);
|
|
270
|
+
}
|
|
271
|
+
function resultBlock(data) {
|
|
272
|
+
return data.message?.content?.find((part) => part.type === "tool-result");
|
|
273
|
+
}
|
|
274
|
+
function toolResultCallId(data) {
|
|
275
|
+
if (resultBlock(data) === void 0) return void 0;
|
|
276
|
+
const sourceId = data.message?.source?.kind === "tool" ? data.message.source.callId : void 0;
|
|
277
|
+
const blockId = resultBlock(data)?.toolCallId;
|
|
278
|
+
const source = typeof sourceId === "string" && sourceId !== "" ? sourceId : void 0;
|
|
279
|
+
const block = typeof blockId === "string" && blockId !== "" ? blockId : void 0;
|
|
280
|
+
if (source !== void 0 && block !== void 0 && source !== block) return void 0;
|
|
281
|
+
return source ?? block;
|
|
282
|
+
}
|
|
225
283
|
function toolResultFacts(data) {
|
|
226
|
-
const
|
|
227
|
-
|
|
284
|
+
const result = resultBlock(data);
|
|
285
|
+
const failed = data.error !== void 0 || result?.isError === true;
|
|
286
|
+
return {
|
|
287
|
+
ok: !failed,
|
|
288
|
+
excerpt: extractText(result?.content, TOOL_RESULT_CAP),
|
|
289
|
+
identity: stableFingerprint({ content: result?.content ?? [], isError: failed })
|
|
290
|
+
};
|
|
228
291
|
}
|
|
292
|
+
var MAX_PENDING_TOOL_CALLS = 64;
|
|
293
|
+
var MAX_SEEN_TOOL_CALL_IDS = 256;
|
|
294
|
+
var ToolInvocationTracker = class {
|
|
295
|
+
constructor() {
|
|
296
|
+
this.pendingById = /* @__PURE__ */ new Map();
|
|
297
|
+
this.pendingInOrder = [];
|
|
298
|
+
this.seenCalls = /* @__PURE__ */ new Map();
|
|
299
|
+
this.seenInOrder = [];
|
|
300
|
+
this.lastEventSeq = -1;
|
|
301
|
+
}
|
|
302
|
+
reset() {
|
|
303
|
+
this.pendingById.clear();
|
|
304
|
+
this.pendingInOrder.length = 0;
|
|
305
|
+
this.seenCalls.clear();
|
|
306
|
+
this.seenInOrder.length = 0;
|
|
307
|
+
this.latest = void 0;
|
|
308
|
+
this.run = void 0;
|
|
309
|
+
this.repeatSignal = void 0;
|
|
310
|
+
this.lastEventSeq = -1;
|
|
311
|
+
}
|
|
312
|
+
/** 新回合边界:清空工具态,同时把重放水位推进到 turn/start。 */
|
|
313
|
+
startTurn(seq) {
|
|
314
|
+
if (!Number.isSafeInteger(seq) || seq < 0 || seq <= this.lastEventSeq) return;
|
|
315
|
+
this.reset();
|
|
316
|
+
this.lastEventSeq = seq;
|
|
317
|
+
}
|
|
318
|
+
/** 回合已结束:保留最后一次调用的护栏,丢弃不再可用的 loop 关联态。 */
|
|
319
|
+
resetRepeat() {
|
|
320
|
+
this.pendingById.clear();
|
|
321
|
+
this.pendingInOrder.length = 0;
|
|
322
|
+
this.seenCalls.clear();
|
|
323
|
+
this.seenInOrder.length = 0;
|
|
324
|
+
this.run = void 0;
|
|
325
|
+
this.repeatSignal = void 0;
|
|
326
|
+
}
|
|
327
|
+
recordCall(event) {
|
|
328
|
+
if (!this.acceptEventSeq(event.seq)) return false;
|
|
329
|
+
this.repeatSignal = void 0;
|
|
330
|
+
const data = event.data;
|
|
331
|
+
if (typeof data.name !== "string") {
|
|
332
|
+
this.breakCorrelation();
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
const key = `${data.name}
|
|
336
|
+
${typeof data.arguments === "string" ? data.arguments : ""}`;
|
|
337
|
+
const callId = typeof data.callId === "string" && data.callId !== "" ? data.callId : void 0;
|
|
338
|
+
const id = toolCorrelationKey(data, callId);
|
|
339
|
+
if (id === void 0) {
|
|
340
|
+
this.breakCorrelation({
|
|
341
|
+
id: void 0,
|
|
342
|
+
name: data.name,
|
|
343
|
+
key,
|
|
344
|
+
result: void 0,
|
|
345
|
+
resultSeq: void 0
|
|
346
|
+
});
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
const seen = this.seenCalls.get(id);
|
|
350
|
+
if (seen !== void 0) {
|
|
351
|
+
this.breakCorrelation({
|
|
352
|
+
id: void 0,
|
|
353
|
+
name: data.name,
|
|
354
|
+
key,
|
|
355
|
+
result: void 0,
|
|
356
|
+
resultSeq: void 0
|
|
357
|
+
});
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
const call = {
|
|
361
|
+
id,
|
|
362
|
+
name: data.name,
|
|
363
|
+
key,
|
|
364
|
+
result: void 0,
|
|
365
|
+
resultSeq: void 0
|
|
366
|
+
};
|
|
367
|
+
this.latest = call;
|
|
368
|
+
this.pendingById.set(id, call);
|
|
369
|
+
this.pendingInOrder.push(call);
|
|
370
|
+
this.seenCalls.set(id, call);
|
|
371
|
+
this.seenInOrder.push(id);
|
|
372
|
+
this.trim(call);
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
recordResult(event) {
|
|
376
|
+
if (!this.acceptEventSeq(event.seq)) return void 0;
|
|
377
|
+
const data = event.data;
|
|
378
|
+
const id = toolCorrelationKey(data, toolResultCallId(data));
|
|
379
|
+
if (id === void 0) {
|
|
380
|
+
this.breakCorrelation(this.latest);
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
const surfaceOp = event.surfaceOp;
|
|
384
|
+
if (typeof surfaceOp === "object" && surfaceOp !== null) {
|
|
385
|
+
const call2 = this.seenCalls.get(id);
|
|
386
|
+
if (surfaceOp.start !== surfaceOp.end || call2 === void 0 || call2.result === void 0 || call2.resultSeq !== surfaceOp.start) {
|
|
387
|
+
this.breakCorrelation(this.latest);
|
|
388
|
+
return void 0;
|
|
389
|
+
}
|
|
390
|
+
call2.result = toolResultFacts(data);
|
|
391
|
+
call2.resultSeq = event.seq;
|
|
392
|
+
this.breakCorrelation(this.latest);
|
|
393
|
+
return void 0;
|
|
394
|
+
}
|
|
395
|
+
const call = this.pendingById.get(id);
|
|
396
|
+
if (call === void 0) {
|
|
397
|
+
const seen = this.seenCalls.get(id);
|
|
398
|
+
const duplicate = seen?.result;
|
|
399
|
+
const incoming = toolResultFacts(data);
|
|
400
|
+
if (seen !== void 0 && duplicate !== void 0 && seen.resultSeq === event.seq && duplicate.identity === incoming.identity) {
|
|
401
|
+
return void 0;
|
|
402
|
+
}
|
|
403
|
+
if (seen !== void 0 && duplicate !== void 0) {
|
|
404
|
+
seen.result = void 0;
|
|
405
|
+
seen.resultSeq = void 0;
|
|
406
|
+
}
|
|
407
|
+
this.breakCorrelation(this.latest);
|
|
408
|
+
return void 0;
|
|
409
|
+
}
|
|
410
|
+
if (call.result !== void 0) {
|
|
411
|
+
const incoming = toolResultFacts(data);
|
|
412
|
+
if (call.resultSeq === event.seq && call.result.identity === incoming.identity) {
|
|
413
|
+
return void 0;
|
|
414
|
+
}
|
|
415
|
+
call.result = void 0;
|
|
416
|
+
call.resultSeq = void 0;
|
|
417
|
+
this.breakCorrelation(this.latest);
|
|
418
|
+
return void 0;
|
|
419
|
+
}
|
|
420
|
+
call.result = toolResultFacts(data);
|
|
421
|
+
call.resultSeq = event.seq;
|
|
422
|
+
return this.drainCompleted();
|
|
423
|
+
}
|
|
424
|
+
guard() {
|
|
425
|
+
const latest = this.latest;
|
|
426
|
+
if (latest === void 0) return { kind: "none" };
|
|
427
|
+
if (latest.result === void 0) return { kind: "pending", tool: latest.name };
|
|
428
|
+
if (latest.result.ok) {
|
|
429
|
+
return { kind: "done", tool: latest.name, result: latest.result.excerpt };
|
|
430
|
+
}
|
|
431
|
+
return { kind: "failed", tool: latest.name };
|
|
432
|
+
}
|
|
433
|
+
lastTool() {
|
|
434
|
+
return this.latest?.name;
|
|
435
|
+
}
|
|
436
|
+
/** 下一模型 step 是稳定边界;此前 replacement/新调用会先清除候选。 */
|
|
437
|
+
confirmRepeatAtStep(seq) {
|
|
438
|
+
if (!this.acceptEventSeq(seq)) return void 0;
|
|
439
|
+
const signal = this.pendingInOrder.length === 0 ? this.repeatSignal : void 0;
|
|
440
|
+
this.repeatSignal = void 0;
|
|
441
|
+
return signal;
|
|
442
|
+
}
|
|
443
|
+
/** 非工具 surface range replacement(如 compaction summary)同样终止旧工具证据。 */
|
|
444
|
+
recordSurfaceReplacement(seq) {
|
|
445
|
+
if (!this.acceptEventSeq(seq)) return;
|
|
446
|
+
this.breakCorrelation(this.latest);
|
|
447
|
+
}
|
|
448
|
+
restore(events, untilSeq) {
|
|
449
|
+
this.reset();
|
|
450
|
+
for (const event of events) {
|
|
451
|
+
if (event.seq >= untilSeq) continue;
|
|
452
|
+
if (event.type === "turn/start") this.startTurn(event.seq);
|
|
453
|
+
else if (event.type === "step/start") this.confirmRepeatAtStep(event.seq);
|
|
454
|
+
else if (event.type === "tool/call") this.recordCall(event);
|
|
455
|
+
else if (event.type === "tool/result") this.recordResult(event);
|
|
456
|
+
else if ((event.type === "user/message" || event.type === "assistant/message") && typeof event.surfaceOp === "object" && event.surfaceOp !== null) {
|
|
457
|
+
this.recordSurfaceReplacement(event.seq);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
acceptEventSeq(seq) {
|
|
462
|
+
if (!Number.isSafeInteger(seq) || seq < 0) {
|
|
463
|
+
this.breakCorrelation(this.latest);
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
if (seq <= this.lastEventSeq) return false;
|
|
467
|
+
this.lastEventSeq = seq;
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
breakCorrelation(latest, preserve) {
|
|
471
|
+
this.pendingById.clear();
|
|
472
|
+
this.pendingInOrder.length = 0;
|
|
473
|
+
this.invalidateRunHistory();
|
|
474
|
+
this.latest = latest;
|
|
475
|
+
if (preserve?.id !== void 0 && preserve.result === void 0 && this.seenCalls.get(preserve.id) === preserve) {
|
|
476
|
+
this.pendingById.set(preserve.id, preserve);
|
|
477
|
+
this.pendingInOrder.push(preserve);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
invalidateRunHistory() {
|
|
481
|
+
this.run = void 0;
|
|
482
|
+
this.repeatSignal = void 0;
|
|
483
|
+
}
|
|
484
|
+
trim(current) {
|
|
485
|
+
while (this.pendingInOrder.length > MAX_PENDING_TOOL_CALLS) {
|
|
486
|
+
this.breakCorrelation(current, current);
|
|
487
|
+
}
|
|
488
|
+
while (this.seenInOrder.length > MAX_SEEN_TOOL_CALL_IDS) {
|
|
489
|
+
const id = this.seenInOrder.shift();
|
|
490
|
+
if (id !== void 0) {
|
|
491
|
+
this.seenCalls.delete(id);
|
|
492
|
+
this.breakCorrelation(current, current);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
drainCompleted() {
|
|
497
|
+
let advanced = false;
|
|
498
|
+
while (this.pendingInOrder[0]?.result !== void 0) {
|
|
499
|
+
const call = this.pendingInOrder.shift();
|
|
500
|
+
if (call === void 0 || call.result === void 0) break;
|
|
501
|
+
advanced = true;
|
|
502
|
+
if (call.id !== void 0) this.pendingById.delete(call.id);
|
|
503
|
+
this.advanceRun(call);
|
|
504
|
+
}
|
|
505
|
+
if (!advanced) return void 0;
|
|
506
|
+
return this.refreshRepeatSignal();
|
|
507
|
+
}
|
|
508
|
+
advanceRun(call) {
|
|
509
|
+
if (call.result === void 0) return;
|
|
510
|
+
if (this.run?.key === call.key && this.run.identity === call.result.identity) {
|
|
511
|
+
this.run.count += 1;
|
|
512
|
+
} else {
|
|
513
|
+
this.run = { key: call.key, tool: call.name, identity: call.result.identity, count: 1 };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
refreshRepeatSignal() {
|
|
517
|
+
this.repeatSignal = this.pendingInOrder.length === 0 && this.run !== void 0 ? { tool: this.run.tool, count: this.run.count } : void 0;
|
|
518
|
+
return this.repeatSignal;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
229
521
|
function effectiveCooldown(consecutive, base, factor, max) {
|
|
230
522
|
const multiplier = Math.pow(factor, consecutive);
|
|
231
523
|
return Math.min(Math.max(base, base * multiplier), Math.max(base, max));
|
|
@@ -244,38 +536,53 @@ function emptyDayStats() {
|
|
|
244
536
|
}
|
|
245
537
|
var freshState = () => ({
|
|
246
538
|
consecutive: 0,
|
|
247
|
-
lastAutoAt: 0,
|
|
248
539
|
lastAttemptAt: 0,
|
|
249
|
-
|
|
540
|
+
pendingEchoMessageIds: /* @__PURE__ */ new Map(),
|
|
250
541
|
pendingTimer: void 0,
|
|
251
542
|
running: void 0,
|
|
252
543
|
queued: 0,
|
|
253
544
|
subagent: false,
|
|
254
545
|
lastFailure: void 0,
|
|
255
546
|
lastFailureAt: 0,
|
|
256
|
-
|
|
257
|
-
lastToolResult: void 0,
|
|
547
|
+
tools: new ToolInvocationTracker(),
|
|
258
548
|
lastTurn: void 0,
|
|
259
549
|
pendingRecoveryAt: 0,
|
|
260
550
|
shortRun: 0,
|
|
261
551
|
lastShortAt: 0,
|
|
262
552
|
lastAssistantText: "",
|
|
263
553
|
sameTextRun: 0,
|
|
264
|
-
toolRun: void 0,
|
|
265
554
|
loopFired: false,
|
|
266
|
-
loopCancelled: false,
|
|
267
555
|
loopRetryTimer: void 0
|
|
268
556
|
});
|
|
269
557
|
var RECOVERY_WINDOW_MS = 10 * 60 * 1e3;
|
|
270
558
|
var ECHO_WINDOW_MS = 10 * 60 * 1e3;
|
|
559
|
+
var MAX_PENDING_ECHO_MESSAGE_IDS = 64;
|
|
560
|
+
function prunePendingEchoMessageIds(state, now) {
|
|
561
|
+
for (const [messageId, queuedAt] of state.pendingEchoMessageIds) {
|
|
562
|
+
if (now - queuedAt > ECHO_WINDOW_MS) state.pendingEchoMessageIds.delete(messageId);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function trackPendingEcho(state, messageId) {
|
|
566
|
+
const now = Date.now();
|
|
567
|
+
prunePendingEchoMessageIds(state, now);
|
|
568
|
+
state.pendingEchoMessageIds.set(messageId, now);
|
|
569
|
+
while (state.pendingEchoMessageIds.size > MAX_PENDING_ECHO_MESSAGE_IDS) {
|
|
570
|
+
const oldest = state.pendingEchoMessageIds.keys().next();
|
|
571
|
+
if (oldest.done) break;
|
|
572
|
+
state.pendingEchoMessageIds.delete(oldest.value);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function forgetPendingEcho(state, messageId) {
|
|
576
|
+
state.pendingEchoMessageIds.delete(messageId);
|
|
577
|
+
}
|
|
271
578
|
function isOurEcho(state, event) {
|
|
272
579
|
if (event.type !== "user/message") return false;
|
|
273
580
|
const message = event.data;
|
|
274
581
|
if (message.source.kind !== "user") return false;
|
|
275
|
-
if (state.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return
|
|
582
|
+
if (state.pendingEchoMessageIds.size === 0) return false;
|
|
583
|
+
const now = Date.now();
|
|
584
|
+
prunePendingEchoMessageIds(state, now);
|
|
585
|
+
return state.pendingEchoMessageIds.delete(message.id);
|
|
279
586
|
}
|
|
280
587
|
|
|
281
588
|
// src/host/engine.ts
|
|
@@ -301,6 +608,39 @@ var NOTICE_COPY = {
|
|
|
301
608
|
stoppedBody: (sessionId, count) => `${sessionId}: ${count} consecutive failures; manual intervention required`
|
|
302
609
|
}
|
|
303
610
|
};
|
|
611
|
+
var LOOP_GUARD_CANCEL_CAUSE = {
|
|
612
|
+
kind: "hook",
|
|
613
|
+
reason: "dsh-auto-continue:loop-guard"
|
|
614
|
+
};
|
|
615
|
+
function snapshotSessionEvents(session) {
|
|
616
|
+
const compatible = session;
|
|
617
|
+
if (typeof compatible.snapshotEvents === "function") return compatible.snapshotEvents();
|
|
618
|
+
if (compatible.events !== void 0) return compatible.events;
|
|
619
|
+
throw new TypeError("session exposes neither snapshotEvents() nor events");
|
|
620
|
+
}
|
|
621
|
+
function parseFailureFacts(value) {
|
|
622
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
623
|
+
const failure = value;
|
|
624
|
+
const code = typeof failure.code === "string" && failure.code.trim() !== "" ? failure.code : void 0;
|
|
625
|
+
const message = typeof failure.message === "string" && failure.message.trim() !== "" ? failure.message : void 0;
|
|
626
|
+
const status = typeof failure.status === "number" && Number.isFinite(failure.status) ? failure.status : void 0;
|
|
627
|
+
if (code === void 0 && message === void 0 && status === void 0) return void 0;
|
|
628
|
+
return {
|
|
629
|
+
code: code ?? "UNKNOWN",
|
|
630
|
+
message: message ?? code ?? `HTTP ${status}`,
|
|
631
|
+
...status !== void 0 ? { status } : {}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function readReasonKind(value) {
|
|
635
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
636
|
+
const kind = value.kind;
|
|
637
|
+
return typeof kind === "string" && kind.trim() !== "" ? kind : void 0;
|
|
638
|
+
}
|
|
639
|
+
function isLoopGuardCancelReason(value) {
|
|
640
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
641
|
+
const cause = value;
|
|
642
|
+
return cause.kind === LOOP_GUARD_CANCEL_CAUSE.kind && cause.reason === LOOP_GUARD_CANCEL_CAUSE.reason;
|
|
643
|
+
}
|
|
304
644
|
var AutoContinueRunner = class {
|
|
305
645
|
/**
|
|
306
646
|
* @param ctx - host plugin context (agents registry, session events, settings).
|
|
@@ -316,10 +656,15 @@ var AutoContinueRunner = class {
|
|
|
316
656
|
this.noticeListeners = /* @__PURE__ */ new Set();
|
|
317
657
|
this.stateListeners = /* @__PURE__ */ new Set();
|
|
318
658
|
this.disposed = false;
|
|
319
|
-
this.disposeSessionEvents = ctx.on(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
659
|
+
this.disposeSessionEvents = ctx.on("session/event", (session, event) => {
|
|
660
|
+
try {
|
|
661
|
+
this.onHostEvent(session, event);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
console.error(
|
|
664
|
+
`[auto-continue] 会话事件处理异常 ${session.id}: ${error instanceof Error ? error.message : String(error)}`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
323
668
|
const config = this.getConfig();
|
|
324
669
|
if (config.scanOnBoot) {
|
|
325
670
|
void this.bootScanLoop();
|
|
@@ -404,40 +749,20 @@ var AutoContinueRunner = class {
|
|
|
404
749
|
*/
|
|
405
750
|
onHostEvent(session, event) {
|
|
406
751
|
const sessionId = session.id;
|
|
752
|
+
if ((event.type === "user/message" || event.type === "assistant/message") && typeof event.surfaceOp === "object" && event.surfaceOp !== null) {
|
|
753
|
+
this.state(sessionId).tools.recordSurfaceReplacement(event.seq);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
407
756
|
if (event.type === "tool/call") {
|
|
408
|
-
const
|
|
409
|
-
if (
|
|
410
|
-
const state = this.state(sessionId);
|
|
411
|
-
state.lastTool = name;
|
|
412
|
-
state.lastToolResult = "pending";
|
|
413
|
-
state.shortRun = 0;
|
|
414
|
-
const key = `${name}
|
|
415
|
-
${event.data.arguments}`;
|
|
416
|
-
if (state.toolRun?.key === key) {
|
|
417
|
-
state.toolRun.waiting = true;
|
|
418
|
-
} else {
|
|
419
|
-
state.toolRun = { key, count: 1, lastResult: void 0, waiting: false };
|
|
420
|
-
}
|
|
421
|
-
}
|
|
757
|
+
const state = this.state(sessionId);
|
|
758
|
+
if (state.tools.recordCall(event)) state.shortRun = 0;
|
|
422
759
|
} else if (event.type === "tool/result") {
|
|
423
760
|
const state = this.state(sessionId);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
run.waiting = false;
|
|
430
|
-
if (run.lastResult !== void 0 && run.lastResult === facts.excerpt) {
|
|
431
|
-
run.count += 1;
|
|
432
|
-
this.checkLoop(sessionId, state);
|
|
433
|
-
} else {
|
|
434
|
-
run.lastResult = facts.excerpt;
|
|
435
|
-
run.count = 1;
|
|
436
|
-
}
|
|
437
|
-
} else if (run !== void 0 && !run.waiting) {
|
|
438
|
-
run.lastResult = facts.excerpt;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
761
|
+
state.tools.recordResult(event);
|
|
762
|
+
} else if (event.type === "step/start") {
|
|
763
|
+
const state = this.state(sessionId);
|
|
764
|
+
const repeat = state.tools.confirmRepeatAtStep(event.seq);
|
|
765
|
+
if (repeat !== void 0) this.checkLoop(sessionId, state, repeat);
|
|
441
766
|
} else if (event.type === "assistant/message") {
|
|
442
767
|
const state = this.state(sessionId);
|
|
443
768
|
this.onAssistantMessage(sessionId, state, event);
|
|
@@ -474,50 +799,47 @@ ${event.data.arguments}`;
|
|
|
474
799
|
this.checkLoop(sessionId, state);
|
|
475
800
|
}
|
|
476
801
|
/** 两个循环信号的公共检查; 命中且本回合未打断过则打断。 */
|
|
477
|
-
checkLoop(sessionId, state) {
|
|
802
|
+
checkLoop(sessionId, state, toolRepeat) {
|
|
478
803
|
if (!this.getConfig().loopGuard) return;
|
|
479
804
|
if (state.loopFired) return;
|
|
480
805
|
if (!state.running) return;
|
|
481
806
|
const config = this.getConfig();
|
|
482
807
|
if (state.sameTextRun >= config.loopRepeatText) {
|
|
483
808
|
this.log(`检测到空转循环 ${sessionId}: 连续 ${state.sameTextRun} 条相同消息`);
|
|
484
|
-
|
|
809
|
+
this.interruptLoop(sessionId, state);
|
|
485
810
|
} else if (state.shortRun >= config.loopShortCount) {
|
|
486
811
|
this.log(`检测到空转循环 ${sessionId}: 连续 ${state.shortRun} 条短句且无工具调用`);
|
|
487
|
-
|
|
488
|
-
} else if (
|
|
489
|
-
|
|
490
|
-
this.
|
|
491
|
-
void this.interruptLoop(sessionId, state);
|
|
812
|
+
this.interruptLoop(sessionId, state);
|
|
813
|
+
} else if (toolRepeat !== void 0 && toolRepeat.count >= config.loopToolRepeat) {
|
|
814
|
+
this.log(`检测到工具死循环 ${sessionId}: 「${toolRepeat.tool}」连续 ${toolRepeat.count} 次(同参数同结果)`);
|
|
815
|
+
this.interruptLoop(sessionId, state);
|
|
492
816
|
}
|
|
493
817
|
}
|
|
494
818
|
/**
|
|
495
819
|
* 打断运行中的回合: cancel(带来源标记)+ 进冷却。
|
|
496
|
-
*
|
|
497
|
-
*
|
|
820
|
+
* 只有随后持久化的 turn/end 精确携带专属 hook cause 时,
|
|
821
|
+
* 才会用 loopText 重启回合——DSH 的 first-cause 语义保证用户 Stop 优先。
|
|
498
822
|
*/
|
|
499
|
-
|
|
823
|
+
interruptLoop(sessionId, state) {
|
|
500
824
|
if (state.loopFired) return;
|
|
501
825
|
if (Date.now() - state.lastAttemptAt < this.cooldownFor(state)) {
|
|
502
826
|
this.log(`跳过循环打断 ${sessionId}: 处于冷却期`);
|
|
503
827
|
return;
|
|
504
828
|
}
|
|
505
829
|
state.loopFired = true;
|
|
506
|
-
state.loopCancelled = true;
|
|
507
830
|
state.lastAttemptAt = Date.now();
|
|
508
|
-
this.bumpStat({ looped: 1 });
|
|
509
831
|
try {
|
|
510
832
|
const agent = this.ctx.agents.get(sessionId);
|
|
511
833
|
if (agent === void 0) {
|
|
512
834
|
this.log(`打断循环失败 ${sessionId}: 无 live agent`);
|
|
513
|
-
state.
|
|
835
|
+
state.loopFired = false;
|
|
514
836
|
return;
|
|
515
837
|
}
|
|
516
|
-
agent.cancel(
|
|
838
|
+
agent.cancel(LOOP_GUARD_CANCEL_CAUSE, { keepInbox: true });
|
|
517
839
|
this.log(`已打断循环 ${sessionId}: cancel 已受理`);
|
|
518
840
|
} catch (error) {
|
|
519
841
|
this.log(`打断循环失败 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
520
|
-
state.
|
|
842
|
+
state.loopFired = false;
|
|
521
843
|
}
|
|
522
844
|
}
|
|
523
845
|
onSessionEvent(sessionId, event) {
|
|
@@ -525,15 +847,12 @@ ${event.data.arguments}`;
|
|
|
525
847
|
switch (event.type) {
|
|
526
848
|
case "turn/start":
|
|
527
849
|
state.running = true;
|
|
528
|
-
state.
|
|
529
|
-
state.lastToolResult = void 0;
|
|
850
|
+
state.tools.startTurn(event.seq);
|
|
530
851
|
state.shortRun = 0;
|
|
531
852
|
state.lastShortAt = 0;
|
|
532
853
|
state.lastAssistantText = "";
|
|
533
854
|
state.sameTextRun = 0;
|
|
534
|
-
state.toolRun = void 0;
|
|
535
855
|
state.loopFired = false;
|
|
536
|
-
state.loopCancelled = false;
|
|
537
856
|
if (state.loopRetryTimer !== void 0) {
|
|
538
857
|
clearTimeout(state.loopRetryTimer);
|
|
539
858
|
state.loopRetryTimer = void 0;
|
|
@@ -542,22 +861,28 @@ ${event.data.arguments}`;
|
|
|
542
861
|
break;
|
|
543
862
|
case "turn/end": {
|
|
544
863
|
state.running = false;
|
|
864
|
+
const loopCancelPending = state.loopFired;
|
|
865
|
+
state.loopFired = false;
|
|
545
866
|
this.cancelPending(sessionId, "收到新的 turn/end");
|
|
546
867
|
const reason = event.data.reason;
|
|
547
|
-
|
|
868
|
+
const reasonKind = readReasonKind(reason);
|
|
869
|
+
if (reasonKind === void 0) {
|
|
870
|
+
console.error(`[auto-continue] 忽略畸形 turn/end ${sessionId}: reason 无法解释`);
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
if (reasonKind === "completed") {
|
|
548
874
|
state.consecutive = 0;
|
|
549
875
|
state.lastFailure = void 0;
|
|
550
876
|
this.noteRecovery(sessionId, "completed");
|
|
551
|
-
} else if (
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
state.loopFired = false;
|
|
877
|
+
} else if (reasonKind === "aborted") {
|
|
878
|
+
if (isLoopGuardCancelReason(reason.reason)) {
|
|
879
|
+
if (loopCancelPending) this.bumpStat({ looped: 1 });
|
|
555
880
|
state.pendingRecoveryAt = 0;
|
|
556
881
|
state.shortRun = 0;
|
|
557
882
|
state.lastShortAt = 0;
|
|
558
883
|
state.lastAssistantText = "";
|
|
559
884
|
state.sameTextRun = 0;
|
|
560
|
-
state.
|
|
885
|
+
state.tools.resetRepeat();
|
|
561
886
|
const cooldown = this.cooldownFor(state);
|
|
562
887
|
const remaining = cooldown - (Date.now() - state.lastAttemptAt);
|
|
563
888
|
if (remaining > 0) {
|
|
@@ -578,22 +903,22 @@ ${event.data.arguments}`;
|
|
|
578
903
|
state.consecutive = 0;
|
|
579
904
|
state.pendingRecoveryAt = 0;
|
|
580
905
|
}
|
|
581
|
-
} else if (
|
|
582
|
-
} else if (
|
|
906
|
+
} else if (reasonKind === "blocked") {
|
|
907
|
+
} else if (reasonKind === "interrupted") {
|
|
583
908
|
state.consecutive = 0;
|
|
584
909
|
state.pendingRecoveryAt = 0;
|
|
585
|
-
} else if (
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
910
|
+
} else if (reasonKind === "error") {
|
|
911
|
+
const failure = parseFailureFacts(reason.error);
|
|
912
|
+
if (failure === void 0) {
|
|
913
|
+
console.error(`[auto-continue] 忽略畸形 turn/end ${sessionId}: error details 无法解释`);
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
state.lastFailure = failure;
|
|
592
917
|
state.lastTurn = event.data.turn;
|
|
593
918
|
state.lastFailureAt = Date.now();
|
|
594
919
|
this.noteRecovery(sessionId, "error");
|
|
595
920
|
this.onTurnFailure(sessionId, "turn/end:error", state.lastFailure);
|
|
596
|
-
} else if (
|
|
921
|
+
} else if (reasonKind === "max-tokens") {
|
|
597
922
|
state.lastFailureAt = Date.now();
|
|
598
923
|
this.noteRecovery(sessionId, "error");
|
|
599
924
|
this.schedule(sessionId, "turn/end:max-tokens");
|
|
@@ -621,6 +946,7 @@ ${event.data.arguments}`;
|
|
|
621
946
|
this.bumpStat({ skipped: 1, code: failure.code });
|
|
622
947
|
if (config.notify) {
|
|
623
948
|
this.notify(
|
|
949
|
+
sessionId,
|
|
624
950
|
copy.notContinuedTitle,
|
|
625
951
|
copy.permanentErrorBody(sessionId, summary),
|
|
626
952
|
this.notifyOptions(sessionId, config.locale)
|
|
@@ -666,11 +992,12 @@ ${event.data.arguments}`;
|
|
|
666
992
|
}
|
|
667
993
|
}
|
|
668
994
|
/** 通知桥: 产生一条通知事件, SSE 端点推给 browser 侧展示。 */
|
|
669
|
-
notify(title, body, options) {
|
|
995
|
+
notify(sessionId, title, body, options) {
|
|
670
996
|
const notice = {
|
|
671
997
|
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
672
998
|
title,
|
|
673
999
|
body,
|
|
1000
|
+
sessionId,
|
|
674
1001
|
...options?.actions !== void 0 && options.actions.length > 0 ? { actions: options.actions } : { actions: [] },
|
|
675
1002
|
at: Date.now()
|
|
676
1003
|
};
|
|
@@ -787,22 +1114,26 @@ ${event.data.arguments}`;
|
|
|
787
1114
|
}
|
|
788
1115
|
state.lastAttemptAt = Date.now();
|
|
789
1116
|
try {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1117
|
+
const message = createUserMessage({
|
|
1118
|
+
content: [{ type: "text", text }],
|
|
1119
|
+
source: { kind: "user" }
|
|
1120
|
+
});
|
|
1121
|
+
trackPendingEcho(state, message.id);
|
|
1122
|
+
try {
|
|
1123
|
+
agent.followup(message);
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
forgetPendingEcho(state, message.id);
|
|
1126
|
+
throw error;
|
|
1127
|
+
}
|
|
796
1128
|
const now = Date.now();
|
|
797
1129
|
state.consecutive += 1;
|
|
798
|
-
state.lastAutoAt = now;
|
|
799
|
-
state.lastSentText = text;
|
|
800
1130
|
state.pendingRecoveryAt = now;
|
|
801
1131
|
this.bumpStat({ sent: 1, ...state.lastFailure !== void 0 ? { code: state.lastFailure.code } : {} });
|
|
802
1132
|
this.log(`已自动发送「${text}」到 ${sessionId}(${reason}), 第 ${state.consecutive} 次连续`);
|
|
803
1133
|
if (config.notify) {
|
|
804
1134
|
const copy = NOTICE_COPY[config.locale];
|
|
805
1135
|
this.notify(
|
|
1136
|
+
sessionId,
|
|
806
1137
|
copy.continuedTitle,
|
|
807
1138
|
copy.continuedBody(sessionId, text, state.consecutive),
|
|
808
1139
|
this.notifyOptions(sessionId, config.locale)
|
|
@@ -814,6 +1145,7 @@ ${event.data.arguments}`;
|
|
|
814
1145
|
if (config.notify) {
|
|
815
1146
|
const copy = NOTICE_COPY[config.locale];
|
|
816
1147
|
this.notify(
|
|
1148
|
+
sessionId,
|
|
817
1149
|
copy.stoppedTitle,
|
|
818
1150
|
copy.stoppedBody(sessionId, state.consecutive),
|
|
819
1151
|
this.notifyOptions(sessionId, config.locale)
|
|
@@ -834,7 +1166,7 @@ ${event.data.arguments}`;
|
|
|
834
1166
|
buildContinueText(config, state, template) {
|
|
835
1167
|
let text = fillTemplate(template, {
|
|
836
1168
|
facts: state.lastFailure,
|
|
837
|
-
tool: state.lastTool,
|
|
1169
|
+
tool: state.tools.lastTool(),
|
|
838
1170
|
turn: state.lastTurn,
|
|
839
1171
|
errorCount: state.consecutive + 1,
|
|
840
1172
|
elapsedMs: state.lastFailureAt > 0 ? Date.now() - state.lastFailureAt : void 0
|
|
@@ -850,12 +1182,7 @@ ${event.data.arguments}`;
|
|
|
850
1182
|
}
|
|
851
1183
|
/** 上一步工具调用的护栏状态(实时路径, 由 mux 帧维护)。 */
|
|
852
1184
|
currentGuard(state) {
|
|
853
|
-
|
|
854
|
-
if (state.lastToolResult === "pending") return { kind: "pending", tool: state.lastTool };
|
|
855
|
-
if (state.lastToolResult.ok) {
|
|
856
|
-
return { kind: "done", tool: state.lastTool, result: state.lastToolResult.excerpt };
|
|
857
|
-
}
|
|
858
|
-
return { kind: "failed", tool: state.lastTool };
|
|
1185
|
+
return state.tools.guard();
|
|
859
1186
|
}
|
|
860
1187
|
async bootScanLoop() {
|
|
861
1188
|
await this.scanLoop(Infinity, 3e3);
|
|
@@ -888,8 +1215,21 @@ ${event.data.arguments}`;
|
|
|
888
1215
|
for (const agent of this.ctx.agents.list()) {
|
|
889
1216
|
const session = agent.session;
|
|
890
1217
|
if (session.header.origin === "subagent") continue;
|
|
891
|
-
|
|
1218
|
+
const events = snapshotSessionEvents(session);
|
|
1219
|
+
const lastActivityAt = events.reduce(
|
|
1220
|
+
(latest, event) => Math.max(latest, event.time),
|
|
1221
|
+
Number.isFinite(session.header.createdAt) ? session.header.createdAt : 0
|
|
1222
|
+
);
|
|
1223
|
+
candidates.push({
|
|
1224
|
+
sessionId: session.id,
|
|
1225
|
+
events,
|
|
1226
|
+
lastActivityAt,
|
|
1227
|
+
listIndex: candidates.length
|
|
1228
|
+
});
|
|
892
1229
|
}
|
|
1230
|
+
candidates.sort(
|
|
1231
|
+
(left, right) => right.lastActivityAt - left.lastActivityAt || left.listIndex - right.listIndex
|
|
1232
|
+
);
|
|
893
1233
|
for (const candidate of candidates.slice(0, config.scanLimit)) {
|
|
894
1234
|
if (this.disposed) return true;
|
|
895
1235
|
const state = this.state(candidate.sessionId);
|
|
@@ -908,7 +1248,8 @@ ${event.data.arguments}`;
|
|
|
908
1248
|
}
|
|
909
1249
|
if (lastEnd === void 0) continue;
|
|
910
1250
|
const reason = lastEnd.data.reason;
|
|
911
|
-
|
|
1251
|
+
const reasonKind = readReasonKind(reason);
|
|
1252
|
+
if (reasonKind === void 0 || !isNonHumanReason(reasonKind)) continue;
|
|
912
1253
|
if (lastEnd.time < now - config.freshMs) continue;
|
|
913
1254
|
let superseded = false;
|
|
914
1255
|
for (const event of events) {
|
|
@@ -919,30 +1260,27 @@ ${event.data.arguments}`;
|
|
|
919
1260
|
}
|
|
920
1261
|
if (superseded) continue;
|
|
921
1262
|
this.applyGuardFromEvents(state, events, lastEnd.seq);
|
|
922
|
-
|
|
923
|
-
this.
|
|
1263
|
+
const scanReason = `scan:turn/end:${reasonKind}`;
|
|
1264
|
+
this.log(`扫描发现中断 ${candidate.sessionId}(turn/end:${reasonKind}), 交给恢复策略处理`);
|
|
1265
|
+
if (reasonKind === "error") {
|
|
1266
|
+
const failure = parseFailureFacts(reason.error);
|
|
1267
|
+
if (failure === void 0) {
|
|
1268
|
+
console.error(`[auto-continue] 忽略畸形扫描 turn/end ${candidate.sessionId}: error details 无法解释`);
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
state.lastFailure = failure;
|
|
1272
|
+
state.lastTurn = lastEnd.data.turn;
|
|
1273
|
+
state.lastFailureAt = lastEnd.time;
|
|
1274
|
+
this.onTurnFailure(candidate.sessionId, scanReason, state.lastFailure);
|
|
1275
|
+
} else {
|
|
1276
|
+
this.schedule(candidate.sessionId, scanReason);
|
|
1277
|
+
}
|
|
924
1278
|
}
|
|
925
1279
|
return true;
|
|
926
1280
|
}
|
|
927
1281
|
/** 从历史事件恢复上一步工具调用状态(扫描路径的幂等护栏)。 */
|
|
928
1282
|
applyGuardFromEvents(state, events, untilSeq) {
|
|
929
|
-
state.
|
|
930
|
-
state.lastToolResult = void 0;
|
|
931
|
-
let call;
|
|
932
|
-
for (const event of events) {
|
|
933
|
-
if (event.seq >= untilSeq) continue;
|
|
934
|
-
if (event.type === "tool/call") call = event;
|
|
935
|
-
}
|
|
936
|
-
if (call === void 0) return;
|
|
937
|
-
state.lastTool = call.data.name;
|
|
938
|
-
state.lastToolResult = "pending";
|
|
939
|
-
for (const event of events) {
|
|
940
|
-
if (event.seq <= call.seq || event.seq >= untilSeq) continue;
|
|
941
|
-
if (event.type === "tool/result") {
|
|
942
|
-
state.lastToolResult = toolResultFacts(event.data);
|
|
943
|
-
break;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
1283
|
+
state.tools.restore(events, untilSeq);
|
|
946
1284
|
}
|
|
947
1285
|
};
|
|
948
1286
|
|