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

@@ -113,11 +113,33 @@ function formatTimeLabel(timestampIso: string): string {
second: '2-digit',
hour12: false,
timeZone: TIMEZONE,
}).format(new Date(timestampIso));
})
.format(new Date(timestampIso))
.replace(/:/g, '시 ')
.replace(/시 (\d{2})$/, '분 $1초');
}
function formatWatchIntervalLabel(task: Pick<ScheduledTask, 'schedule_type' | 'schedule_value'>): string | null {
if (task.schedule_type !== 'interval') return null;
const ms = parseInt(task.schedule_value, 10);
if (!Number.isFinite(ms) || ms <= 0) return null;
const totalSeconds = Math.floor(ms / 1000);
if (totalSeconds < 60) return `${totalSeconds}`;
const totalMinutes = Math.floor(totalSeconds / 60);
if (totalMinutes < 60) {
const seconds = totalSeconds % 60;
return seconds > 0 ? `${totalMinutes}${seconds}` : `${totalMinutes}`;
}
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return minutes > 0 ? `${hours}시간 ${minutes}` : `${hours}시간`;
}
export function renderWatchCiStatusMessage(args: {
task: Pick<ScheduledTask, 'prompt'>;
task: Pick<ScheduledTask, 'prompt' | 'schedule_type' | 'schedule_value'>;
phase: WatcherStatusPhase;
checkedAt: string;
nextRun?: string | null;
@@ -141,6 +163,10 @@ export function renderWatchCiStatusMessage(args: {
`- 상태: ${statusLabel}`,
`- 마지막 확인: ${formatTimeLabel(args.checkedAt)}`,
];
const intervalLabel = formatWatchIntervalLabel(args.task);
if (intervalLabel) {
lines.push(`- 확인 간격: ${intervalLabel}`);
}
if (args.nextRun) {
lines.push(`- 다음 확인: ${formatTimeLabel(args.nextRun)}`);