Skip to content

fix: hydrate thread titles from Codex session index#8

Open
SeleiXi wants to merge 4 commits intofriuns2:mainfrom
SeleiXi:codex/fix-session-title-source
Open

fix: hydrate thread titles from Codex session index#8
SeleiXi wants to merge 4 commits intofriuns2:mainfrom
SeleiXi:codex/fix-session-title-source

Conversation

@SeleiXi
Copy link
Contributor

@SeleiXi SeleiXi commented Mar 23, 2026

fix: hydrate thread titles from Codex session index

  • read thread_name values from the local Codex session_index.jsonl
  • merge session index titles with the existing thread title cache
  • prefer real Codex session titles over preview text in the sidebar/session list while keeping the cache fallback path

Closes #7

@qodo-code-review
Copy link

Review Summary by Qodo

Hydrate thread titles from Codex session index

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Read thread titles from local Codex session_index.jsonl file
• Merge session index titles with persisted thread title cache
• Prefer session index titles over cached fallback values
• Implement cache merging and trimming logic for consistency
Diagram
flowchart LR
  SI["session_index.jsonl"] -->|"read and parse"| SITC["Session Index<br/>Thread Cache"]
  PC["Persisted Cache<br/>thread-titles"] -->|"read"| PTC["Persisted<br/>Thread Cache"]
  SITC -->|"merge with<br/>preference"| MTC["Merged Thread<br/>Title Cache"]
  PTC --> MTC
  MTC -->|"trim to<br/>MAX_THREAD_TITLES"| API["GET /codex-api/<br/>thread-titles"]
Loading

Grey Divider

File Changes

1. src/server/codexAppServerBridge.ts ✨ Enhancement +106/-1

Add session index title hydration and cache merging

• Added getCodexSessionIndexPath() function to locate session_index.jsonl
• Implemented normalizeSessionIndexThreadTitle() to parse and validate session index entries
• Added trimThreadTitleCache() to enforce MAX_THREAD_TITLES limit
• Implemented mergeThreadTitleCaches() to combine session index and persisted caches with overlay
 priority
• Added readThreadTitlesFromSessionIndex() to read and parse JSONL file, deduplicating by latest
 timestamp
• Implemented readMergedThreadTitleCache() to combine both cache sources
• Updated /codex-api/thread-titles endpoint to use merged cache instead of persisted cache only

src/server/codexAppServerBridge.ts


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 23, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. Session titles overridden🐞 Bug ✓ Correctness
Description
readMergedThreadTitleCache() merges session_index.jsonl titles with the persisted cache in a way
that lets persisted titles override session-index titles, so preview/generated titles can still win
over real Codex thread_name values.
Code

src/server/codexAppServerBridge.ts[R424-444]

+function mergeThreadTitleCaches(base: ThreadTitleCache, overlay: ThreadTitleCache): ThreadTitleCache {
+  const titles = { ...base.titles, ...overlay.titles }
+  const order: string[] = []
+
+  for (const id of [...overlay.order, ...base.order]) {
+    if (!titles[id] || order.includes(id)) continue
+    order.push(id)
+  }
+
+  for (const id of Object.keys(titles)) {
+    if (!order.includes(id)) {
+      order.push(id)
+    }
+  }
+
+  return trimThreadTitleCache({ titles, order })
+}
+
async function readThreadTitleCache(): Promise<ThreadTitleCache> {
 const statePath = getCodexGlobalStatePath()
 try {
Evidence
In mergeThreadTitleCaches(base, overlay), the merged titles object is built as `{
...base.titles, ...overlay.titles }, meaning overlay` wins key conflicts.
readMergedThreadTitleCache() passes sessionIndexCache as base and persistedCache as
overlay, so persisted titles override session-index titles. The UI persists both generated titles
(from prompt preview) and real rename updates via persistThreadTitle(), so this precedence
directly affects what users see in the sidebar.

src/server/codexAppServerBridge.ts[424-508]
src/composables/useDesktopState.ts[1936-1947]
src/composables/useDesktopState.ts[1657-1666]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GET /codex-api/thread-titles` is intended (per PR) to prefer real Codex session titles from `session_index.jsonl`, but the current merge gives precedence to persisted cache entries on key conflict.
### Issue Context
- `mergeThreadTitleCaches(base, overlay)` uses `{ ...base.titles, ...overlay.titles }` so `overlay` wins.
- `readMergedThreadTitleCache()` calls `mergeThreadTitleCaches(sessionIndexCache, persistedCache)` which makes persisted titles override session-index titles.
### Fix Focus Areas
- Ensure session-index titles win conflicts (either swap argument order in `readMergedThreadTitleCache()` or invert the spread order in `mergeThreadTitleCaches()`).
- Keep trimming behavior (`MAX_THREAD_TITLES`) intact.
- src/server/codexAppServerBridge.ts[424-508]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. JSONL parsed on request🐞 Bug ➹ Performance
Description
GET /codex-api/thread-titles now reads and parses the full session_index.jsonl file on the
request path, which can delay initial thread list loading because the UI awaits this endpoint during
loadThreads().
Code

src/server/codexAppServerBridge.ts[R466-508]

+async function readThreadTitlesFromSessionIndex(): Promise<ThreadTitleCache> {
+  try {
+    const raw = await readFile(getCodexSessionIndexPath(), 'utf8')
+    const latestById = new Map<string, SessionIndexThreadTitle>()
+
+    for (const line of raw.split(/\r?\n/u)) {
+      const trimmed = line.trim()
+      if (!trimmed) continue
+
+      try {
+        const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed) as unknown)
+        if (!entry) continue
+
+        const previous = latestById.get(entry.id)
+        if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
+          latestById.set(entry.id, entry)
+        }
+      } catch {
+        // Skip malformed lines and keep scanning the rest of the index.
+      }
+    }
+
+    const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs)
+    const titles: Record<string, string> = {}
+    const order: string[] = []
+    for (const entry of entries) {
+      titles[entry.id] = entry.title
+      order.push(entry.id)
+    }
+
+    return trimThreadTitleCache({ titles, order })
+  } catch {
+    return { titles: {}, order: [] }
+  }
+}
+
+async function readMergedThreadTitleCache(): Promise<ThreadTitleCache> {
+  const [sessionIndexCache, persistedCache] = await Promise.all([
+    readThreadTitlesFromSessionIndex(),
+    readThreadTitleCache(),
+  ])
+  return mergeThreadTitleCaches(sessionIndexCache, persistedCache)
+}
Evidence
readThreadTitlesFromSessionIndex() does readFile(..., 'utf8'), splits the entire contents into
lines, and JSON.parses each line before returning. The client calls getThreadTitleCache() during
loadThreads() via loadThreadTitleCacheIfNeeded(), and loadThreads() awaits that promise
alongside getThreadGroups(), so any extra latency in this endpoint is directly user-visible during
initial load.

src/server/codexAppServerBridge.ts[466-508]
src/composables/useDesktopState.ts[1924-1959]
src/api/codexGateway.ts[486-496]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The server parses the entire `session_index.jsonl` on each `GET /codex-api/thread-titles`, which can be slow for large files and impacts UI load because the client awaits this call during initial thread loading.
### Issue Context
- Server: `readFile()` + `split()` + `JSON.parse` per line.
- Client: `loadThreads()` awaits `loadThreadTitleCacheIfNeeded()` which calls `/codex-api/thread-titles`.
### Fix Focus Areas
- Add a simple in-memory cache with an mtime check for `session_index.jsonl` (recompute only when the file changes).
- Or stream the file line-by-line (e.g., `readline`) to avoid loading the full file into memory.
- Consider early limiting work (e.g., keep only the most recent N entries) since `MAX_THREAD_TITLES` is 500.
- src/server/codexAppServerBridge.ts[466-508]
- src/composables/useDesktopState.ts[1924-1959]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@friuns2
Copy link
Owner

friuns2 commented Mar 23, 2026

@SeleiXi
Copy link
Contributor Author

SeleiXi commented Mar 23, 2026

Thanks but what are those commits?

@SeleiXi fix(cli): support Windows codex command resolution b09c077 @SeleiXi build(cli): regenerate dist bundle for spawn fix 695762a

Sorry, those were unrelated commits to this issue. I've reverted them all and double-checked.

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.

Use Codex session index titles instead of preview text for thread names

2 participants