feat: add seq cursor, work_items, provider fallback, and delivery reliability

Major reliability improvements to the message processing pipeline:

- Add messages.seq monotonic cursor replacing timestamp-based cursors,
  preventing message loss from timestamp collisions with LIMIT queries
- Add work_items table separating agent production from delivery,
  enabling delivery retry without re-running the agent
- Propagate Discord send failures instead of silently swallowing them
- Add Claude 429 → Kimi K2.5 automatic provider fallback with cooldown
- Fix follow-up turn state reset in live index.ts path (not just
  message-runtime.ts) to prevent final output from being lost
- Add restart context tracking for graceful restart announcements
- Lazy migration from timestamp cursors to seq cursors for existing data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eyejoker
2026-03-23 00:11:32 +09:00
parent dd15585941
commit b647502a10
19 changed files with 2782 additions and 209 deletions

View File

@@ -11,6 +11,12 @@ interface QueuedTask {
fn: () => Promise<void>;
}
export interface GroupActivityTouchMeta {
source: 'follow-up';
textLength: number;
filename: string;
}
export interface GroupRunContext {
runId: string;
reason: 'messages' | 'drain';
@@ -35,6 +41,7 @@ interface GroupState {
retryTimer: ReturnType<typeof setTimeout> | null;
retryScheduledAt: number | null;
startedAt: number | null;
activityTouch: ((meta?: GroupActivityTouchMeta) => void) | null;
}
export interface GroupStatus {
@@ -73,6 +80,7 @@ export class GroupQueue {
retryTimer: null,
retryScheduledAt: null,
startedAt: null,
activityTouch: null,
};
this.groups.set(groupJid, state);
}
@@ -85,6 +93,14 @@ export class GroupQueue {
this.processMessagesFn = fn;
}
setActivityTouch(
groupJid: string,
touch: ((meta?: GroupActivityTouchMeta) => void) | null,
): void {
const state = this.getGroup(groupJid);
state.activityTouch = touch;
}
private createRunId(): string {
return `run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
@@ -262,6 +278,11 @@ export class GroupQueue {
const tempPath = `${filepath}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify({ type: 'message', text }));
fs.renameSync(tempPath, filepath);
state.activityTouch?.({
source: 'follow-up',
textLength: text.length,
filename,
});
logger.info(
{
groupJid,
@@ -390,6 +411,7 @@ export class GroupQueue {
state.processName = null;
state.groupFolder = null;
state.currentRunId = null;
state.activityTouch = null;
this.activeCount--;
this.drainGroup(groupJid);
}
@@ -424,6 +446,7 @@ export class GroupQueue {
state.process = null;
state.processName = null;
state.groupFolder = null;
state.activityTouch = null;
this.activeCount--;
this.drainGroup(groupJid);
}