Skip to content

feat(runtime): give each project its own conversations - #52

Merged
mangit955 merged 1 commit into
mainfrom
feat/sessions
Aug 5, 2026
Merged

feat(runtime): give each project its own conversations#52
mangit955 merged 1 commit into
mainfrom
feat/sessions

Conversation

@mangit955

Copy link
Copy Markdown
Owner

Why

Woopcode kept one conversation for the whole machine. getConversationPath
returned a single global conversation.json, and getExecutionLogPath a single
execution-log.json beside it. Three things followed, all of them live on
main:

  • /new destroyed history. It called saveConversation([]), and
    docs/guides/sessions-and-history.md said outright that there was no undo.
  • History leaked between repositories. The same guide documented this as a
    warning rather than a bug: "there is one history file, not one per repository
    … use /new when you switch projects, or the agent begins with context from
    somewhere else entirely." It applied to the execution log too, which reaches
    the model through contextForTurn as a description of what has already been
    done here.
  • There was no way back to anything. No ids, no listing, no --continue, no
    --resume, no branching.

What

A session is one record under sessions/<project-slug>/<id>.json, keyed by the
repository root. index.json beside them is a derived cache — delete it, or let
it fall behind the files, and it is rebuilt by scanning.

Surface
Interactive /resume (picker or by ref), /sessions, /rename, /branch; /new keeps the old session and prints the id to get back to it
Command line -c/--continue, --resume [ref], --new, --fork-session, -n/--name, --no-session-persistence
Subcommand woopcode sessions list | show | prune
Retention retentionDays in providers.json, default 30, 0 keeps forever

Decisions worth arguing with:

  • The slug carries a hash of the full path. Slugifying alone maps /a/b and
    /a-b to the same directory, silently merging two unrelated projects.
  • A bare launch continues; -p does not. Continuing is what the docs have
    always promised, now correctly scoped. A headless run inheriting whatever was
    open interactively is a surprise a scripted caller cannot see or opt out of.
  • Titles come from the first prompt, not a model. A generated title costs a
    provider request per session on whichever of the three is configured, and adds
    a failure path at the moment a session is created. /rename covers the rest.
  • Pre-sessions history lands in a legacy bucket, not in whichever project
    is opened first — it was shared by every repository, so no project can honestly
    claim it. Take a turn in it and it is adopted by the project you are working
    in; open it to read and it stays put.
  • Nothing is written until a turn has run, so starting Woopcode and quitting
    leaves no empty session for the picker to offer.

Two windows on one session used to lose a turn, each writing the whole record so
the later save discarded the earlier. saveConversation on main behaved the
same way, so this is inherited rather than introduced — but it is silent loss of
someone's work, so it is now detected and answered by branching, leaving the
other window untouched.

Verified

A first pass went green on every gate and sweep, and reading the diff afterwards
found seven defects — one of which made --fork-session write into the session
it exists to protect. All are fixed, and each regression test was checked by
reverting its fix and confirming it goes red:

Defect Consequence
forkSession scoped to the current project /branch failed on legacy and cross-project sessions
fork() ?? original fallback a failed copy wrote into the original
index lost a row on concurrent writes the session was never listed again
prune counter shared across projects one expired session restamped every later project
pruneIfDue wrote an index to record itself created the directory lazy creation exists to avoid
bare --resume silently identical to --continue
/resume returned "" blank row in the transcript
bun run verify --staged   4 gates passed (docs lint, docs surface, tsc, tests)
bun test                  1503 pass, 0 fail, 95 files
reverse-order sweep       1503 pass, 0 fail
4x concurrent             1503 pass, 0 fail on all four
bun run replay:baseline   byte-identical to the pre-change baseline

The replay comparison matters because moving the execution log into the session
changes what the prompt carries; it does not.

Also exercised by hand: migration importing and retiring its source, legacy
history invisible to a project but visible under --all, sessions show/prune,
lazy creation leaving no directory behind, exit codes, and the benchmark
harness's woopcode -p invocation shape (fresh session per trial under its own
XDG_CONFIG_HOME, unchanged events and exit codes).

Not verified: no live-provider turn — everything through the agent loop runs
against MockProviderClient, and the one real request made was an auth failure
that proved the session path only. The session picker has a render test and store
unit tests but its keyboard flow has never been driven by hand, so whether
ink-text-input swallows CtrlA in the search field is
still open; Tab is the fallback if it does.

Woopcode kept one conversation for the whole machine. `getConversationPath`
returned a single global `conversation.json`, and `getExecutionLogPath` a single
`execution-log.json` beside it. Three things followed, all of them live:

`/new` destroyed history — it called `saveConversation([])`, and the guide said
outright that there was no undo. History leaked between repositories, which the
guide also documented, as a warning rather than a bug: a session started in one
project restored into a turn taken in another, execution log included, which the
model reads as a description of what has already been done *here*. And there was
no way back to anything: no ids, no listing, no resume, no branching.

A session is now one record under `sessions/<project-slug>/<id>.json`, keyed by
the repository root. The slug carries a hash of the full path because slugifying
alone maps `/a/b` and `/a-b` to the same name, which would silently merge two
unrelated projects. `index.json` beside them is a derived cache: delete it, or
let it fall behind the files, and it is rebuilt by scanning.

A bare launch continues the newest session in the project, which is what the
documentation has always promised, now correctly scoped. `-p` starts its own
instead of inheriting whatever was open interactively — a surprise a scripted
caller could not see or opt out of. Nothing is written until a turn has run, so
starting Woopcode and quitting leaves nothing behind.

Titles come from the first prompt rather than a model. A generated title costs a
provider request per session on whichever of the three is configured, and adds a
failure path at the moment a session is created; `/rename` is there for anyone
who wants better.

History from before sessions existed is imported once into a `legacy` bucket
rather than attributed to whichever project is opened first — it was shared by
every repository, so no project can honestly claim it. Take a turn in it and it
is adopted by the project you are working in; open it to read and it stays put.

Two windows on one session used to lose a turn, each writing the whole record so
the later save discarded the earlier one. The single conversation file behaved
the same way, so this is inherited rather than new, but it is silent loss of
someone's work: it is now detected and answered by branching, leaving the other
window untouched.
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
woop-code Ready Ready Preview Aug 5, 2026 1:46pm

@mangit955
mangit955 marked this pull request as ready for review August 5, 2026 13:48
@mangit955
mangit955 merged commit 2b11577 into main Aug 5, 2026
6 checks passed
@mangit955
mangit955 deleted the feat/sessions branch August 5, 2026 13:48
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.

1 participant