Skip to content

fix(conversations): an unreadable conversation file is said, not 'no prompts' (#1306) - #1434

Merged
Juliusolsson05 merged 7 commits into
integration/batch-2026-09-27-pfrom
fix/conversation-prompts-unreadable
Sep 27, 2026
Merged

Juliusolsson05 merged 7 commits into
integration/batch-2026-09-27-pfrom
fix/conversation-prompts-unreadable

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1306.

Problem

View Prompts (and search) on a conversation whose transcript or store is there but unreadable said "no prompts". As the issue suspected, the pattern was in every source:

  • Pi: catch { return [] }, and the package's resolution skips an unreadable header, so the session reads as "not found".
  • Grok: catch { return [] }.
  • Claude and Codex: the shared extractPromptsFromFile turned every stat and read failure into an empty answer.
  • OpenCode: a store that could not be opened answered [].
  • The modal showed the raw IPC message for a rejection (q22).

Change

  • ConversationPromptsUnreadable (sources/types.ts): a typed error with a fixed message and the cause.
  • One rule for every source: no file (yet) is [], because a fresh session has no transcript and View Prompts shows the live feed. A file or store that is there but unreadable throws.
    • promptFolder is empty only for ENOENT/ENOTDIR.
    • Pi checks whether a file named for the session exists but cannot be opened.
    • OpenCode throws only when opencode.db exists.
    • Finding the file follows the same rule (steering q116): Claude's direct stat and projects walk, Codex's rollout walk and Pi's listing treat only ENOENT/ENOTDIR as absence. The Codex and Pi packages swallow their own readdir errors, so each source probes its root directories first.
  • ConversationService.prompts logs the cause in main and rethrows. Search already catches per conversation (label-only for that row).
  • ViewPromptsModal shows "Couldn't read this conversation's prompts."

Plan: docs/plans/2026-09-27-conversation-prompts-unreadable.md.

Rules added in review (round 1 and the capped verification pass)

  • Present means readable, or it is unknown. isPresent(provider, path) treats only ENOENT/ENOTDIR as absence; EACCES and every other stat failure throw. Used for Codex's known and indexed rollout paths and for OpenCode's opencode.db, including one under an inaccessible parent.
  • A tree that cannot be listed is unknown, not empty. assertTreeListable throws on any readdir failure other than absence. It also throws on a file named for THIS conversation that the locator skipped, i.e. an unreadable own rollout in a listable tree.
  • Codex: an index that cannot be opened is unknown. On a miss, the walk runs, and an unknown index then throws instead of answering [].
  • A transcript whose every complete line is malformed is unreadable (TranscriptUnparseable). This is checked on both the fresh fold and the cache hit. A file with some good lines still answers its good prompts.
  • Pi: only a real session header for this id may mean "another cwd". A header that doesn't parse, has another type or id, or id: null is damage.
  • Grok: a transcript without its sibling summary.json (or the reverse) is unreadable. A write still in progress is retried twice at 150 ms before it is called unreadable.
  • OpenCode: a query that fails after the store opened is rethrown as ConversationPromptsUnreadable.

Tests (all red on main; real files from the recorded conversation corpus, made unreadable with chmod)

  • New promptsUnreadable.system.test.ts: Claude, Codex, OpenCode (a non-database store) and Grok throw the typed error, and a missing file stays [].
  • pi.system.test.ts: the same for Pi.
  • Steering q116, through prompts(), each failing then recovering: an unlistable Claude project directory; an unlistable Codex sessions tree (a missing one stays []); an unlistable Pi session directory.
  • service.system.test.ts: View Prompts rejects, and search still finds another conversation.
  • ViewPromptsModal.renderer.test.tsx: fixed words, no IPC text.
  • Round 1 and verification, each red before its fix (promptsUnreadable.system.test.ts unless noted):
    • Codex: a locked day directory, an unopenable index, and its own rollout unreadable in a listable tree (the recorded rollout ff781eb7-…).
    • OpenCode: a locked parent directory, plus opencodeQueryError.test.ts (a query that fails after the store opened).
    • Pi: a damaged header, a fresh home, ENOTDIR, a non-header first row, and id: null.
    • Grok: a missing summary, and a write in progress.
    • Claude: an all-garbage transcript (including a second, cached read) and a recorded file with one garbage line.
    • service.system.test.ts asserts the warning in main.

Verification

  • rm -rf .tsc-out; npx tsc -b: clean.
  • NODE_ENV=test npx vitest run src/main/conversations: 127/127 at ba9c793 (origin/main merged in: a real conflict in service.system.test.ts's imports, resolved by keeping both). The modal test passes.
  • Reviews: round 1 (a, b, c), then a capped verification pass. The final verdicts, a and b verified by B6 (manager) and c MERGE-READY, are in the disposition comments.
  • Residual (B6, not blocking): Codex matches its rollout file name to the conversation id case-sensitively. An upper-case UUID file name would not be recognised as this conversation's, so it would read as absent.
  • History: the first commit (87692683) is the plan alone; the implementation is 5cee073e.
  • The app was not launched.
  • This is read-only code; the "unknown is never empty" rule is for delete paths, but its spirit is the fix here.

🤖 Generated with Claude Code

@Juliusolsson05 Juliusolsson05 added type:bug Something works wrong class:C3-silent-failure The app knows it failed and does not say sev:P3 Minor labels Sep 27, 2026
Juliusolsson05 and others added 2 commits September 27, 2026 07:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…prompts' (#1306)

Every source (Claude, Codex, OpenCode, Pi, Grok) raises a typed
ConversationPromptsUnreadable when a transcript or store is there but cannot
be read or listed; only genuine absence answers []. promptFolder rethrows
non-ENOENT errors; View Prompts says it in fixed words; search degrades that
one row. Steering q116: discovery (stat/readdir walks) follows the same rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 force-pushed the fix/conversation-prompts-unreadable branch from 290621a to 5cee073 Compare September 27, 2026 14:22
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Steering q116 addressed; the branch was rebuilt before any reviewer started (none had).

  1. Discovery no longer converts unreadable into "not here". Claude's direct stat and projects walk, Codex's rollout walk and Pi's listing treat only ENOENT/ENOTDIR as absence; anything else raises ConversationPromptsUnreadable. The Codex and Pi packages swallow their own readdir errors, so each source probes its root directories first. Three fail-then-recover tests go through prompts() (Claude project dir, Codex sessions tree, Pi session dir); all three are red on the previous head.
  2. Plan-first history: 87692683 holds the plan alone, and 5cee073e holds the implementation and tests. The PR body is updated.

src/main/conversations: 101/101; npx tsc -b: clean. The review briefs point at 5cee073e.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits September 27, 2026 10:43
… review a, b)

existsSync read EACCES as absent (Codex known/indexed rollouts, the OpenCode
database); the Codex walk skipped locked day directories; an unopenable Codex
index with no rollout found; a Pi session named for the id with a damaged
header; a Grok transcript without its summary; and a Claude/Codex transcript
whose every line is garbage (TranscriptUnparseable) all answered []. Each is
now ConversationPromptsUnreadable. isPresent / assertTreeListable tell absent
from unknown; an OpenCode query failure after open is typed; a Grok read racing
its writer retries twice before it is said.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1434 review c)

A fresh Pi home with no .pi answers [] (the probe's missing half survived a
mutation), a store path through a plain file (ENOTDIR) is absence, and the
service's cause log is asserted. Plan records round 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Round-1 disposition. a, b and c were all FIX-BEFORE-MERGE. Every valid finding is fixed fail-first, and each new test is red on the previous head (5cee073).

a and b (major): unknown still read as "no prompts" one level down. Fixed in 0974c69.

  • Codex: a known or indexed rollout under a locked directory. existsSync answers false for EACCES; isPresent now tells absent from unknown.
  • Codex: the package walk skips locked sessions/YYYY/MM/DD. assertTreeListable runs on the miss path.
  • Codex: an index that is there but won't open, with no rollout found.
  • OpenCode: a database in an inaccessible directory. A query that fails after the open is now typed too.
  • Pi: a session named for the id whose header is damaged. The header is parse-checked; a readable header for another cwd is still "not this session".
  • Grok: a transcript whose summary.json is gone. A read racing Grok's writer retries twice before it is said (a's minor).
  • Claude/Codex: a transcript whose every line is garbage now raises TranscriptUnparseable. One good record keeps it readable.

c (major, test gap): the absent half of the Pi rule survived a mutation. It is pinned on a fresh home with no .pi (8412949). c (minor): ENOTDIR is pinned as absence, and the service's cause log is asserted. All three of c's mutations (M17, M22, M5) are now red.

Not changed (residuals):

  • c4: the prompt folder's stat-catch rethrow is unreachable through the sources.
  • c5: the stat/read vanish race is untested defensive code.
  • c7 and M19: the Codex and Pi package-walk catches are unreachable; the packages swallow their own errors, and the probes cover the real path.
  • c8: the modal shows the error above "no prompts found". That is pre-existing; I can file an issue if wanted.

Declined: b4. An inaccessible unrelated Claude project aborting the fallback walk is the stated contract (unknown is never "no prompts"), and c independently called it a design stance, not a defect.

Checks: npx tsc -b clean; the conversations and renderer conversation suites pass 132/132. Head: 8412949.

🤖 Generated with Claude Code

…row are unreadable (#1434 verification a)

The Codex locator skips a file it cannot open, so a listable tree holding the
conversation's own unreadable rollout read as absent; assertTreeListable now
fails on a file named for the conversation. A Pi file named for the session
whose first row is not its session header is damaged, not 'another cwd'.
The OpenCode typed query error and the prompt folder's cache-hit parse check
are pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Verification (a) disposition. Verifier a was FIX-BEFORE-MERGE: two major findings and two test gaps. All four are fixed fail-first in e1e67e8, and each mutation is red.

  • Codex (major): the conversation's own rollout, with an unreadable mode inside a listable tree, still read as absent. The locator skips a file it cannot open, and the tree check listed directories only. assertTreeListable now also fails on a file named for this conversation. It is pinned with the corpus's recorded rollout, chmod 000, no index and no cached path. After the file is restored, the prompts are the same as before.
  • Pi (major): a named file whose first row parses but is not this session's header still read as absent. Only a real session header for this id may say "another cwd"; anything else is damaged. Pinned.
  • OpenCode (test gap): the typed error after a successful open had no test. It is now pinned at the sqlite boundary.
  • Prompt folder (test gap): the cache-hit parse check had no test. A second read of an all-garbage transcript is now pinned.
  • Residual (a's own note): the known-path isPresent is not isolated from the tree check that also catches its case. The behaviour is covered either way.
  • The verification pass is capped, so B6: please manager-verify a at e1e67e8. Verifiers b and c are running.

🤖 Generated with Claude Code

…1434 verification b)

Verifier b's repro ({type:'session', id:null}) was already rejected at
e1e67e8 by the id check; this pins it (dropping the id check turns it red).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Verification b and c disposition at 38c1644.

  • c: MERGE-READY. It confirmed that all three of its round-1 survivors are now killed, and that the stated residuals (unreachable stat catches, package-walk catches) are unchanged.
  • b (FIX-BEFORE-MERGE, reviewed at 8412949):
    • The finding: a Pi file named for this session whose header parses but has id: null read as "no prompts".
    • The id check added at e1e67e8 (verification a) already rejects it: only {type:'session', id: <this id>} may mean "another cwd".
    • 38c1644 adds b's exact repro as a test. Dropping the id check turns only that test red, which also closes b's surviving parsed-object mutation.
    • b's four round-1 findings were confirmed fixed, each fail-first.

B6: the verification cap is spent. Reviewer c's latest report is MERGE-READY; please manager-verify a (e1e67e8) and b (38c1644).

🤖 Generated with Claude Code

…pts-unreadable

# Conflicts:
#	src/main/conversations/service.system.test.ts
@Juliusolsson05
Juliusolsson05 changed the base branch from main to integration/batch-2026-09-27-p September 27, 2026 20:35
@Juliusolsson05
Juliusolsson05 merged commit d58e1d0 into integration/batch-2026-09-27-p Sep 27, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/conversation-prompts-unreadable branch September 27, 2026 20:35
Juliusolsson05 added a commit that referenced this pull request Sep 27, 2026
Real conflict: batch Q's #1434 and this PR both changed codex.ts's imports
(#1434 adds ConversationPromptsUnreadable and friends from ./types.js, this
PR adds LruMap). Both kept.

Semantic conflict: #1434's promptsUnreadable.system.test.ts reads the
private rolloutPaths as a Map (.has/.get), which this PR bounded as an
LruMap. The two Codex tests now read it with peek(), which does not touch
recency, so the lookup cannot change what the source evicts next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

class:C3-silent-failure The app knows it failed and does not say sev:P3 Minor type:bug Something works wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pi conversation prompts show 'no prompts' when the session file is unreadable

1 participant