From 694af4bb4ca725d39034003a50691da4ac895536 Mon Sep 17 00:00:00 2001 From: Raphael Pothin Date: Wed, 28 Jan 2026 21:52:21 +0000 Subject: [PATCH] fix: reduce exit delay after task completion - Serialize iteration checkpoint commits into a queue to ensure they complete before final output - Await checkpoint queue immediately after engine.start() to flush pending git work - This prevents long post-success delays caused by slow git hooks, LFS, signing, or auto-push The CLI now returns control to the user immediately after the success message, while still ensuring all checkpoint commits are completed before exit. BREAKING: None. This is a behavioral improvement that eliminates delay without changing the checkpoint guarantee model. Documentation: - Added 'Long delay before returning to prompt' troubleshooting section in README - Explains causes (git hooks, LFS, signing) and mitigations (disable checkpoints/push) Bumps version to 0.1.5 --- JOURNAL.md | 32 ++++++++++++++++++++++++++++++++ README.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/commands/run.ts | 28 +++++++++++++++++++++------- 5 files changed, 64 insertions(+), 10 deletions(-) diff --git a/JOURNAL.md b/JOURNAL.md index 0e5ad78..6b37566 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -1710,3 +1710,35 @@ The `git-branch-manager.test.ts` test was failing on Windows CI due to: - `npm run build` ✅ - `npm run lint` ✅ - `npm test` ✅ (311 tests passing) + +## 2026-01-28 - Fix Post-Run Exit Delay + +### Context +After `✔ 🎉 All 11 tasks completed successfully!` appeared, there was a long delay before terminal control returned to the user. Investigation revealed un-awaited async git checkpoint commits kept the Node.js event loop alive after the final success message printed. + +### Root Cause +The `run` command subscribed to `LoopEngine`'s `iterationEnd` event and started `checkpointManager.createCheckpoint(...)` asynchronously without awaiting it. These git operations (which invoke hooks, LFS, signing, or push operations) could continue running after "All tasks completed" was printed, delaying `process.exit()`. + +### Changes Made +1. **src/commands/run.ts** + - Added a `checkpointQueue: Promise` to serialize iteration checkpoint commits + - Replaced fire-and-forget `.createCheckpoint(...).then(...)` with chaining onto the queue + - Added `await checkpointQueue` immediately after `await engine.start(activeTask)` to flush pending commits before printing final output + +2. **README.md** + - Added new Troubleshooting section: "Long delay before returning to prompt" + - Explains git checkpoint work (hooks/signing/LFS) can add latency + - Lists mitigations: disable `autoCommit`, disable `autoPush`, inspect/disable slow hooks + +### Impact +- CLI now promptly returns control to the user after final success message +- Checkpoint commits still run (ensuring safety) but don't block the exit path +- Provides users with mitigation options for slow git operations + +### Files Modified +- `src/commands/run.ts` +- `README.md` + +### Validation +- `npm run build` ✅ +- `npm test` ✅ (311 tests passing) diff --git a/README.md b/README.md index 8da0707..de31c09 100644 --- a/README.md +++ b/README.md @@ -325,6 +325,14 @@ Check status: `ghcralph status` View checkpoints: `ghcralph rollback --list` Rollback if needed: `ghcralph rollback` +### Long delay before returning to prompt +`ghcralph run` creates git checkpoint commits (and may auto-push depending on config). If your repo has slow git hooks (e.g. Husky), commit signing, or Git LFS filters, the command may take extra time to finish. + +Mitigations: +- Disable checkpoints: set `autoCommit: false` (or `GHCRALPH_AUTO_COMMIT=false`) +- Disable pushing: set `autoPush: false` / `pushStrategy: manual` +- Inspect/disable slow hooks in `.git/hooks/` (or Husky scripts) if appropriate + ## Credits & Attribution **GitHub Copilot Ralph** is an opinionated interpretation of the **Ralph Wiggum loop** approach, originally proposed by **[Geoffrey Huntley](https://ghuntley.com/)**. diff --git a/package-lock.json b/package-lock.json index dc28d35..c9cfc07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghcralph", - "version": "0.1.4", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghcralph", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "dependencies": { "@github/copilot-sdk": "^0.1.17", diff --git a/package.json b/package.json index 562f852..2e5c1c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghcralph", - "version": "0.1.4", + "version": "0.1.5", "description": "GitHub Copilot Ralph - A cross-platform CLI for running autonomous agentic coding loops using the Ralph Wiggum pattern with GitHub Copilot", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/commands/run.ts b/src/commands/run.ts index ee7aa24..06401b6 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -486,6 +486,11 @@ See also: // Setup event listeners const events = engine.getEvents(); + // Iteration checkpoint commits are intentionally started asynchronously so the loop can continue, + // but we still need to wait for them at task end so the CLI can exit immediately after printing + // the final success line. + let checkpointQueue: Promise = Promise.resolve(); + events.on('iterationStart', (iteration, state) => { debug( `Iteration ${iteration}/${maxIterations} - Tokens: ${state.tokensUsed.toLocaleString()}` @@ -500,17 +505,22 @@ See also: if (record.summary) { console.log(` ${dim(record.summary)}`); } - + // Create checkpoint commit after successful iterations if (record.success && checkpointManager.isAutoCommitEnabled()) { // Build task context for commit message if we have plan info const taskContext: TaskContext | undefined = totalTasksInPlan > 0 ? { taskNumber: totalTasksProcessed, totalTasks: totalTasksInPlan } : undefined; - - checkpointManager - .createCheckpoint(record.iteration, record.summary ?? 'iteration complete', record.tokensUsed, taskContext) - .then((checkpoint) => { + + checkpointQueue = checkpointQueue + .then(async () => { + const checkpoint = await checkpointManager.createCheckpoint( + record.iteration, + record.summary ?? 'iteration complete', + record.tokensUsed, + taskContext + ); if (checkpoint) { debug(`Checkpoint created: ${checkpoint.commitHash.substring(0, 7)}`); // Update progress with commit hash @@ -518,10 +528,10 @@ See also: } }) .catch(() => { - // Ignore checkpoint errors + // Ignore checkpoint errors (keep queue alive) }); } - + // Update in-memory progress state (file written at task completion) progressTracker.setCurrentTask(totalTasksProcessed, state); }); @@ -541,6 +551,10 @@ See also: try { const finalState = await engine.start(activeTask); + // Flush any queued iteration checkpoint commits before we stop spinners / print final output. + await checkpointQueue; + checkpointQueue = Promise.resolve(); + loopSpinner.stop(); console.log('');