Fix: stop hard-resetting source/ on every chat tick#1860
Conversation
Every non-whitelisted chat message ran `syncWorkspace(..., {
resetAfterFetch: true })` on the canonical source/ for Archon-managed
clones, hard-resetting any agent edits, local commits, or non-default
branches before the next turn. The agent then re-did its own work,
entering a redo loop.
Changes:
- Replace `resetAfterFetch: boolean` with `mode: 'fast-forward' | 'reset'`
in `syncWorkspace`. Default `'fast-forward'` is non-destructive: it
fetches, inspects state, and only advances HEAD via `git merge --ff-only`
when strictly behind on the target branch with a clean tree. `'ahead'`,
`'diverged'`, and `'dirty'` states are preserved.
- Add `WorkspaceSyncState` and report it on `WorkspaceSyncResult`.
- Add `getCurrentBranch` and `countCommitsAhead` git helpers.
- Chat-tick discovery in `orchestrator-agent.ts` now uses the new default
(fast-forward) — managed vs. unmanaged distinction removed for chat.
- Worktree creation in `@archon/isolation` keeps `mode: 'reset'` for
managed clones (the legitimate destructive-mirror caller).
- Add `default_branch` column to codebases (migration 023, bundled-schema
regenerated, SQLite `migrateColumns` entry). Stored at clone/register
time via `getCurrentBranch`; chat sync passes it through instead of
re-detecting on every message.
- Add `post-message-reminder.ts`: after a direct-chat turn that touches a
managed source/, surface a one-line `system` event if there are
uncommitted edits or unpushed commits, so users know to push/commit
before the next worktree creation reclaims that work.
- Update git/isolation/orchestrator tests for the new API; add coverage
for all five fast-forward states (in_sync/dirty/ahead/behind/diverged),
the off-branch behind guard, and the managed-clone `mode: 'reset'` path.
Fixes #1516
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Comprehensive PR ReviewPR: #1860 — Fix: stop hard-resetting source/ on every chat tick SummaryThe core fix is solid. The Verdict:
🔴 HIGH Issues (Should fix before merge)HIGH-1:
|
| # | Issue | Location | Suggestion |
|---|---|---|---|
| L1 | state: 'behind' returned after successful ff-merge (updated: true but state still 'behind') |
packages/git/src/repo.ts:277-285 |
Return state: 'in_sync' post-merge; updated: true already signals the advance |
| L2 | Redundant ?? null in detectDefaultBranch — getCurrentBranch already returns BranchName | null |
packages/core/src/handlers/clone.ts:139-142 |
return getCurrentBranch(toRepoPath(targetPath)) directly |
| L3 | default_branch uses nullable().optional() while all other nullable columns use only nullable() |
packages/core/src/schemas/codebase.ts:20 |
Use z.string().nullable() — column is TEXT DEFAULT NULL, never undefined |
| L4 | getCurrentBranch silent-catch swallows unexpected errors (EACCES, ETIMEDOUT) with no log entry |
packages/git/src/branch.ts:321-336 |
Add getLog().debug({ workingPath, err }, 'get_current_branch_failed') in the catch |
| L5 | revParse failure returns 'in_sync' in debug logs when actual state is unknown |
packages/git/src/repo.ts |
Add getLog().debug({ local, remote }, 'sync_workspace_rev_parse_inconclusive_skipping_merge') |
| L6 | updateCodebase with default_branch not covered — column typo or null-vs-undefined would be invisible |
packages/core/src/db/codebases.ts:155-158 |
Add 2 tests to existing describe('updateCodebase', ...): update + null-clear |
| L7 | CLAUDE.md codebases table entry omits default_branch |
CLAUDE.md (Database Schema) | Add "; default_branch captured at clone time for non-destructive chat-tick sync" |
✅ What's Good
mode: 'fast-forward' | 'reset'API is clean and unambiguous. Named modes make intent explicit at every call site; the safe?? 'fast-forward'default is the right conservative choice.- All 5 fast-forward states are covered. The regression guard test ("chat tick never passes
mode: 'reset'") is exactly the right contract test for the original bug git reset --hard origin/<default_branch> on source/ every message — destroys any local state that has diverged from origin for managed clones #1516. - Error paths for the sync refactor are sound. Fetch errors are non-fatal;
syncErrorcaptures the message for the AI's context. Double-boundary pattern (reportUnpushedWorkInSourcehas its own try/catch + orchestrator outer catch) is correctly defensive. - Migration sequencing is correct.
023_add_default_branch_to_codebases.sqlusesIF NOT EXISTS, is present in000_combined.sql, and numbering is sequential. worktree.tscorrectly distinguishes managed vs. non-managed clones.mode: 'reset'for managed clones,mode: 'fast-forward'for local repos — logic is clear and tested.detectDefaultBrancherror contract is clean. Returnsnullfor all error cases; callers store NULL; NULL in DB falls back to runtime detection. Safe at every layer.
Suggested Follow-up Issues
| Issue Title | Priority |
|---|---|
"Add tests for clone.ts default_branch detection and backfill logic" |
P2 |
"Add updateCodebase tests for default_branch field" |
P3 |
"Fix state: 'behind' returned after successful fast-forward merge" |
P3 |
Reviewed by Archon comprehensive-pr-review workflow
Full artifacts: /Users/rasmus/.archon/workspaces/coleam00/Archon/artifacts/runs/29af962b000d1be0108709498fcf5f30/review/
Tests added: - post-message-reminder.test.ts: 9 tests covering all branches (path guard, no-sendStructuredEvent, detached HEAD, clean/sync, ahead only, dirty only, both, error swallowed) - git.test.ts: getCurrentBranch (5 tests) and countCommitsAhead (4 tests) describe blocks with fail-silent contract verification - codebases.test.ts: 2 tests for updateCodebase with default_branch (set value and null-clear) Code fixes: - orchestrator-agent.ts: eliminate redundant codebase DB fetch per tick by carrying codebase through DiscoverResult; use discoveredCodebase at reminder call site - codebase.ts schema: remove .optional() from default_branch — column is TEXT DEFAULT NULL, never undefined - clone.ts: remove redundant ?? null in detectDefaultBranch - branch.ts: add debug log in getCurrentBranch catch for unexpected errors (permission denied, timeout) — keeps fail-silent contract, adds breadcrumb - repo.ts: add debug log when revParse returns null (inconclusive state was silently logged as in_sync); return state: 'in_sync' after a successful ff-merge where HEAD advanced (was 'behind') Docs: - CLAUDE.md: add default_branch to codebases table description Skipped (deferred to follow-up issues): - clone.ts default_branch backfill tests (MEDIUM-2): clone.ts has never had tests; adding a full test file is out of scope for this regression fix (per consolidated review recommendation)
⚡ Self-Fix Report (Aggressive)Status: COMPLETE Fixes Applied (10 total)
View all fixesHIGH
MEDIUM
LOW
Tests Added
Skipped (1)
Suggested Follow-up Issues
Validation✅ Type check | ✅ Lint (0 warnings) | ✅ Format | ✅ Tests (all packages pass) Self-fix by Archon · aggressive mode · fixes pushed to |
Summary
discoverAllWorkflows()calledsyncWorkspace()withresetAfterFetch: true, which rangit reset --hard origin/<default_branch>againstsource/. Any uncommitted edits, local commits, or non-default-branch checkouts insource/were silently destroyed before the AI executed — creating an infinite redo loop where turn-N work was gone by turn-N+1.source/in Web Chat mode could never make lasting progress; users who registered a local repo or checked out a feature branch lost all in-progress state on every message.syncWorkspacegains amode: 'fast-forward' | 'reset'option (default'fast-forward'). Fast-forward mode inspects branch state and only runsgit merge --ff-onlywhen strictly safe (clean tree, on target branch, behind origin). The chat-tick discovery path now uses the default (non-destructive). Worktree creation explicitly passesmode: 'reset'— the one legitimate use of a hard reset. A newpost-message-reminderemits a system-status notice whensource/has unpushed commits or uncommitted changes after a chat turn.default_branchis stored in the DB at clone/register time to avoid re-detecting on every tick.syncRepository, and anything related to issue Web UI silently hydrates long conversations to oldest 200 messages — newest messages disappear after refresh or mid-session SSE recovery (DB is OK, UI bug) #1531.UX Journey
Before
After
Architecture Diagram
Before
After
Connection inventory:
orchestrator-agent.tssyncWorkspaceresetAfterFetch→mode(default ff)orchestrator-agent.tspost-message-reminder.tsreportUnpushedWorkInSourceafter AI turnpost-message-reminder.ts@archon/git(getCurrentBranch,countCommitsAhead,hasUncommittedChanges)isolation/worktree.tssyncWorkspacemode: 'reset'for managed clonespackages/git/src/repo.tssyncWorkspacepackages/git/src/branch.tsgetCurrentBranch,countCommitsAheadpackages/git/src/types.tsWorkspaceSyncState,state?on resultpackages/core/src/handlers/clone.tsgetCurrentBranchpackages/core/src/db/codebases.tsdefault_branchmigrations/023_add_default_branch_to_codebases.sqlLabel Snapshot
risk: mediumsize: Mcore,git,isolationcore:orchestrator,git:repo,isolation:worktreeChange Metadata
bugmulti(core + git + isolation)Linked Issue
Validation Evidence (required)
All seven checks pass:
bun run check:bundledbun run check:bundled-skillbun run check:bundled-schemabun run type-checkbun run lint --max-warnings 0bun run format:checkbun run testbun run validateoutput — all clean.Security Impact (required)
syncWorkspacenow touchessource/less (no more hard reset), reducing blast radius.Compatibility / Migration
{ resetAfterFetch: true }are the one call site inworktree.ts, now updated to{ mode: 'reset' }. No external API changes.migrations/023_add_default_branch_to_codebases.sqladds a nullabledefault_branch TEXTcolumn. Existing rows getNULL; the orchestrator falls back togetDefaultBranch()auto-detect for those rows, preserving pre-PR behavior.Human Verification (required)
Automated test suite covers all five
syncWorkspacefast-forward states (dirty, in_sync, ahead, behind, diverged), the off-branch guard, and regression guard that the chat-tick path never passesmode: 'reset'. Manual scenario verification:bun run validatesuite — 4375 tests, 0 failures. Regression guard test explicitly asserts the git reset --hard origin/<default_branch> on source/ every message — destroys any local state that has diverged from origin for managed clones #1516 root cause cannot recur.getCurrentBranchreturns null → skip),default_branchNULL fallback (auto-detect).Side Effects / Blast Radius (required)
@archon/git(new helpers + mode enum),@archon/coreorchestrator + DB + clone handler,@archon/isolationworktree creation. Workflow execution paths are untouched.origin/mainvia hard reset will no longer be force-synced on every chat tick. Ifsource/is used as a passive mirror, it may now drift from origin. That is the intended trade-off (prefer not-destroying-data over aggressive sync).Rollback Plan (required)
2d5aed12. The change is fully self-contained; reverting restores the oldresetAfterFetch: booleaninterface and removes the post-message reminder.source/drifts fromorigin/<default_branch>in ways that cause workflow YAML reads to fail, workflow discovery errors will surface in/workflow list.syncWorkspacereturningstate: 'diverged'would appear in debug logs.Risks and Mitigations
syncWorkspaceusing{ resetAfterFetch: true }break at the type level after this rename.worktree.ts— updated in this PR.resetAfterFetchremoved from the type signature.source/stale.state: 'dirty'is returned; the caller (orchestrator) already skips the update path on non-updatedresults. The post-message-reminder warns the user.default_branchNULL for existing rows causes a regression on the stored-branch fast-path.?? undefinedfallback passesundefinedtosyncWorkspace, which auto-detects viagetDefaultBranch()— identical to pre-PR behavior.