Skip to content

fix(plan): keep plan version history across midnight (#1679) - #1680

Open
backnotprop wants to merge 2 commits into
mainfrom
fix/plan-history-across-midnight-1679
Open

backnotprop wants to merge 2 commits into
mainfrom
fix/plan-history-across-midnight-1679

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

Fixes #1679.

Problem

Plan version history lived in history/{project}/{generateSlug(plan)}/, and generateSlug is <heading>-<UTC date of now>. A revision that arrived after UTC midnight landed in a new directory as 001.md, so previousPlan was null and the Versions tab and diff disappeared. The same slug also names the decision snapshots in plans/, where the date is wanted (parseArchiveFilename / listArchivedPlans read it back).

Fix

The two jobs are now separate:

  • generateSlug(plan, now?) is unchanged and still names the archive files in plans/ (decision-day UTC date).
  • New resolvePlanHistorySlug(project, plan, { now?, current? }) in packages/shared/storage.ts picks the history directory. The directory name keeps the {heading}-YYYY-MM-DD form, but the date now records when the chain started. The rule, in order:
    1. current: an open server keeps its own slug for revisions with the same heading (Pi updatePlan, the Claude Code mod's revisions).
    2. Today's {heading}-{date} directory, if it already has versions. Same-day behavior is unchanged.
    3. The most recently written existing {heading}-YYYY-MM-DD directory for this project, if its newest version file's mtime is within 48h.
    4. Otherwise, today's directory (a new chain).

Both plan servers use it at startup and in updatePlan (packages/server/index.ts, apps/pi-extension/server/serverPlan.ts, vendored through vendor.sh). The history endpoints and the feedback archive's target.slug / planVersionFile use the history slug. The archive snapshot calls keep the dated slug. Nothing on disk is renamed or migrated: existing dated directories are simply continued.

Why a 48h recency window

  • The issue suggests always continuing the newest sibling. That would merge every later plan with a generic heading (# Implementation Plan) into one chain forever. A window keeps unrelated same-heading plans days apart separate.
  • 24h would not fix the reported case. v1 was saved at 2026-10-02T20:02Z and the revision came at 2026-10-04T10:57Z, about 39h later. 48h covers "resumed the next day" with margin.
  • Recency comes from the saved file's mtime, not the date in the directory name, so the UTC-vs-local boundary noted in the issue no longer matters.
  • Every saved revision refreshes the window, so a plan that is actively iterated keeps one chain however long it runs.
  • Trade-off: a same-heading but unrelated plan within 48h joins the chain. That is already the behavior within one day, and the version picker lets the reviewer choose any base.

Tests

  • packages/server/storage.test.ts:
    • save at 23:50Z, revise at 00:10Z: one chain, previousPlan set
    • the reported ~39h gap continues the chain
    • the same heading 4 days later starts fresh
    • today's directory wins when it already has versions (a split left by an older binary)
    • another heading's chain is never continued
    • a session's current slug holds
    • archive filename stays dated by the decision day
    • The old "same heading on same day produces same slug" test now pins the archive slug's date against an injected clock on both sides of midnight.
  • packages/server/plan-history-midnight.test.ts and its Pi twin start a real plan server against a chain seeded under yesterday's date. They check /api/plan previousPlan / versionInfo, /api/plan/versions slug, Pi updatePlan staying on the chain, and the approved snapshot named with today's date.
  • Every test above fails with the old keying (checked by stubbing the resolver back to generateSlug).

All tests use a temp PLANNOTATOR_DATA_DIR set inside the test, with the clock injected or mtimes set via utimes.

Verified

  • bun run typecheck: clean.
  • bun test: 5934 pass, 0 fail.
  • Live check with a CLI compiled to /tmp (not ~/.local/bin), using a temp data dir with history/<project>/live-midnight-check-<yesterday>/001.md stamped 3h ago. Fed an ExitPlanMode PermissionRequest:
    • /api/plan returned previousPlan = v1 and version 2 / totalVersions 2.
    • /api/plan/versions returned slug: live-midnight-check-2026-10-03.
    • 002.md was written into yesterday's directory.
    • Approving wrote plans/live-midnight-check-2026-10-04-approved.md.

Not verified live: the Pi extension inside a running Pi session (it is covered by the Pi server test above).

Plan history was keyed by generateSlug() = <heading>-<UTC date>, so a
revision after UTC midnight landed in a new history dir as version 1 and
lost its Versions tab and diff. Split the two jobs: generateSlug() still
names the dated archive snapshots in plans/, while the new
resolvePlanHistorySlug() picks the history chain: the open server's own
slug (same heading), else today's dir if it has versions, else the most
recently written <heading>-<date> dir of the project when its newest
version is within 48h, else today's dir. Nothing on disk is renamed.

Both plan servers (Bun and Pi) use it at startup and in updatePlan.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plan history is keyed by the current date, so a plan that spans midnight loses its Versions tab and diff

1 participant