-
Notifications
You must be signed in to change notification settings - Fork 20
Persist thread read state across refreshes, ports, and browsers #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -488,6 +488,10 @@ function trimThreadTitleCache(cache: ThreadTitleCache): ThreadTitleCache { | |
| return { titles, order } | ||
| } | ||
|
|
||
| function normalizeThreadReadStateMap(value: unknown): Record<string, string> { | ||
| return normalizeStringRecord(value) | ||
| } | ||
|
|
||
| function mergeThreadTitleCaches(base: ThreadTitleCache, overlay: ThreadTitleCache): ThreadTitleCache { | ||
| const titles = { ...base.titles, ...overlay.titles } | ||
| const order: string[] = [] | ||
|
|
@@ -530,6 +534,31 @@ async function writeThreadTitleCache(cache: ThreadTitleCache): Promise<void> { | |
| await writeFile(statePath, JSON.stringify(payload), 'utf8') | ||
| } | ||
|
|
||
| async function readThreadReadStateMap(): Promise<Record<string, string>> { | ||
| const statePath = getCodexGlobalStatePath() | ||
| try { | ||
| const raw = await readFile(statePath, 'utf8') | ||
| const payload = asRecord(JSON.parse(raw)) ?? {} | ||
| return normalizeThreadReadStateMap(payload['thread-read-state']) | ||
| } catch { | ||
| return {} | ||
| } | ||
| } | ||
|
|
||
| async function writeThreadReadStateMap(state: Record<string, string>): Promise<void> { | ||
| const statePath = getCodexGlobalStatePath() | ||
| let payload: Record<string, unknown> = {} | ||
| try { | ||
| const raw = await readFile(statePath, 'utf8') | ||
| payload = asRecord(JSON.parse(raw)) ?? {} | ||
| } catch { | ||
| payload = {} | ||
| } | ||
|
|
||
| payload['thread-read-state'] = normalizeThreadReadStateMap(state) | ||
| await writeFile(statePath, JSON.stringify(payload), 'utf8') | ||
| } | ||
|
Comment on lines
+548
to
+560
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Global state write races Multiple endpoints update the same ~/.codex/.codex-global-state.json using independent read-modify-write cycles without serialization, so concurrent requests can clobber unrelated keys. Adding frequent thread-read-state writes increases the likelihood of losing workspace roots or thread title updates. Agent Prompt
|
||
|
|
||
| function getSessionIndexFileSignature(stats: { mtimeMs: number; size: number }): string { | ||
| return `${String(stats.mtimeMs)}:${String(stats.size)}` | ||
| } | ||
|
|
@@ -2132,6 +2161,12 @@ export function createCodexBridgeMiddleware(): CodexBridgeMiddleware { | |
| return | ||
| } | ||
|
|
||
| if (req.method === 'GET' && url.pathname === '/codex-api/thread-read-state') { | ||
| const state = await readThreadReadStateMap() | ||
| setJson(res, 200, { data: state }) | ||
| return | ||
| } | ||
|
|
||
| if (req.method === 'POST' && url.pathname === '/codex-api/thread-search') { | ||
| const payload = asRecord(await readJsonBody(req)) | ||
| const query = typeof payload?.query === 'string' ? payload.query.trim() : '' | ||
|
|
@@ -2181,11 +2216,22 @@ export function createCodexBridgeMiddleware(): CodexBridgeMiddleware { | |
| return | ||
| } | ||
|
|
||
| if (req.method === 'PUT' && url.pathname === '/codex-api/thread-read-state') { | ||
| const payload = await readJsonBody(req) | ||
| const record = asRecord(payload) | ||
| if (!record) { | ||
| setJson(res, 400, { error: 'Invalid body: expected object' }) | ||
| return | ||
| } | ||
| await writeThreadReadStateMap(normalizeThreadReadStateMap(record.state ?? record)) | ||
| setJson(res, 200, { ok: true }) | ||
| return | ||
|
Comment on lines
+2219
to
+2228
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Read-state put overwrites map PUT /codex-api/thread-read-state replaces the entire stored map with the request body instead of merging with existing state. If multiple browsers/windows are open, a stale client can overwrite and delete other threads’ read timestamps (lost updates). Agent Prompt
|
||
| } | ||
|
|
||
| if (req.method === 'GET' && url.pathname === '/codex-api/telegram/status') { | ||
| setJson(res, 200, { data: telegramBridge.getStatus() }) | ||
| return | ||
| } | ||
|
|
||
| if (req.method === 'GET' && url.pathname === '/codex-api/events') { | ||
| res.statusCode = 200 | ||
| res.setHeader('Content-Type', 'text/event-stream; charset=utf-8') | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. Shared read-state gets pruned
🐞 Bug✓ CorrectnessAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools