Skip to content

fix(store): resolve four packages/store bugs (#499, #500, #501, #502) - #522

Draft
TYRMars wants to merge 1 commit into
mainfrom
claude/vibrant-dijkstra-1ymjgp
Draft

fix(store): resolve four packages/store bugs (#499, #500, #501, #502)#522
TYRMars wants to merge 1 commit into
mainfrom
claude/vibrant-dijkstra-1ymjgp

Conversation

@TYRMars

@TYRMars TYRMars commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Auto-resolves four code-review bug reports in packages/store, all in the JSON-file backend and the WS chat save path.

#502atomicWrite shared .tmp → concurrent writes to one id ENOENT

The staging file was derived solely from the target path, so all concurrent writers of a row shared one .tmp; whoever renamed first consumed it and the second rename rejected with ENOENT, silently dropping a turn (both message routes catch-and-ignore save failures). Now each write stages to a unique <path>.<pid>.<uuid>.tmp and cleans it up on failure → last-writer-wins.

#501list() no shape guard → one foreign .json 500s GET /v1/conversations

JsonFileConversationStore.list blind-cast every parsed .json and dereferenced stored.messages.length. A well-formed but foreign file (e.g. workspaces.json flushed into the same dir, or a ..-escaped requirement row) threw a TypeError that killed the whole endpoint. Now records without an array messages are skipped and each per-file read is wrapped in try/catch, matching activity-store / observability.

#499encodeId leaves .. intact → partition escape

encodeId("..") === "..", so a project_id of .. joined as a directory component escaped the requirements partition into the sibling conversation dir (corrupting it and permanently breaking the conversation list). Added encodeDirComponent (rejects ./../empty via StoreError) and applied it to every directory-partition helper: requirement, comment, activity, and requirement-run stores.

#500 — WS chat save clobbers conversation metadata

The WS turn-completion handler saved via ConversationStoreBase.save, which hard-codes defaultMetadata(), so every completed turn erased project_id and reset lifecycle to active — permanently un-archiving requirement-run conversations and dropping their project link. The handler now round-trips ConversationMetadata: loaded via loadEnvelope on resume, kept in sync on new/reset, and persisted via saveEnvelope on done.

Tests

  • store.test.ts: list skips a foreign JSON file; concurrent same-id saves don't ENOENT and leave no .tmp litter; encodeDirComponent rejects escaping ids.
  • requirement-store.test.ts: JSON-file store refuses a partition-escaping project_id.

Verification

  • pnpm -r typecheck clean across the workspace.
  • packages/store + packages/server tests pass except pre-existing environmental failures (unbuildable better-sqlite3 / node-pty native modules — same failures on the clean base, unrelated to this change).
  • eslint clean on all touched files.

🤖 Generated with Claude Code

https://claude.ai/code/session_013ehdr6rdrz54r9gWN6oJ7P


Generated by Claude Code

…rite tmp, WS metadata round-trip

Resolves four packages/store code-review bugs (#499, #500, #501, #502):

- #502 atomicWrite shared a fixed `<path>.tmp` across all writers of a row,
  so two concurrent writes to one id deterministically ENOENT on rename and
  the loser's turn was silently dropped. Stage to a per-write unique name
  (`<path>.<pid>.<uuid>.tmp`) and clean up on failure → last-writer-wins.

- #501 JsonFileConversationStore.list blind-cast every `.json` in the base
  dir and dereferenced `stored.messages.length` with no shape guard, so one
  foreign file (e.g. workspaces.json flushed into the same dir) permanently
  500'd GET /v1/conversations. Skip records whose `messages` isn't an array
  and wrap each per-file read in try/catch.

- #499 encodeId leaves `.`/`..` intact, so a project_id of `..` joined as a
  directory component escaped the requirements partition into the sibling
  conversation dir. Add encodeDirComponent (rejects `.`/`..`/empty) and use
  it in every directory-partition helper (requirement / comment / activity /
  requirement-run stores).

- #500 the WS chat path saved via ConversationStoreBase.save, which hard-codes
  defaultMetadata(), so every completed turn erased project_id and un-archived
  the conversation. Round-trip ConversationMetadata through loadEnvelope on
  resume and saveEnvelope on done, keeping it in sync on new/reset.

Adds tests for the three store-level fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ehdr6rdrz54r9gWN6oJ7P
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.

2 participants