| 1 | #!/usr/bin/env node |
| 2 | /** |
| 3 | * Recover the next agent action from the durable live-session journal. |
| 4 | */ |
| 5 | |
| 6 | import { createLiveSessionStore } from './live-session-store.mjs'; |
| 7 | |
| 8 | function manualApplyReplyCommand(eventOrId = 'EVENT_ID') { |
| 9 | const id = typeof eventOrId === 'string' ? eventOrId : eventOrId?.id || 'EVENT_ID'; |
| 10 | return `live-poll.mjs --reply ${id} done --data '<json>'`; |
| 11 | } |
| 12 | |
| 13 | export function manualApplyResumeHint(event = {}) { |
| 14 | const summary = event.manualApplySummary || summarizeManualApplyEvent(event); |
| 15 | const parts = []; |
| 16 | if (summary.pageUrl) parts.push(`page ${summary.pageUrl}`); |
| 17 | if (summary.chunk) parts.push(`chunk ${summary.chunk.index}/${summary.chunk.total}`); |
| 18 | if (Number.isFinite(summary.opCount)) parts.push(`${summary.opCount} op(s)`); |
| 19 | if (Number.isFinite(summary.entryCount)) parts.push(`${summary.entryCount} entr${summary.entryCount === 1 ? 'y' : 'ies'}`); |
| 20 | if (summary.files?.length) parts.push(`likely files: ${summary.files.join(', ')}`); |
| 21 | const scope = parts.length ? ` (${parts.join(', ')})` : ''; |
| 22 | return `Manual Apply pending${scope}. If you have not already leased it, run live-poll.mjs. Apply the source edits from the manual_edit_apply batch, then reply with ${manualApplyReplyCommand(event.id)}. Polling only leases this work item; it does not commit source edits. Do not run live-commit-manual-edits.mjs for this leased event. Do not poll again before replying.`; |
| 23 | } |
| 24 | |
| 25 | function summarizeManualApplyEvent(event = {}) { |
| 26 | const entries = Array.isArray(event.batch?.entries) ? event.batch.entries : []; |
| 27 | const opCount = entries.reduce((sum, entry) => sum + (Array.isArray(entry.ops) ? entry.ops.length : 0), 0); |
| 28 | return { |
| 29 | pageUrl: event.pageUrl || null, |
| 30 | chunk: event.chunk || null, |
| 31 | entryCount: entries.length, |
| 32 | opCount, |
| 33 | files: collectManualApplyFiles(event.batch), |
| 34 | }; |
| 35 | } |
| 36 | |
| 37 | function collectManualApplyFiles(batch) { |
| 38 | const files = []; |
| 39 | for (const entry of batch?.entries || []) { |
| 40 | for (const op of entry.ops || []) files.push(op.sourceHint?.file); |
| 41 | } |
| 42 | for (const candidate of batch?.candidates || []) { |
| 43 | files.push(candidate.sourceHint?.relativeFile, candidate.sourceHint?.file); |
| 44 | for (const item of candidate.textMatches || []) files.push(item.file); |
| 45 | for (const item of candidate.objectKeyMatches || []) files.push(item.file); |
| 46 | for (const item of candidate.locatorMatches || []) files.push(item.file); |
| 47 | for (const item of candidate.contextTextMatches || []) files.push(item.file); |
| 48 | } |
| 49 | return [...new Set(files.filter((file) => typeof file === 'string' && file.length > 0))].sort(); |
| 50 | } |
| 51 | |
| 52 | function parseArgs(argv) { |
| 53 | const out = { id: null }; |
| 54 | for (let i = 0; i < argv.length; i++) { |
| 55 | const arg = argv[i]; |
| 56 | if (arg === '--id') out.id = argv[++i]; |
| 57 | else if (arg.startsWith('--id=')) out.id = arg.slice('--id='.length); |
| 58 | else if (arg === '--help' || arg === '-h') out.help = true; |
| 59 | } |
| 60 | return out; |
| 61 | } |
| 62 | |
| 63 | export async function resumeCli() { |
| 64 | const args = parseArgs(process.argv.slice(2)); |
| 65 | if (args.help) { |
| 66 | console.log(`Usage: node live-resume.mjs [--id SESSION_ID]\n\nPrint the active durable session checkpoint and the next safe agent action.`); |
| 67 | return; |
| 68 | } |
| 69 | |
| 70 | const store = createLiveSessionStore({ cwd: process.cwd(), sessionId: args.id || undefined }); |
| 71 | const snapshot = args.id ? store.getSnapshot(args.id) : store.listActiveSessions()[0] || null; |
| 72 | if (!snapshot) { |
| 73 | console.log(JSON.stringify({ active: false, nextAction: 'No active durable live session found.' }, null, 2)); |
| 74 | return; |
| 75 | } |
| 76 | |
| 77 | const pending = snapshot.pendingEvent || null; |
| 78 | const nextAction = pending |
| 79 | ? pending.type === 'manual_edit_apply' |
| 80 | ? manualApplyResumeHint(pending) |
| 81 | : `Run live-poll.mjs, handle ${pending.type} ${pending.id}, then acknowledge with live-poll.mjs --reply ${pending.id} done.` |
| 82 | : snapshot.phase === 'carbonize_required' |
| 83 | ? `Finish carbonize cleanup${snapshot.sourceFile ? ` in ${snapshot.sourceFile}` : ''}, then run live-complete.mjs --id ${snapshot.id}.` |
| 84 | : snapshot.phase === 'accept_requested' |
| 85 | ? `Run live-complete.mjs --id ${snapshot.id} after verifying the accepted variant is written.` |
| 86 | : `Inspect ${snapshot.id}; no pending agent event is currently queued.`; |
| 87 | |
| 88 | console.log(JSON.stringify({ active: true, snapshot, pendingEvent: pending, nextAction }, null, 2)); |
| 89 | } |
| 90 | |
| 91 | const _running = process.argv[1]; |
| 92 | if (_running?.endsWith('live-resume.mjs') || _running?.endsWith('live-resume.mjs/')) { |
| 93 | resumeCli(); |
| 94 | } |
| 95 |