Auto-continue after a phantom-notification dead-end instead of just warning
Extends the phantom-notification guard: when a non-paired turn dead-ends on an "I'll wait … I'll be notified" final with no watcher registered, re-invoke the agent once (inject a corrective nudge as an inbound instruction + re-enqueue) so it actually finishes the work in the foreground, rather than only appending a "send a follow-up" notice. A hard per-chat cap (max 1 re-run per phantom streak, reset on any clean final) makes it loop-safe — a model that keeps producing the dead-end can never spin the bot. Paired rooms are excluded because they already continue on their own (owner → reviewer → …). Pure cap/decision logic is unit- tested; reuses the proven ipc_injected_human + enqueueMessageCheck path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -51,3 +51,61 @@ export function applyPhantomNotificationGuard(
|
||||
if (text.includes(NOTICE_MARKER)) return text;
|
||||
return `${text}${PHANTOM_NOTIFICATION_NOTICE}`;
|
||||
}
|
||||
|
||||
// ── Auto-continue after a phantom dead-end ────────────────────────────────
|
||||
//
|
||||
// When a non-paired turn dead-ends on a phantom "I'll be notified" final with no
|
||||
// watcher, instead of just leaving a notice we re-invoke the agent once so it
|
||||
// actually finishes the work in the foreground. A hard per-chat cap makes this
|
||||
// safe: at most MAX_PHANTOM_AUTO_CONTINUES re-runs per phantom streak, so a model
|
||||
// that keeps producing the dead-end can never spin the bot in a loop. The streak
|
||||
// resets as soon as any clean (non-phantom) final is produced.
|
||||
|
||||
const MAX_PHANTOM_AUTO_CONTINUES = 1;
|
||||
const phantomAutoContinueStreak = new Map<string, number>();
|
||||
|
||||
/** Corrective nudge injected (as an inbound instruction) before the re-run. */
|
||||
export const PHANTOM_CONTINUE_NUDGE =
|
||||
'[시스템 자동 안내] 방금 실제 감시 작업(watch_ci 등)을 등록하지 않은 채 "기다렸다가 알림을 받겠다"는 식으로 응답을 끝냈습니다. 이 환경에는 백그라운드 완료 알림이 없어, 그대로 두면 아무것도 다시 시작되지 않습니다. 지금 바로 포그라운드에서 작업을 끝까지 진행해 실제 결과를 보고하세요. 정말 오래 걸리는 작업이면 watch_ci/schedule_task로 감시를 등록한 뒤 그 사실을 알려주세요. 다시는 "알림을 받겠다"며 끝내지 마세요.';
|
||||
|
||||
export type PhantomFinalOutcome =
|
||||
| { kind: 'clean'; text: string }
|
||||
| { kind: 'auto-continue'; text: string }
|
||||
| { kind: 'notice'; text: string };
|
||||
|
||||
/**
|
||||
* Decide what to do with a final that may be a phantom dead-end. Pure except for
|
||||
* the per-chat streak counter, which is the loop guard.
|
||||
*
|
||||
* - `clean`: not a dead-end (or a watcher is active) — deliver as-is; resets streak.
|
||||
* - `auto-continue`: dead-end, under the cap — deliver as-is, and the caller must
|
||||
* re-invoke the agent once with PHANTOM_CONTINUE_NUDGE.
|
||||
* - `notice`: dead-end but the cap is exhausted (or auto-continue disabled) —
|
||||
* deliver with the follow-up notice appended so the user isn't left hanging.
|
||||
*/
|
||||
export function resolvePhantomFinalOutcome(args: {
|
||||
chatJid: string;
|
||||
text: string;
|
||||
hasActiveWatcher: boolean;
|
||||
canAutoContinue: boolean;
|
||||
}): PhantomFinalOutcome {
|
||||
const { chatJid, text, hasActiveWatcher, canAutoContinue } = args;
|
||||
if (hasActiveWatcher || !isPhantomNotificationDeadEnd(text)) {
|
||||
phantomAutoContinueStreak.delete(chatJid);
|
||||
return { kind: 'clean', text };
|
||||
}
|
||||
const prior = phantomAutoContinueStreak.get(chatJid) ?? 0;
|
||||
if (canAutoContinue && prior < MAX_PHANTOM_AUTO_CONTINUES) {
|
||||
phantomAutoContinueStreak.set(chatJid, prior + 1);
|
||||
return { kind: 'auto-continue', text };
|
||||
}
|
||||
return {
|
||||
kind: 'notice',
|
||||
text: applyPhantomNotificationGuard(text, { hasActiveWatcher: false }),
|
||||
};
|
||||
}
|
||||
|
||||
/** Test-only: clear the per-chat auto-continue streak counters. */
|
||||
export function _resetPhantomAutoContinueForTests(): void {
|
||||
phantomAutoContinueStreak.clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user