backup current stable ejclaw state

This commit is contained in:
Codex
2026-05-27 10:53:05 +09:00
parent 646bc34372
commit 1509108e04
57 changed files with 7127 additions and 154 deletions

View File

@@ -21,6 +21,7 @@ import { writeGroupsSnapshot } from './agent-runner.js';
import {
type AssignRoomInput,
getAllTasks,
getEarliestUnansweredHumanSeq,
hasRecentRestartAnnouncement,
initDatabase,
storeChatMetadata,
@@ -55,6 +56,7 @@ import {
import { createMessageRuntime } from './message-runtime.js';
import { nudgeSchedulerLoop, startSchedulerLoop } from './task-scheduler.js';
import { startUnifiedDashboard } from './unified-dashboard.js';
import { startUsagePrimer } from './usage-primer.js';
import { startWebDashboardServer } from './web-dashboard-server.js';
import { Channel, NewMessage, RegisteredGroup } from './types.js';
import { logger } from './logger.js';
@@ -270,14 +272,29 @@ async function main(): Promise<void> {
status: 'processing' | 'waiting';
} => status.status !== 'inactive',
)
.map((status) => ({
chatJid: status.jid,
groupName: roomBindings[status.jid]?.name || status.jid,
status: status.status,
elapsedMs: status.elapsedMs,
pendingMessages: status.pendingMessages,
pendingTasks: status.pendingTasks,
}));
.map((status) => {
// Capture the seq of the earliest unanswered human message for this
// chat so the next startup can rewind the agent cursor and re-run
// the interrupted turn automatically.
let rewindToSeq: number | null = null;
try {
rewindToSeq = getEarliestUnansweredHumanSeq(status.jid);
} catch (err) {
logger.warn(
{ err, chatJid: status.jid },
'Failed to capture rewind seq for shutdown snapshot',
);
}
return {
chatJid: status.jid,
groupName: roomBindings[status.jid]?.name || status.jid,
status: status.status,
elapsedMs: status.elapsedMs,
pendingMessages: status.pendingMessages,
pendingTasks: status.pendingTasks,
rewindToSeq,
};
});
const writtenPaths = writeShutdownRestartContext(
roomBindings,
interruptedGroups,
@@ -488,6 +505,37 @@ async function main(): Promise<void> {
restartContext,
roomBindings,
)) {
// Auto-resume: rewind the agent cursor to just before the earliest
// unanswered human message so the missed-message processor picks it up
// again. Claude/Codex sessions are persisted by group folder, so the
// agent will resume mid-turn rather than restarting from scratch.
if (candidate.rewindToSeq != null && candidate.rewindToSeq > 0) {
const cursors = runtimeState.getLastAgentTimestamps();
const previousCursor = cursors[candidate.chatJid];
const targetCursor = String(candidate.rewindToSeq - 1);
// Only rewind if the current cursor is past the target — never advance.
const previousNum = previousCursor
? Number.parseInt(previousCursor, 10)
: 0;
if (
Number.isFinite(previousNum) &&
previousNum >= candidate.rewindToSeq
) {
cursors[candidate.chatJid] = targetCursor;
runtimeState.saveState();
logger.info(
{
chatJid: candidate.chatJid,
groupFolder: candidate.groupFolder,
previousCursor,
rewoundTo: targetCursor,
rewindToSeq: candidate.rewindToSeq,
},
'Rewound agent cursor for interrupted-turn auto-resume',
);
}
}
queue.enqueueMessageCheck(
candidate.chatJid,
resolveGroupIpcPath(candidate.groupFolder),
@@ -499,6 +547,7 @@ async function main(): Promise<void> {
status: candidate.status,
pendingMessages: candidate.pendingMessages,
pendingTasks: candidate.pendingTasks,
rewindToSeq: candidate.rewindToSeq,
},
'Queued interrupted group for restart recovery',
);
@@ -522,6 +571,7 @@ async function main(): Promise<void> {
purgeOnStart: true,
});
webDashboardServer = startWebDashboardServer(WEB_DASHBOARD);
startUsagePrimer();
leaseRecoveryTimer = setInterval(() => {
const failover = getGlobalFailoverInfo();