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:
@@ -1,9 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
applyPhantomNotificationGuard,
|
||||
isPhantomNotificationDeadEnd,
|
||||
PHANTOM_NOTIFICATION_NOTICE,
|
||||
resolvePhantomFinalOutcome,
|
||||
_resetPhantomAutoContinueForTests,
|
||||
} from './phantom-notification-guard.js';
|
||||
|
||||
describe('isPhantomNotificationDeadEnd', () => {
|
||||
@@ -16,9 +18,9 @@ describe('isPhantomNotificationDeadEnd', () => {
|
||||
});
|
||||
|
||||
it('detects close variants of the notification promise', () => {
|
||||
expect(isPhantomNotificationDeadEnd('I will be notified when it finishes.')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isPhantomNotificationDeadEnd('I will be notified when it finishes.'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPhantomNotificationDeadEnd('Waiting for the deploy to finish now.'),
|
||||
).toBe(true);
|
||||
@@ -74,3 +76,85 @@ describe('applyPhantomNotificationGuard', () => {
|
||||
expect(twice).toBe(once);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePhantomFinalOutcome', () => {
|
||||
const deadEnd = "I'll wait for the build to complete — I'll be notified.";
|
||||
const chatJid = 'dc:test-room';
|
||||
|
||||
beforeEach(() => {
|
||||
_resetPhantomAutoContinueForTests();
|
||||
});
|
||||
|
||||
it('auto-continues once, then falls back to the notice on a repeated dead-end', () => {
|
||||
const first = resolvePhantomFinalOutcome({
|
||||
chatJid,
|
||||
text: deadEnd,
|
||||
hasActiveWatcher: false,
|
||||
canAutoContinue: true,
|
||||
});
|
||||
expect(first.kind).toBe('auto-continue');
|
||||
|
||||
// Second consecutive dead-end for the same chat hits the cap → notice, no
|
||||
// further auto-continue (this is the loop guard).
|
||||
const second = resolvePhantomFinalOutcome({
|
||||
chatJid,
|
||||
text: deadEnd,
|
||||
hasActiveWatcher: false,
|
||||
canAutoContinue: true,
|
||||
});
|
||||
expect(second.kind).toBe('notice');
|
||||
expect(second.text).toContain(PHANTOM_NOTIFICATION_NOTICE.trim());
|
||||
});
|
||||
|
||||
it('resets the streak after a clean final so a later dead-end can auto-continue again', () => {
|
||||
expect(
|
||||
resolvePhantomFinalOutcome({
|
||||
chatJid,
|
||||
text: deadEnd,
|
||||
hasActiveWatcher: false,
|
||||
canAutoContinue: true,
|
||||
}).kind,
|
||||
).toBe('auto-continue');
|
||||
|
||||
// A clean final resets the streak.
|
||||
expect(
|
||||
resolvePhantomFinalOutcome({
|
||||
chatJid,
|
||||
text: 'TASK_DONE\n작업을 마쳤습니다.',
|
||||
hasActiveWatcher: false,
|
||||
canAutoContinue: true,
|
||||
}).kind,
|
||||
).toBe('clean');
|
||||
|
||||
// Streak reset → next dead-end auto-continues again.
|
||||
expect(
|
||||
resolvePhantomFinalOutcome({
|
||||
chatJid,
|
||||
text: deadEnd,
|
||||
hasActiveWatcher: false,
|
||||
canAutoContinue: true,
|
||||
}).kind,
|
||||
).toBe('auto-continue');
|
||||
});
|
||||
|
||||
it('never auto-continues when a watcher is active (legitimate wait)', () => {
|
||||
expect(
|
||||
resolvePhantomFinalOutcome({
|
||||
chatJid,
|
||||
text: deadEnd,
|
||||
hasActiveWatcher: true,
|
||||
canAutoContinue: true,
|
||||
}).kind,
|
||||
).toBe('clean');
|
||||
});
|
||||
|
||||
it('emits the notice (never auto-continue) when auto-continue is disabled', () => {
|
||||
const outcome = resolvePhantomFinalOutcome({
|
||||
chatJid,
|
||||
text: deadEnd,
|
||||
hasActiveWatcher: false,
|
||||
canAutoContinue: false,
|
||||
});
|
||||
expect(outcome.kind).toBe('notice');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user