style: sync pre-commit formatting fallout
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
|||||||
classifyClaudeAuthError,
|
classifyClaudeAuthError,
|
||||||
detectClaudeProviderFailureMessage,
|
detectClaudeProviderFailureMessage,
|
||||||
isClaudeOrgAccessDeniedMessage,
|
isClaudeOrgAccessDeniedMessage,
|
||||||
|
|
||||||
shouldRotateClaudeToken,
|
shouldRotateClaudeToken,
|
||||||
} from './agent-error-detection.js';
|
} from './agent-error-detection.js';
|
||||||
|
|
||||||
@@ -64,5 +63,4 @@ describe('agent-error-detection', () => {
|
|||||||
expect(shouldRotateClaudeToken('overloaded')).toBe(false);
|
expect(shouldRotateClaudeToken('overloaded')).toBe(false);
|
||||||
expect(shouldRotateClaudeToken('success-null-result')).toBe(false);
|
expect(shouldRotateClaudeToken('success-null-result')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ vi.mock('./group-folder.js', () => ({
|
|||||||
folder: string,
|
folder: string,
|
||||||
serviceId: string,
|
serviceId: string,
|
||||||
taskId: string,
|
taskId: string,
|
||||||
) => `${process.env.EJ_TEST_ROOT}/task-sessions/${folder}/${serviceId}/${taskId}`,
|
) =>
|
||||||
|
`${process.env.EJ_TEST_ROOT}/task-sessions/${folder}/${serviceId}/${taskId}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('os', async () => {
|
vi.mock('os', async () => {
|
||||||
|
|||||||
@@ -273,7 +273,10 @@ function prepareCodexSessionEnvironment(args: {
|
|||||||
: [
|
: [
|
||||||
readPlatformPrompt('codex', args.projectRoot),
|
readPlatformPrompt('codex', args.projectRoot),
|
||||||
isReviewService(SERVICE_ID)
|
isReviewService(SERVICE_ID)
|
||||||
? readOptionalPromptFile(args.projectRoot, 'codex-review-platform.md')
|
? readOptionalPromptFile(
|
||||||
|
args.projectRoot,
|
||||||
|
'codex-review-platform.md',
|
||||||
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
args.isPairedRoom
|
args.isPairedRoom
|
||||||
? readPairedRoomPrompt('codex', args.projectRoot)
|
? readPairedRoomPrompt('codex', args.projectRoot)
|
||||||
|
|||||||
@@ -137,12 +137,10 @@ export function isSessionCommandSenderAllowed(sender: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delta handoff: cross-provider session continuity with probe + fallback
|
// Delta handoff: cross-provider session continuity with probe + fallback
|
||||||
export const DELTA_HANDOFF_ENABLED =
|
export const DELTA_HANDOFF_ENABLED = getEnv('DELTA_HANDOFF_ENABLED') === 'true';
|
||||||
getEnv('DELTA_HANDOFF_ENABLED') === 'true';
|
|
||||||
|
|
||||||
// Comma-separated list of group folders for canary testing
|
// Comma-separated list of group folders for canary testing
|
||||||
const rawDeltaHandoffCanaryGroups =
|
const rawDeltaHandoffCanaryGroups = getEnv('DELTA_HANDOFF_CANARY_GROUPS') || '';
|
||||||
getEnv('DELTA_HANDOFF_CANARY_GROUPS') || '';
|
|
||||||
export const DELTA_HANDOFF_CANARY_GROUPS = new Set(
|
export const DELTA_HANDOFF_CANARY_GROUPS = new Set(
|
||||||
rawDeltaHandoffCanaryGroups
|
rawDeltaHandoffCanaryGroups
|
||||||
.split(',')
|
.split(',')
|
||||||
|
|||||||
12
src/db.ts
12
src/db.ts
@@ -361,7 +361,9 @@ function createSchema(database: Database.Database): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
database.exec(`ALTER TABLE work_items ADD COLUMN service_id TEXT DEFAULT ''`);
|
database.exec(
|
||||||
|
`ALTER TABLE work_items ADD COLUMN service_id TEXT DEFAULT ''`,
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
/* column already exists */
|
/* column already exists */
|
||||||
}
|
}
|
||||||
@@ -884,9 +886,7 @@ export function getRecentChatMessages(
|
|||||||
LIMIT ?
|
LIMIT ?
|
||||||
) ORDER BY timestamp
|
) ORDER BY timestamp
|
||||||
`;
|
`;
|
||||||
const rows = db
|
const rows = db.prepare(sql).all(chatJid, limit) as Array<
|
||||||
.prepare(sql)
|
|
||||||
.all(chatJid, limit) as Array<
|
|
||||||
NewMessage & {
|
NewMessage & {
|
||||||
is_from_me?: boolean | number;
|
is_from_me?: boolean | number;
|
||||||
is_bot_message?: boolean | number;
|
is_bot_message?: boolean | number;
|
||||||
@@ -1834,7 +1834,9 @@ function parseLastAgentSeqState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
throw new Error(`Invalid last_agent_seq JSON for ${serviceId}: not an object`);
|
throw new Error(
|
||||||
|
`Invalid last_agent_seq JSON for ${serviceId}: not an object`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cursors: Record<string, string> = {};
|
const cursors: Record<string, string> = {};
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper to create a message
|
// Helper to create a message
|
||||||
const createMessage = (overrides: Partial<{
|
const createMessage = (
|
||||||
|
overrides: Partial<{
|
||||||
id: string;
|
id: string;
|
||||||
chat_jid: string;
|
chat_jid: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
@@ -28,7 +29,8 @@ const createMessage = (overrides: Partial<{
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
is_from_me: boolean;
|
is_from_me: boolean;
|
||||||
is_bot_message: boolean;
|
is_bot_message: boolean;
|
||||||
}> = {}) => ({
|
}> = {},
|
||||||
|
) => ({
|
||||||
id: overrides.id ?? 'msg-1',
|
id: overrides.id ?? 'msg-1',
|
||||||
chat_jid: overrides.chat_jid ?? 'dc:test-room',
|
chat_jid: overrides.chat_jid ?? 'dc:test-room',
|
||||||
sender: overrides.sender ?? 'user1',
|
sender: overrides.sender ?? 'user1',
|
||||||
@@ -41,7 +43,13 @@ const createMessage = (overrides: Partial<{
|
|||||||
|
|
||||||
// Helper to setup chat metadata
|
// Helper to setup chat metadata
|
||||||
const setupChat = (jid: string) => {
|
const setupChat = (jid: string) => {
|
||||||
storeChatMetadata(jid, new Date().toISOString(), 'Test Chat', 'discord', true);
|
storeChatMetadata(
|
||||||
|
jid,
|
||||||
|
new Date().toISOString(),
|
||||||
|
'Test Chat',
|
||||||
|
'discord',
|
||||||
|
true,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('isPairedRoomJid', () => {
|
describe('isPairedRoomJid', () => {
|
||||||
@@ -103,34 +111,40 @@ describe('getLastBotFinalMessage', () => {
|
|||||||
setupChat(jid);
|
setupChat(jid);
|
||||||
|
|
||||||
// Store older bot message (from any bot)
|
// Store older bot message (from any bot)
|
||||||
storeMessage(createMessage({
|
storeMessage(
|
||||||
|
createMessage({
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
chat_jid: jid,
|
chat_jid: jid,
|
||||||
content: 'First bot message',
|
content: 'First bot message',
|
||||||
timestamp: new Date(now.getTime() - 1000).toISOString(),
|
timestamp: new Date(now.getTime() - 1000).toISOString(),
|
||||||
is_from_me: true,
|
is_from_me: true,
|
||||||
is_bot_message: true,
|
is_bot_message: true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Store newer bot message (from different bot - is_from_me=0 but is_bot_message=1)
|
// Store newer bot message (from different bot - is_from_me=0 but is_bot_message=1)
|
||||||
storeMessage(createMessage({
|
storeMessage(
|
||||||
|
createMessage({
|
||||||
id: 'msg-2',
|
id: 'msg-2',
|
||||||
chat_jid: jid,
|
chat_jid: jid,
|
||||||
content: 'Second bot message',
|
content: 'Second bot message',
|
||||||
timestamp: now.toISOString(),
|
timestamp: now.toISOString(),
|
||||||
is_from_me: false, // Different bot
|
is_from_me: false, // Different bot
|
||||||
is_bot_message: true,
|
is_bot_message: true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Store human message (should not be returned)
|
// Store human message (should not be returned)
|
||||||
storeMessage(createMessage({
|
storeMessage(
|
||||||
|
createMessage({
|
||||||
id: 'msg-3',
|
id: 'msg-3',
|
||||||
chat_jid: jid,
|
chat_jid: jid,
|
||||||
content: 'Human message',
|
content: 'Human message',
|
||||||
timestamp: new Date(now.getTime() + 1000).toISOString(),
|
timestamp: new Date(now.getTime() + 1000).toISOString(),
|
||||||
is_from_me: false,
|
is_from_me: false,
|
||||||
is_bot_message: false,
|
is_bot_message: false,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const lastMessages = getLastBotFinalMessage(jid, 'claude-code', 1);
|
const lastMessages = getLastBotFinalMessage(jid, 'claude-code', 1);
|
||||||
expect(lastMessages).toHaveLength(1);
|
expect(lastMessages).toHaveLength(1);
|
||||||
@@ -183,21 +197,27 @@ describe('isDuplicateOfLastBotFinal (runtime function)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Store a bot message (from any bot)
|
// Store a bot message (from any bot)
|
||||||
storeMessage(createMessage({
|
storeMessage(
|
||||||
|
createMessage({
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
chat_jid: jid,
|
chat_jid: jid,
|
||||||
content: 'DONE — Task completed successfully',
|
content: 'DONE — Task completed successfully',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
is_from_me: false, // Different bot
|
is_from_me: false, // Different bot
|
||||||
is_bot_message: true,
|
is_bot_message: true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Verify it's a paired room
|
// Verify it's a paired room
|
||||||
expect(isPairedRoomJid(jid)).toBe(true);
|
expect(isPairedRoomJid(jid)).toBe(true);
|
||||||
|
|
||||||
// Verify duplicate detection works via the actual runtime function
|
// Verify duplicate detection works via the actual runtime function
|
||||||
expect(isDuplicateOfLastBotFinal(jid, 'DONE — Task completed successfully')).toBe(true);
|
expect(
|
||||||
expect(isDuplicateOfLastBotFinal(jid, 'done — task completed successfully')).toBe(true); // Normalized match
|
isDuplicateOfLastBotFinal(jid, 'DONE — Task completed successfully'),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isDuplicateOfLastBotFinal(jid, 'done — task completed successfully'),
|
||||||
|
).toBe(true); // Normalized match
|
||||||
});
|
});
|
||||||
|
|
||||||
it('non-paired room: duplicate check is bypassed', () => {
|
it('non-paired room: duplicate check is bypassed', () => {
|
||||||
@@ -214,14 +234,16 @@ describe('isDuplicateOfLastBotFinal (runtime function)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Store a bot message
|
// Store a bot message
|
||||||
storeMessage(createMessage({
|
storeMessage(
|
||||||
|
createMessage({
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
chat_jid: jid,
|
chat_jid: jid,
|
||||||
content: 'DONE — Task completed',
|
content: 'DONE — Task completed',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
is_from_me: true,
|
is_from_me: true,
|
||||||
is_bot_message: true,
|
is_bot_message: true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Verify it's NOT a paired room
|
// Verify it's NOT a paired room
|
||||||
expect(isPairedRoomJid(jid)).toBe(false);
|
expect(isPairedRoomJid(jid)).toBe(false);
|
||||||
@@ -252,17 +274,21 @@ describe('isDuplicateOfLastBotFinal (runtime function)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Store a bot message
|
// Store a bot message
|
||||||
storeMessage(createMessage({
|
storeMessage(
|
||||||
|
createMessage({
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
chat_jid: jid,
|
chat_jid: jid,
|
||||||
content: 'First message content',
|
content: 'First message content',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
is_from_me: true,
|
is_from_me: true,
|
||||||
is_bot_message: true,
|
is_bot_message: true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Verify different content is not a duplicate
|
// Verify different content is not a duplicate
|
||||||
expect(isDuplicateOfLastBotFinal(jid, 'Different message content')).toBe(false);
|
expect(isDuplicateOfLastBotFinal(jid, 'Different message content')).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
expect(isDuplicateOfLastBotFinal(jid, 'First message content')).toBe(true);
|
expect(isDuplicateOfLastBotFinal(jid, 'First message content')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -288,17 +314,21 @@ describe('isDuplicateOfLastBotFinal (runtime function)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Store a bot message from "codex" (is_from_me=0)
|
// Store a bot message from "codex" (is_from_me=0)
|
||||||
storeMessage(createMessage({
|
storeMessage(
|
||||||
|
createMessage({
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
chat_jid: jid,
|
chat_jid: jid,
|
||||||
content: 'DONE — Analysis complete',
|
content: 'DONE — Analysis complete',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
is_from_me: false, // Other bot
|
is_from_me: false, // Other bot
|
||||||
is_bot_message: true,
|
is_bot_message: true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Verify claude service detects this as duplicate (cross-bot detection)
|
// Verify claude service detects this as duplicate (cross-bot detection)
|
||||||
expect(isDuplicateOfLastBotFinal(jid, 'DONE — Analysis complete')).toBe(true);
|
expect(isDuplicateOfLastBotFinal(jid, 'DONE — Analysis complete')).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('normalization handles whitespace and case differences', () => {
|
it('normalization handles whitespace and case differences', () => {
|
||||||
@@ -323,18 +353,27 @@ describe('isDuplicateOfLastBotFinal (runtime function)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Store a bot message with specific formatting
|
// Store a bot message with specific formatting
|
||||||
storeMessage(createMessage({
|
storeMessage(
|
||||||
|
createMessage({
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
chat_jid: jid,
|
chat_jid: jid,
|
||||||
content: 'DONE — Task completed\n\nSuccessfully',
|
content: 'DONE — Task completed\n\nSuccessfully',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
is_from_me: true,
|
is_from_me: true,
|
||||||
is_bot_message: true,
|
is_bot_message: true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Same content with different whitespace should be detected as duplicate
|
// Same content with different whitespace should be detected as duplicate
|
||||||
expect(isDuplicateOfLastBotFinal(jid, 'done — task completed successfully')).toBe(true);
|
expect(
|
||||||
expect(isDuplicateOfLastBotFinal(jid, ' DONE — Task completed Successfully ')).toBe(true);
|
isDuplicateOfLastBotFinal(jid, 'done — task completed successfully'),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isDuplicateOfLastBotFinal(
|
||||||
|
jid,
|
||||||
|
' DONE — Task completed Successfully ',
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
// Different content should not be duplicate
|
// Different content should not be duplicate
|
||||||
expect(isDuplicateOfLastBotFinal(jid, 'FAILED — Task failed')).toBe(false);
|
expect(isDuplicateOfLastBotFinal(jid, 'FAILED — Task failed')).toBe(false);
|
||||||
@@ -362,14 +401,16 @@ describe('isDuplicateOfLastBotFinal (runtime function)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Store a bot message (simulating previous delivery)
|
// Store a bot message (simulating previous delivery)
|
||||||
storeMessage(createMessage({
|
storeMessage(
|
||||||
|
createMessage({
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
chat_jid: jid,
|
chat_jid: jid,
|
||||||
content: 'DONE — Task completed successfully',
|
content: 'DONE — Task completed successfully',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
is_from_me: true,
|
is_from_me: true,
|
||||||
is_bot_message: true,
|
is_bot_message: true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Create a work item with the same content (duplicate)
|
// Create a work item with the same content (duplicate)
|
||||||
const workItem = createProducedWorkItem({
|
const workItem = createProducedWorkItem({
|
||||||
@@ -416,14 +457,16 @@ describe('isDuplicateOfLastBotFinal (runtime function)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Store a bot message
|
// Store a bot message
|
||||||
storeMessage(createMessage({
|
storeMessage(
|
||||||
|
createMessage({
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
chat_jid: jid,
|
chat_jid: jid,
|
||||||
content: 'First message',
|
content: 'First message',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
is_from_me: true,
|
is_from_me: true,
|
||||||
is_bot_message: true,
|
is_bot_message: true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Create a work item with DIFFERENT content (non-duplicate)
|
// Create a work item with DIFFERENT content (non-duplicate)
|
||||||
const workItem = createProducedWorkItem({
|
const workItem = createProducedWorkItem({
|
||||||
|
|||||||
@@ -75,7 +75,12 @@ export function resolveServiceGroupSessionsPath(
|
|||||||
assertValidGroupFolder(folder);
|
assertValidGroupFolder(folder);
|
||||||
assertValidRuntimeSegment(serviceId, 'service ID');
|
assertValidRuntimeSegment(serviceId, 'service ID');
|
||||||
const sessionsBaseDir = path.resolve(DATA_DIR, 'sessions');
|
const sessionsBaseDir = path.resolve(DATA_DIR, 'sessions');
|
||||||
const sessionsPath = path.resolve(sessionsBaseDir, folder, 'services', serviceId);
|
const sessionsPath = path.resolve(
|
||||||
|
sessionsBaseDir,
|
||||||
|
folder,
|
||||||
|
'services',
|
||||||
|
serviceId,
|
||||||
|
);
|
||||||
ensureWithinBase(sessionsBaseDir, sessionsPath);
|
ensureWithinBase(sessionsBaseDir, sessionsPath);
|
||||||
return sessionsPath;
|
return sessionsPath;
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/index.ts
16
src/index.ts
@@ -71,7 +71,10 @@ import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
|||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { normalizeStoredSeqCursor } from './message-cursor.js';
|
import { normalizeStoredSeqCursor } from './message-cursor.js';
|
||||||
import { initCodexTokenRotation } from './codex-token-rotation.js';
|
import { initCodexTokenRotation } from './codex-token-rotation.js';
|
||||||
import { hasAvailableClaudeToken, initTokenRotation } from './token-rotation.js';
|
import {
|
||||||
|
hasAvailableClaudeToken,
|
||||||
|
initTokenRotation,
|
||||||
|
} from './token-rotation.js';
|
||||||
import {
|
import {
|
||||||
shouldStartTokenRefreshLoop,
|
shouldStartTokenRefreshLoop,
|
||||||
startTokenRefreshLoop,
|
startTokenRefreshLoop,
|
||||||
@@ -442,7 +445,10 @@ async function main(): Promise<void> {
|
|||||||
editFormattedTrackedChannelMessage(channels, jid, messageId, rawText),
|
editFormattedTrackedChannelMessage(channels, jid, messageId, rawText),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.info({ serviceId: SERVICE_ID }, 'Skipping scheduler for review service');
|
logger.info(
|
||||||
|
{ serviceId: SERVICE_ID },
|
||||||
|
'Skipping scheduler for review service',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
startIpcWatcher({
|
startIpcWatcher({
|
||||||
sendMessage: (jid, text) => {
|
sendMessage: (jid, text) => {
|
||||||
@@ -536,7 +542,11 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
restoreDefaultChannelLease(lease.chatJid);
|
restoreDefaultChannelLease(lease.chatJid);
|
||||||
logger.info(
|
logger.info(
|
||||||
{ chatJid: lease.chatJid, serviceId: SERVICE_ID, elapsedMin: Math.round(elapsed / 60_000) },
|
{
|
||||||
|
chatJid: lease.chatJid,
|
||||||
|
serviceId: SERVICE_ID,
|
||||||
|
elapsedMin: Math.round(elapsed / 60_000),
|
||||||
|
},
|
||||||
'Claude token available and failover hold period elapsed, restored default channel lease',
|
'Claude token available and failover hold period elapsed, restored default channel lease',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ vi.mock('./logger.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./agent-error-detection.js', async (importOriginal) => {
|
vi.mock('./agent-error-detection.js', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('./agent-error-detection.js')>();
|
const actual =
|
||||||
|
await importOriginal<typeof import('./agent-error-detection.js')>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
classifyRotationTrigger: vi.fn((error?: string | null) => {
|
classifyRotationTrigger: vi.fn((error?: string | null) => {
|
||||||
@@ -528,7 +529,8 @@ describe('runAgentForGroup Claude rotation', () => {
|
|||||||
await onOutput?.({
|
await onOutput?.({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
phase: 'final',
|
phase: 'final',
|
||||||
result: 'You\u2019re out of extra usage \u00b7 resets 4am (Asia/Seoul)',
|
result:
|
||||||
|
'You\u2019re out of extra usage \u00b7 resets 4am (Asia/Seoul)',
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
|
|||||||
@@ -20,12 +20,19 @@ import {
|
|||||||
shouldResetSessionOnAgentFailure,
|
shouldResetSessionOnAgentFailure,
|
||||||
shouldRetryFreshSessionOnAgentFailure,
|
shouldRetryFreshSessionOnAgentFailure,
|
||||||
} from './session-recovery.js';
|
} from './session-recovery.js';
|
||||||
import { CODEX_MAIN_SERVICE_ID, CODEX_REVIEW_SERVICE_ID, SERVICE_SESSION_SCOPE } from './config.js';
|
import {
|
||||||
|
CODEX_MAIN_SERVICE_ID,
|
||||||
|
CODEX_REVIEW_SERVICE_ID,
|
||||||
|
SERVICE_SESSION_SCOPE,
|
||||||
|
} from './config.js';
|
||||||
import {
|
import {
|
||||||
buildSuppressTokenPrompt,
|
buildSuppressTokenPrompt,
|
||||||
classifySuppressTokenOutput,
|
classifySuppressTokenOutput,
|
||||||
} from './output-suppression.js';
|
} from './output-suppression.js';
|
||||||
import { activateCodexFailover, getEffectiveChannelLease } from './service-routing.js';
|
import {
|
||||||
|
activateCodexFailover,
|
||||||
|
getEffectiveChannelLease,
|
||||||
|
} from './service-routing.js';
|
||||||
import {
|
import {
|
||||||
evaluateStreamedOutput,
|
evaluateStreamedOutput,
|
||||||
type StreamedOutputState,
|
type StreamedOutputState,
|
||||||
@@ -111,7 +118,8 @@ export async function runAgentForGroup(
|
|||||||
|
|
||||||
const canRotateToken = isClaudeCodeAgent && getTokenCount() > 1;
|
const canRotateToken = isClaudeCodeAgent && getTokenCount() > 1;
|
||||||
const currentLease = getEffectiveChannelLease(chatJid);
|
const currentLease = getEffectiveChannelLease(chatJid);
|
||||||
const reviewerMode = currentLease.reviewer_service_id === SERVICE_SESSION_SCOPE;
|
const reviewerMode =
|
||||||
|
currentLease.reviewer_service_id === SERVICE_SESSION_SCOPE;
|
||||||
const effectivePrompt = buildSuppressTokenPrompt(prompt, suppressToken, {
|
const effectivePrompt = buildSuppressTokenPrompt(prompt, suppressToken, {
|
||||||
reviewerMode,
|
reviewerMode,
|
||||||
});
|
});
|
||||||
@@ -569,11 +577,7 @@ export async function runAgentForGroup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (primaryAttempt.error) {
|
if (primaryAttempt.error) {
|
||||||
if (
|
if (canRotateToken && provider === 'claude' && !primaryAttempt.sawOutput) {
|
||||||
canRotateToken &&
|
|
||||||
provider === 'claude' &&
|
|
||||||
!primaryAttempt.sawOutput
|
|
||||||
) {
|
|
||||||
const errMsg = getErrorMessage(primaryAttempt.error);
|
const errMsg = getErrorMessage(primaryAttempt.error);
|
||||||
const trigger = primaryAttempt.streamedTriggerReason
|
const trigger = primaryAttempt.streamedTriggerReason
|
||||||
? {
|
? {
|
||||||
@@ -666,11 +670,7 @@ export async function runAgentForGroup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (output.status === 'error') {
|
if (output.status === 'error') {
|
||||||
if (
|
if (canRotateToken && provider === 'claude' && !primaryAttempt.sawOutput) {
|
||||||
canRotateToken &&
|
|
||||||
provider === 'claude' &&
|
|
||||||
!primaryAttempt.sawOutput
|
|
||||||
) {
|
|
||||||
const trigger = primaryAttempt.streamedTriggerReason
|
const trigger = primaryAttempt.streamedTriggerReason
|
||||||
? {
|
? {
|
||||||
shouldRetry: true,
|
shouldRetry: true,
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ vi.mock('./agent-runner.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./output-suppression.js', async (importOriginal) => {
|
vi.mock('./output-suppression.js', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('./output-suppression.js')>();
|
const actual =
|
||||||
|
await importOriginal<typeof import('./output-suppression.js')>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
createSuppressToken: vi.fn(() => '__TEST_SUPPRESS__'),
|
createSuppressToken: vi.fn(() => '__TEST_SUPPRESS__'),
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ import {
|
|||||||
SERVICE_ID,
|
SERVICE_ID,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import { GroupQueue, GroupRunContext } from './group-queue.js';
|
import { GroupQueue, GroupRunContext } from './group-queue.js';
|
||||||
import { findChannel, formatMessages, normalizeMessageForDedupe } from './router.js';
|
import {
|
||||||
|
findChannel,
|
||||||
|
formatMessages,
|
||||||
|
normalizeMessageForDedupe,
|
||||||
|
} from './router.js';
|
||||||
import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
|
import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
|
||||||
import {
|
import {
|
||||||
advanceLastAgentCursor,
|
advanceLastAgentCursor,
|
||||||
@@ -52,7 +56,10 @@ import { shouldServiceProcessChat } from './service-routing.js';
|
|||||||
* Check if a message is a duplicate of the last bot final message in a paired room.
|
* Check if a message is a duplicate of the last bot final message in a paired room.
|
||||||
* Exported for testing purposes.
|
* Exported for testing purposes.
|
||||||
*/
|
*/
|
||||||
export function isDuplicateOfLastBotFinal(chatJid: string, text: string): boolean {
|
export function isDuplicateOfLastBotFinal(
|
||||||
|
chatJid: string,
|
||||||
|
text: string,
|
||||||
|
): boolean {
|
||||||
// Only check in paired rooms (both claude and codex registered)
|
// Only check in paired rooms (both claude and codex registered)
|
||||||
if (!isPairedRoomJid(chatJid)) {
|
if (!isPairedRoomJid(chatJid)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -115,7 +122,10 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
* Check if a message is a duplicate of the last bot final message in a paired room.
|
* Check if a message is a duplicate of the last bot final message in a paired room.
|
||||||
* Returns true if duplicate (should be suppressed).
|
* Returns true if duplicate (should be suppressed).
|
||||||
*/
|
*/
|
||||||
const checkDuplicateOfLastBotFinal = (chatJid: string, text: string): boolean => {
|
const checkDuplicateOfLastBotFinal = (
|
||||||
|
chatJid: string,
|
||||||
|
text: string,
|
||||||
|
): boolean => {
|
||||||
return isDuplicateOfLastBotFinal(chatJid, text);
|
return isDuplicateOfLastBotFinal(chatJid, text);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,7 +139,10 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
const replaceMessageId = options?.replaceMessageId ?? null;
|
const replaceMessageId = options?.replaceMessageId ?? null;
|
||||||
|
|
||||||
// Check for duplicate in paired rooms before attempting delivery
|
// Check for duplicate in paired rooms before attempting delivery
|
||||||
const isDuplicate = checkDuplicateOfLastBotFinal(item.chat_jid, item.result_payload);
|
const isDuplicate = checkDuplicateOfLastBotFinal(
|
||||||
|
item.chat_jid,
|
||||||
|
item.result_payload,
|
||||||
|
);
|
||||||
|
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
// Mark as delivered without sending, and don't open continuation
|
// Mark as delivered without sending, and don't open continuation
|
||||||
@@ -263,7 +276,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
const isClaudeCodeAgent =
|
const isClaudeCodeAgent =
|
||||||
(group.agentType || 'claude-code') === 'claude-code';
|
(group.agentType || 'claude-code') === 'claude-code';
|
||||||
const suppressToken =
|
const suppressToken =
|
||||||
isClaudeService() || isReviewService() ? createSuppressToken() : undefined;
|
isClaudeService() || isReviewService()
|
||||||
|
? createSuppressToken()
|
||||||
|
: undefined;
|
||||||
const deferTypingUntilVisible = Boolean(suppressToken) && isClaudeService();
|
const deferTypingUntilVisible = Boolean(suppressToken) && isClaudeService();
|
||||||
|
|
||||||
const turnController = new MessageTurnController({
|
const turnController = new MessageTurnController({
|
||||||
@@ -277,7 +292,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
deferTypingUntilVisible,
|
deferTypingUntilVisible,
|
||||||
suppressToken,
|
suppressToken,
|
||||||
clearSession: () => deps.clearSession(group.folder),
|
clearSession: () => deps.clearSession(group.folder),
|
||||||
requestClose: (reason) => deps.queue.closeStdin(chatJid, { runId, reason }),
|
requestClose: (reason) =>
|
||||||
|
deps.queue.closeStdin(chatJid, { runId, reason }),
|
||||||
deliverFinalText: async (text) => {
|
deliverFinalText: async (text) => {
|
||||||
try {
|
try {
|
||||||
const workItem = createProducedWorkItem({
|
const workItem = createProducedWorkItem({
|
||||||
@@ -352,7 +368,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const processClaimedHandoff = async (handoff: ServiceHandoff): Promise<void> => {
|
const processClaimedHandoff = async (
|
||||||
|
handoff: ServiceHandoff,
|
||||||
|
): Promise<void> => {
|
||||||
const group = deps.getRegisteredGroups()[handoff.chat_jid];
|
const group = deps.getRegisteredGroups()[handoff.chat_jid];
|
||||||
if (!group) {
|
if (!group) {
|
||||||
failServiceHandoff(handoff.id, 'Group not registered on target service');
|
failServiceHandoff(handoff.id, 'Group not registered on target service');
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export function shouldEnableSuppressOutputForService(
|
|||||||
if (!serviceId) return false;
|
if (!serviceId) return false;
|
||||||
const normalized = normalizeServiceId(serviceId);
|
const normalized = normalizeServiceId(serviceId);
|
||||||
return (
|
return (
|
||||||
normalized === CLAUDE_SERVICE_ID ||
|
normalized === CLAUDE_SERVICE_ID || normalized === CODEX_REVIEW_SERVICE_ID
|
||||||
normalized === CODEX_REVIEW_SERVICE_ID
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +28,10 @@ export function classifySuppressTokenOutput(
|
|||||||
suppressToken: string | undefined,
|
suppressToken: string | undefined,
|
||||||
): 'exact' | 'mixed' | 'none' {
|
): 'exact' | 'mixed' | 'none' {
|
||||||
const trimmed = rawText.trim();
|
const trimmed = rawText.trim();
|
||||||
if ((suppressToken && trimmed === suppressToken) || EXACT_ANY_SUPPRESS_TOKEN_PATTERN.test(trimmed)) {
|
if (
|
||||||
|
(suppressToken && trimmed === suppressToken) ||
|
||||||
|
EXACT_ANY_SUPPRESS_TOKEN_PATTERN.test(trimmed)
|
||||||
|
) {
|
||||||
return 'exact';
|
return 'exact';
|
||||||
}
|
}
|
||||||
if (suppressToken && rawText.includes(suppressToken)) {
|
if (suppressToken && rawText.includes(suppressToken)) {
|
||||||
|
|||||||
@@ -99,8 +99,5 @@ export function findChannel(
|
|||||||
* - Collapse consecutive whitespace/newlines into single space
|
* - Collapse consecutive whitespace/newlines into single space
|
||||||
*/
|
*/
|
||||||
export function normalizeMessageForDedupe(text: string): string {
|
export function normalizeMessageForDedupe(text: string): string {
|
||||||
return text
|
return text.trim().replace(/\s+/g, ' ').toLowerCase();
|
||||||
.trim()
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.toLowerCase();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ function getDefaultLease(chatJid: string): EffectiveChannelLease {
|
|||||||
return {
|
return {
|
||||||
chat_jid: chatJid,
|
chat_jid: chatJid,
|
||||||
owner_service_id:
|
owner_service_id:
|
||||||
SERVICE_AGENT_TYPE === 'codex' ? CODEX_MAIN_SERVICE_ID : CLAUDE_SERVICE_ID,
|
SERVICE_AGENT_TYPE === 'codex'
|
||||||
|
? CODEX_MAIN_SERVICE_ID
|
||||||
|
: CLAUDE_SERVICE_ID,
|
||||||
reviewer_service_id: null,
|
reviewer_service_id: null,
|
||||||
activated_at: null,
|
activated_at: null,
|
||||||
reason: null,
|
reason: null,
|
||||||
@@ -106,7 +108,9 @@ export function refreshChannelOwnerCache(force = false): void {
|
|||||||
lastLeaseRefreshAt = now;
|
lastLeaseRefreshAt = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEffectiveChannelLease(chatJid: string): EffectiveChannelLease {
|
export function getEffectiveChannelLease(
|
||||||
|
chatJid: string,
|
||||||
|
): EffectiveChannelLease {
|
||||||
refreshChannelOwnerCache();
|
refreshChannelOwnerCache();
|
||||||
const row = leaseCache.get(chatJid);
|
const row = leaseCache.get(chatJid);
|
||||||
if (row) {
|
if (row) {
|
||||||
@@ -180,7 +184,10 @@ export function getActiveCodexFailoverLeases(): ActiveFailoverLease[] {
|
|||||||
normalizeServiceId(row.reviewer_service_id || '') ===
|
normalizeServiceId(row.reviewer_service_id || '') ===
|
||||||
CODEX_MAIN_SERVICE_ID,
|
CODEX_MAIN_SERVICE_ID,
|
||||||
)
|
)
|
||||||
.map((row) => ({ chatJid: row.chat_jid, activatedAt: row.activated_at ?? null }));
|
.map((row) => ({
|
||||||
|
chatJid: row.chat_jid,
|
||||||
|
activatedAt: row.activated_at ?? null,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use getActiveCodexFailoverLeases() instead */
|
/** @deprecated Use getActiveCodexFailoverLeases() instead */
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ const {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./agent-error-detection.js', async (importOriginal) => {
|
vi.mock('./agent-error-detection.js', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('./agent-error-detection.js')>();
|
const actual =
|
||||||
|
await importOriginal<typeof import('./agent-error-detection.js')>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
classifyRotationTrigger: vi.fn((error?: string | null) => {
|
classifyRotationTrigger: vi.fn((error?: string | null) => {
|
||||||
@@ -566,8 +567,7 @@ Check the run.
|
|||||||
created_at: '2026-02-22T00:00:00.000Z',
|
created_at: '2026-02-22T00:00:00.000Z',
|
||||||
});
|
});
|
||||||
|
|
||||||
(runAgentProcessMock as any)
|
(runAgentProcessMock as any).mockImplementationOnce(
|
||||||
.mockImplementationOnce(
|
|
||||||
async (
|
async (
|
||||||
_group: unknown,
|
_group: unknown,
|
||||||
_input: unknown,
|
_input: unknown,
|
||||||
|
|||||||
@@ -535,8 +535,7 @@ async function runTask(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const provider =
|
const provider = context.taskAgentType === 'codex' ? 'codex' : 'claude';
|
||||||
context.taskAgentType === 'codex' ? 'codex' : 'claude';
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const attempt = await runTaskAttempt(provider);
|
const attempt = await runTaskAttempt(provider);
|
||||||
|
|||||||
Reference in New Issue
Block a user