Skip to content

Commit 742e5d6

Browse files
committed
fix invalid code redo
1 parent a983c85 commit 742e5d6

6 files changed

Lines changed: 317 additions & 109 deletions

File tree

packages/codingcode/src/checkpoint/checkpoint-service.ts

Lines changed: 125 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -170,51 +170,83 @@ function getCompletedTurnsFor(sg: ShadowGit, sessionId: string): number[] {
170170
return ids;
171171
}
172172

173+
interface RestorePlan {
174+
throughTurnId: number;
175+
baseTurnId: number;
176+
affectedTurns: number[];
177+
baseline: string;
178+
}
179+
180+
function getTurnRestorePlan(sg: ShadowGit, sessionId: string, turnId: number): RestorePlan | null {
181+
const baseline = sg.findCommitByMessage(commitMsg(sessionId, turnId, 'baseline'));
182+
const final = sg.findCommitByMessage(commitMsg(sessionId, turnId, 'final'));
183+
if (!baseline || !final) return null;
184+
return {
185+
throughTurnId: turnId,
186+
baseTurnId: turnId,
187+
affectedTurns: [],
188+
baseline,
189+
};
190+
}
191+
192+
function getRollbackToTurnPlan(
193+
sg: ShadowGit,
194+
sessionId: string,
195+
throughTurnId: number
196+
): RestorePlan | null {
197+
const completedTurns = getCompletedTurnsFor(sg, sessionId);
198+
const affectedTurns = completedTurns.filter((id) => id >= throughTurnId);
199+
if (affectedTurns.length === 0) return null;
200+
201+
const baseline = sg.findCommitByMessage(commitMsg(sessionId, throughTurnId, 'baseline'));
202+
if (!baseline) return null;
203+
204+
return {
205+
throughTurnId,
206+
baseTurnId: throughTurnId,
207+
affectedTurns,
208+
baseline,
209+
};
210+
}
211+
173212
function revertFilesImpl(
174213
projectPath: string,
175214
sessionId: string,
176-
turnId: number,
215+
plan: RestorePlan,
177216
selectedFiles: string[],
178217
action: CodeRestoreEntry['action'],
179218
sg: ShadowGit
180219
): CodeRollbackResult {
181220
if (selectedFiles.length === 0) {
182221
return {
183222
reverted: false,
184-
throughTurnId: turnId,
185-
baseTurnId: turnId,
186-
affectedTurns: [],
223+
throughTurnId: plan.throughTurnId,
224+
baseTurnId: plan.baseTurnId,
225+
affectedTurns: plan.affectedTurns,
187226
selectedFiles: [],
188227
restoreEntry: null,
189228
};
190229
}
191230

192-
const baseline = sg.findCommitByMessage(commitMsg(sessionId, turnId, 'baseline'));
193-
if (!baseline)
194-
return {
195-
reverted: false,
196-
throughTurnId: turnId,
197-
baseTurnId: turnId,
198-
affectedTurns: [],
199-
selectedFiles: [],
200-
restoreEntry: null,
201-
};
202-
203231
sg.lock();
204232
try {
205233
let safetyCommit: string;
206234
const existingEntry = readRestoreEntry(sg.gitDir, sessionId);
207235

208-
if (existingEntry && existingEntry.throughTurnId === turnId && existingEntry.safetyCommit) {
236+
if (
237+
existingEntry &&
238+
existingEntry.throughTurnId === plan.throughTurnId &&
239+
existingEntry.safetyCommit
240+
) {
209241
safetyCommit = existingEntry.safetyCommit;
210242
} else {
211-
safetyCommit = sg.commit(commitMsg(sessionId, turnId, 'revert-safety'));
243+
safetyCommit = sg.commit(commitMsg(sessionId, plan.throughTurnId, 'revert-safety'));
212244
}
213245

214-
sg.checkoutFiles(baseline, selectedFiles);
246+
sg.checkoutFiles(plan.baseline, selectedFiles);
215247

216248
const combinedFiles =
217-
existingEntry && existingEntry.throughTurnId === turnId
249+
existingEntry && existingEntry.throughTurnId === plan.throughTurnId
218250
? [
219251
...new Map(
220252
[...existingEntry.selectedFiles, ...selectedFiles].map((f) => [
@@ -227,14 +259,14 @@ function revertFilesImpl(
227259

228260
const entry: CodeRestoreEntry = {
229261
id: createHash('sha256')
230-
.update(`${sessionId}-${turnId}-${Date.now()}`)
262+
.update(`${sessionId}-${plan.throughTurnId}-${Date.now()}`)
231263
.digest('hex')
232264
.slice(0, 12),
233265
sessionId,
234266
action,
235-
throughTurnId: turnId,
236-
baseTurnId: turnId,
237-
affectedTurns: [],
267+
throughTurnId: plan.throughTurnId,
268+
baseTurnId: plan.baseTurnId,
269+
affectedTurns: plan.affectedTurns,
238270
selectedFiles: combinedFiles,
239271
safetyCommit,
240272
timestamp: new Date().toISOString(),
@@ -243,9 +275,9 @@ function revertFilesImpl(
243275

244276
return {
245277
reverted: true,
246-
throughTurnId: turnId,
247-
baseTurnId: turnId,
248-
affectedTurns: [],
278+
throughTurnId: plan.throughTurnId,
279+
baseTurnId: plan.baseTurnId,
280+
affectedTurns: plan.affectedTurns,
249281
selectedFiles: combinedFiles,
250282
restoreEntry: entry,
251283
};
@@ -440,7 +472,17 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
440472
file: string
441473
): CodeRollbackResult => {
442474
const sg = ensure(projectPath);
443-
return revertFilesImpl(projectPath, sessionId, turnId, [file], 'checkpoint-file', sg);
475+
const plan = getTurnRestorePlan(sg, sessionId, turnId);
476+
if (!plan)
477+
return {
478+
reverted: false,
479+
throughTurnId: turnId,
480+
baseTurnId: turnId,
481+
affectedTurns: [],
482+
selectedFiles: [],
483+
restoreEntry: null,
484+
};
485+
return revertFilesImpl(projectPath, sessionId, plan, [file], 'checkpoint-file', sg);
444486
},
445487

446488
revertCheckpointFiles: (
@@ -450,7 +492,17 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
450492
files: string[]
451493
): CodeRollbackResult => {
452494
const sg = ensure(projectPath);
453-
return revertFilesImpl(projectPath, sessionId, turnId, files, 'checkpoint-files', sg);
495+
const plan = getTurnRestorePlan(sg, sessionId, turnId);
496+
if (!plan)
497+
return {
498+
reverted: false,
499+
throughTurnId: turnId,
500+
baseTurnId: turnId,
501+
affectedTurns: [],
502+
selectedFiles: [],
503+
restoreEntry: null,
504+
};
505+
return revertFilesImpl(projectPath, sessionId, plan, files, 'checkpoint-files', sg);
454506
},
455507

456508
// ---- B3: revertCheckpointAgentFiles / revertCheckpointAllFiles ----
@@ -481,10 +533,20 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
481533
selectedFiles: [],
482534
restoreEntry: null,
483535
};
536+
const plan = getTurnRestorePlan(sg, sessionId, turnId);
537+
if (!plan)
538+
return {
539+
reverted: false,
540+
throughTurnId: turnId,
541+
baseTurnId: null,
542+
affectedTurns: [],
543+
selectedFiles: [],
544+
restoreEntry: null,
545+
};
484546
return revertFilesImpl(
485547
projectPath,
486548
sessionId,
487-
turnId,
549+
plan,
488550
changes.agentModified,
489551
'checkpoint-agent',
490552
sg
@@ -518,7 +580,24 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
518580
selectedFiles: [],
519581
restoreEntry: null,
520582
};
521-
return revertFilesImpl(projectPath, sessionId, turnId, all, 'checkpoint-all', sg);
583+
const plan = getTurnRestorePlan(sg, sessionId, turnId);
584+
if (!plan)
585+
return {
586+
reverted: false,
587+
throughTurnId: turnId,
588+
baseTurnId: null,
589+
affectedTurns: [],
590+
selectedFiles: [],
591+
restoreEntry: null,
592+
};
593+
return revertFilesImpl(
594+
projectPath,
595+
sessionId,
596+
plan,
597+
all,
598+
'checkpoint-all',
599+
sg
600+
);
522601
},
523602

524603
// ---- B4: previewRollbackDiff ----
@@ -529,20 +608,18 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
529608
throughTurnId: number
530609
): RollbackPreviewDiff => {
531610
const sg = ensure(projectPath);
532-
const completedTurns = getCompletedTurnsFor(sg, sessionId);
533-
const affectedTurns = completedTurns.filter((id) => id > throughTurnId);
534-
if (affectedTurns.length === 0) {
611+
const plan = getRollbackToTurnPlan(sg, sessionId, throughTurnId);
612+
if (!plan) {
535613
return { throughTurnId, baseTurnId: null, affectedTurns: [], diff: '' };
536614
}
537615

538-
const baseTurnId = affectedTurns[0]! - 1;
539-
const baseline = sg.findCommitByMessage(commitMsg(sessionId, baseTurnId, 'baseline'));
540-
if (!baseline) {
541-
return { throughTurnId, baseTurnId, affectedTurns, diff: '' };
542-
}
543-
544-
const result = sg.git('diff', baseline);
545-
return { throughTurnId, baseTurnId, affectedTurns, diff: result.stdout };
616+
const result = sg.git('diff', plan.baseline);
617+
return {
618+
throughTurnId,
619+
baseTurnId: plan.baseTurnId,
620+
affectedTurns: plan.affectedTurns,
621+
diff: result.stdout,
622+
};
546623
},
547624

548625
// ---- B5: rollbackCodeToTurn ----
@@ -553,33 +630,19 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
553630
throughTurnId: number
554631
): CodeRollbackResult => {
555632
const sg = ensure(projectPath);
556-
const completedTurns = getCompletedTurnsFor(sg, sessionId);
557-
const affectedTurns = completedTurns.filter((id) => id > throughTurnId);
558-
if (affectedTurns.length === 0) {
559-
return {
560-
reverted: true,
561-
throughTurnId,
562-
baseTurnId: null,
563-
affectedTurns: [],
564-
selectedFiles: [],
565-
restoreEntry: null,
566-
};
567-
}
568-
569-
const baseTurnId = affectedTurns[0]! - 1;
570-
const baseline = sg.findCommitByMessage(commitMsg(sessionId, baseTurnId, 'baseline'));
571-
if (!baseline) {
633+
const plan = getRollbackToTurnPlan(sg, sessionId, throughTurnId);
634+
if (!plan) {
572635
return {
573636
reverted: false,
574637
throughTurnId,
575-
baseTurnId,
638+
baseTurnId: null,
576639
affectedTurns: [],
577640
selectedFiles: [],
578641
restoreEntry: null,
579642
};
580643
}
581644

582-
const diffResult = sg.git('diff', '--name-only', baseline);
645+
const diffResult = sg.git('diff', '--name-only', plan.baseline);
583646
const selectedFiles = diffResult.stdout
584647
.trim()
585648
.split('\n')
@@ -590,45 +653,14 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
590653
return {
591654
reverted: true,
592655
throughTurnId,
593-
baseTurnId,
594-
affectedTurns,
656+
baseTurnId: plan.baseTurnId,
657+
affectedTurns: plan.affectedTurns,
595658
selectedFiles: [],
596659
restoreEntry: null,
597660
};
598661
}
599662

600-
sg.lock();
601-
try {
602-
const safetyCommit = sg.commit(commitMsg(sessionId, throughTurnId, 'rollback-safety'));
603-
sg.checkoutFiles(baseline, selectedFiles);
604-
605-
const entry: CodeRestoreEntry = {
606-
id: createHash('sha256')
607-
.update(`${sessionId}-${Date.now()}`)
608-
.digest('hex')
609-
.slice(0, 12),
610-
sessionId,
611-
action: 'rollback-to-turn',
612-
throughTurnId,
613-
baseTurnId,
614-
affectedTurns,
615-
selectedFiles,
616-
safetyCommit,
617-
timestamp: new Date().toISOString(),
618-
};
619-
writeRestoreEntry(sg.gitDir, sessionId, entry);
620-
621-
return {
622-
reverted: true,
623-
throughTurnId,
624-
baseTurnId,
625-
affectedTurns,
626-
selectedFiles,
627-
restoreEntry: entry,
628-
};
629-
} finally {
630-
sg.unlock();
631-
}
663+
return revertFilesImpl(projectPath, sessionId, plan, selectedFiles, 'rollback-to-turn', sg);
632664
},
633665

634666
// ---- B6: undoLastCodeRollback ----

packages/codingcode/src/server/routes/sessions.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ import { runWithLayer, errorResponse } from '../util.js';
1717

1818
export const sessionsRouter = new Hono();
1919

20+
function findUserMessageForTurn(sessionId: string, turnId: number): string {
21+
const dir = resolveSessionDir(sessionId);
22+
if (!dir) return '';
23+
const jsonlPath = join(dir, `${sessionId}.jsonl`);
24+
const rawEvents = readHistory(jsonlPath);
25+
for (const ev of rawEvents) {
26+
if (ev.type === 'user' && (ev as any).turnId === turnId) {
27+
return (ev as any).content ?? '';
28+
}
29+
}
30+
return '';
31+
}
32+
2033
// Active session ApprovalService forks, keyed by sessionId.
2134
// messages.ts registers/unregisters; this file's PUT route updates them.
2235
export const activeApprovalForks = new Map<
@@ -365,21 +378,9 @@ sessionsRouter.post('/:id/rollback-context', async (c) => {
365378
Effect.gen(function* () {
366379
const svc = yield* SessionService;
367380
const state = yield* svc.create(cwd, 'unknown', sessionId);
381+
const rolledBackMessage = findUserMessageForTurn(sessionId, body.throughTurnId);
368382
yield* svc.rollbackToTurn(state, body.throughTurnId, 'user rollback');
369383
const turns = readUIHistory(sessionId);
370-
// Find user message of the rolled-back turn for input refill
371-
let rolledBackMessage = '';
372-
const dir = resolveSessionDir(sessionId);
373-
if (dir) {
374-
const jsonlPath = join(dir, `${sessionId}.jsonl`);
375-
const rawEvents = readHistory(jsonlPath);
376-
for (const ev of rawEvents) {
377-
if (ev.type === 'user' && (ev as any).turnId === body.throughTurnId) {
378-
rolledBackMessage = (ev as any).content ?? '';
379-
break;
380-
}
381-
}
382-
}
383384
return { ok: true, turns, rolledBackMessage, promptEstimate: state.promptEstimate };
384385
})
385386
);
@@ -402,9 +403,10 @@ sessionsRouter.post('/:id/rollback-both-to-turn', async (c) => {
402403
const svc = yield* SessionService;
403404
const codeResult = checkpoint.rollbackCodeToTurn(cwd, sessionId, body.throughTurnId);
404405
const state = yield* svc.create(cwd, 'unknown', sessionId);
406+
const rolledBackMessage = findUserMessageForTurn(sessionId, body.throughTurnId);
405407
yield* svc.rollbackToTurn(state, body.throughTurnId, 'user rollback');
406408
const turns = readUIHistory(sessionId);
407-
return { ok: true, turns, codeResult, promptEstimate: state.promptEstimate };
409+
return { ok: true, turns, codeResult, rolledBackMessage, promptEstimate: state.promptEstimate };
408410
})
409411
);
410412
if (!result.ok) {

0 commit comments

Comments
 (0)