fix(engine): preserve step progress + worktree across internal task bounces

Workflow-step REVISE retries, pause→todo handoffs, and the
context-overflow fresh-session requeue were all routing tasks back to
`todo` before returning to `in-progress`. The default reopen-to-todo
path reset every step to pending and rewrote PROMPT.md checkboxes, so
each retry restarted from step 0 even when earlier steps had already
been done — the symptom seen on FN-2978, where every workflow REVISE
or pause cycle wiped the task's progress.

- Add `preserveResumeState` to `TaskStore.moveTask`. When set, skip
  `resetAllStepsToPending` + `resetPromptCheckboxes` and keep
  `worktree` and `executionStartedAt` so the resumed run reattaches to
  the same checkout. `status`, `error`, and `blockedBy` still clear.
- Use it on the workflow-rerun bounce, the three pause-graceful
  handoffs, and the context-overflow requeue. The agent-terminated
  pause path still discards (it nukes worktree+branch by design).
- Context-overflow requeue clears `sessionFile` synchronously in the
  awaited `updateTask` immediately before `moveTask`, so the next
  dispatch cannot reopen the saturated session via a stale pointer.
- `fn_task_update` no longer silently regresses `done`/`skipped` steps
  to `in-progress`, no longer captures a stale rewind checkpoint when
  it does, and tells the agent honestly when a regression is ignored.
- Mobile chat keyboard: ChatView/QuickChatFAB gate layout on the new
  `keyboardOpen` flag so focused-input + viewport-shrink iOS cases
  still adjust when the computed overlap is zero.

Tests:
- New `preserveResumeState` coverage in store.test.ts; updated
  workflow-rerun + pause-graceful assertions in executor.test.ts.
- Restructured the previously-flaky "routes exhausted prompt-mode
  workflow hard failures" test to drive the bounce inline; passes in
  isolation and in the wider workflow/pause/context sweep (59/59).
- Added regression tests in ChatView.test.tsx and QuickChatFAB.test.tsx
  for the iOS last-resort `keyboardOpen=true, keyboardOverlap=0` case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-30 14:12:03 -07:00
parent 3db00440d9
commit f740bf29e9
11 changed files with 466 additions and 142 deletions

View File

@@ -3499,6 +3499,25 @@ describe("TaskStore", () => {
expect(fetched.steps[0].status).toBe("in-progress");
});
it("preserves done/skipped steps when updateStep is called with in-progress", async () => {
const task = await createTaskWithSteps();
await store.updateStep(task.id, 0, "done");
await store.updateStep(task.id, 1, "done");
const beforeRegression = await store.getTask(task.id);
const currentStepBefore = beforeRegression.currentStep;
// Agent erroneously re-marks an already-done step as in-progress.
const result = await store.updateStep(task.id, 0, "in-progress");
expect(result.steps[0].status).toBe("done");
expect(result.steps[1].status).toBe("done");
expect(result.currentStep).toBe(currentStepBefore);
const fetched = await store.getTask(task.id);
expect(fetched.steps[0].status).toBe("done");
expect(fetched.currentStep).toBe(currentStepBefore);
});
it("addComment recreates missing task directory before persisting metadata", async () => {
const task = await createTestTask();
const dir = await deleteTaskDir(task.id);
@@ -5788,6 +5807,21 @@ Task with acceptance criteria
expect(moved.currentStep).toBe(0);
});
it("preserves step progress when moving in-progress → todo with preserveResumeState option", async () => {
const task = await createTaskWithSteps();
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
await setMixedStepStatuses(task.id);
await store.updateTask(task.id, { currentStep: 2 });
const moved = await store.moveTask(task.id, "todo", { preserveResumeState: true });
expect(moved.steps[0].status).toBe("done");
expect(moved.steps[1].status).toBe("in-progress");
expect(moved.steps[2].status).toBe("pending");
expect(moved.currentStep).toBe(2);
});
it("resets steps when moving from in-review to todo", async () => {
const task = await createTaskWithSteps();
await store.moveTask(task.id, "todo");