# ask-gpt account-bound conversation registry

audience: AI coding agents first.

Status: DONE
Priority: 1
Task IDs: #4
Worker: main session owns investigation/review; native implementation subagent follows pinned contract.

## Source request

2026-08-11 owner: investigate stale `--list`; make `--list-all` durable registry of every conversation initiated by `ask-gpt`; fetch web once now only to seed registry; later append locally; persist user account, conversation id, conversation title; future account changes MUST NOT cross-fetch; resume/download under wrong logged-in account MUST error and crash.

2026-08-11 correction: dispatch implementation using native subagent, NEVER `/ask-codex`.

2026-08-11 review correction: establish ChatGPT root before auth in fresh sessions and daemon bootstrap; never fetch target before identity match; preserve existing title during resume meta; canonicalize registry identity/id, validate ChatGPT UUID ids, reject terminal-corrupting control characters, enforce 0600 files; reject `--list N` when N <= 0; add focused regressions; leave changes uncommitted for main review.

2026-08-11 final corrections: poll fresh/resumed UI titles over a bounded interval; accept a title ONLY when current URL is HTTPS on exact `chatgpt.com` with exact `/c/<emitted-id>` path and hydrated turn evidence proves the target chat loaded. Arbitrary stable titles on wrong origins/routes/surfaces MUST fall back to prior resume title or fresh provisional title; NEVER regex-search an id anywhere in a URL or rely on an expanding interstitial denylist. Repair pre-existing JSONL/lock permissions on read/write. Map `registry.RegistryError` to deterministic daemon and CLI errors; registry metadata persistence failure MUST stop prompt continuation and emit a coherent terminal error event without traceback. Metadata conflict and final registry append failure MUST each emit exactly one terminal `error`, never `done`, and preserve partial turn state. Add malformed-registry daemon/direct, title-trust, and event-semantic regressions; no schema changes.

## Outcome

Replace sidebar cache with append-only account-bound JSONL registry. Listing becomes local-only and immediately reflects successful `ask-gpt` conversations. One explicit migration imports current web sidebar once for `chatgpt@alex.org.il`. Resume/download fail before target navigation when authenticated browser account differs from registry owner.

## Root cause

- `ask_gpt.py:_run_listing` serves `~/.overdeck/gptbridge/threads.json` indefinitely. Age >15m prints “refreshing” but refreshes only with `--fresh`; otherwise stale rows remain.
- `--list-all` bypasses cache and unconditionally calls `_scrape_threads`, which opens daemon/browser and scrolls full web sidebar.
- Existing thread rows carry no account. Resume/download navigate first and infer “not found in this account”; they cannot distinguish wrong login from missing conversation.

## Preserved WIP / ref / path

- Worktree: `/home/user/Projects/overdeck/.worktrees/ask-gpt-registry`
- Branch: `wt/ask-gpt-registry`
- Base: `origin/main` at `8d36d1468`
- Authoritative design: `docs/specs/2026-08-10-ask-gpt-resume-live-design.md` §4c
- Existing behavior under change: `modules/gptbridge/ask_gpt.py`, `chat.py`, `solwebd.py`, `tests/test_listing.py`, `tests/test_resume.py`, `tests/test_download_attachments.py`.

## Constraints

1. Implementer MUST be native Claude subagent. NEVER use `/ask-codex`.
2. Canonical registry: `~/.overdeck/gptbridge/conversations.jsonl`; lock: sibling `conversations.lock`.
3. Each JSONL record MUST contain `account`, `conversation_id`, `title`. Same conversation id MUST NEVER change account.
4. Registry append/read MUST be cross-process safe. Hold flock; append one complete line; flush + fsync. Reader MUST reject malformed non-empty lines and conflicting ownership. Latest record per id wins for title/order.
5. Account identity MUST come from authenticated ChatGPT session. Prefer page-context fetch of `/api/auth/session`; validate non-empty email. NEVER trust configured owner string/profile name.
6. New conversation MUST be durably registered when `meta` first exposes id, before reply completion; browser-owning direct/daemon process writes through shared registry flock so client disconnect/failure cannot lose an initiated conversation. Store authenticated account + deterministic prompt-derived provisional title, then append actual UI title after bounded settle. Resume MUST NOT downgrade existing actual title to provisional text.
7. Resume/download MUST lookup id locally first. Unknown id fails closed. Daemon MUST independently derive expected account from canonical registry; NEVER trust caller-supplied account as authority. Browser compares current authenticated account to registry account before target conversation navigation/fetch. Mismatch error names expected/current.
8. `--list`, `--search`, `--list-all` MUST NOT call daemon/browser/network. `--list-all` writes real `./ask-gpt/conversations.jsonl`, never prints records.
9. Add explicit `--bootstrap-registry`: sole sidebar-fetch path; refuse if canonical registry contains records; import every remote row under detected current account. Run exactly once after deploy for `chatgpt@alex.org.il`.
10. Preserve prompt output, resume/log/live/download semantics and global dispatch limiter. No remote listing compatibility layer, no automatic stale refresh, no symlink.

## Acceptance criteria

- Meta emission appends `{account, conversation_id, provisional title}` before completion; simulated reply failure/client disconnect leaves record durable. Successful completion updates it with actual UI title. Next `--list` includes it without web/daemon invocation.
- Concurrent writers produce valid complete JSONL and no lost entries.
- `--list` defaults latest 10; `--search` retains regex→glob behavior; both local-only.
- `--list-all` writes cwd JSONL copy and prints only its path; local-only.
- Bootstrap on empty registry imports full sidebar and current account; second bootstrap refuses without web call.
- Resume/download unknown id fail before browser target fetch.
- Resume/download account mismatch fail before target fetch, in direct and daemon paths.
- Correct-account resume/download remain functional.
- Fresh direct and daemon sessions navigate/wait at `https://chatgpt.com/` before auth/sidebar work; target conversation is never fetched before identity match.
- Resume meta never replaces an existing registry title with prompt-derived provisional text; final browser title still appends after completion.
- Registry trims and lowercases account/id, accepts only ChatGPT UUID-form conversation ids, rejects control characters in account/title/URL, and creates JSONL/lock as 0600 while retaining flock + append + flush + fsync.
- `--list N` rejects N <= 0 clearly without changing positive/default listing semantics.
- Registry failures during daemon lookup/metadata persistence return deterministic service errors; direct CLI prints a normal `ask-gpt:` error. Neither path continues the target/prompt nor emits a traceback; `ask_on` emits one coherent terminal error event.
- Full `python3 -m pytest modules/gptbridge/tests/ -q` clean; `health.sh` clean/justified.
- Installed `$PATH` runtime verified: bootstrap registry once, inspect account/id/title fields, prove `--list` local by operating with daemon unavailable or browser call denied.
- Landed and deployed.

## Execution steps

1. Pin design and registry/account seams.
2. Native subagent implements module + deterministic tests in worktree.
3. Main session reviews diff for account trust boundary, ordering, lock/append durability, daemon/direct parity, and remote-call absence.
4. Fix findings; run full gptbridge tests + health.
5. Land/deploy.
6. Run one-time bootstrap against current `chatgpt@alex.org.il`; verify canonical JSONL and local-only listing.

## Current receipt

2026-08-11 correction pass implemented uncommitted in preserved worktree. TDD covers root-before-auth/target ordering, resume-title preservation, bounded stable title settling, positive title trust (HTTPS exact `chatgpt.com` + exact `/c/<emitted-id>` + hydrated turn evidence), registry canonicalization/UUID/control-character/0600 create+repair, synchronized multiprocess no-loss/immutable ownership, positive list limits, daemon/direct registry error boundaries, and exactly-one terminal error with preserved partial state for metadata/final-append failures. Main/security/final reviews drove residual fixes: deterministic daemon 503, normal CLI `ask-gpt:` failure without traceback, no prompt/target continuation after registry failure, wrong-origin/query/trailing-slash URL rejection, and arbitrary stable Cloudflare-title fallback on wrong surfaces. Earlier relative pytest commands accidentally targeted sibling `ask-gpt-download-progress`; all such receipts are invalid and superseded by correct-worktree runs. Final correct-worktree receipts: canonical URL/title boundary 7 passed; focused registry/resume/download/concurrency/turn-event/routing suite 90 passed in 19.96s; `git diff --check` clean. Full-minus-Xvfb had one clean run at 243 passed, 1 skipped in 56.93s; two repetitions exposed unrelated pre-existing flakiness in `test_stopping_kills_the_whole_process_group` (242 passed, 1 skipped, 1 failed), and that test passed alone immediately. Complete suite: 244 passed, 1 skipped, 2 environment-only failures in 54.67s. Exact complete-suite failures: `test_a_second_display_starts_alongside_the_first` and `test_the_display_is_usable`, both `Xvfb never reported a display: (EE) Failed to find a socket to listen on(EE)`; third virtual-display test passes. Final independent review finding (non-canonical URL trust) is fixed and regression-covered. Main-session review passed the final registry, account, title-trust, daemon/CLI error-boundary, and ownership-navigation seams. Implementation committed as `282299de8`; verification receipt committed as `33dad023a`; landing recovery receipt committed as `8549c977c`. Current `origin/main` merged cleanly, and full suite rerun outside the tool sandbox passed 246 with 1 skipped in 46.26s; no Xvfb or process-group failures remained. Five controller invocations did not land: one correctly refused because cwd was inside the removable worktree; three owner processes were killed externally while serialized candidate typecheck ran; one background owner was killed after waiting 210 seconds behind an unrelated ticket. No registry gate emitted a red verdict. The latest dead registry ticket lock is acquirable and will be pruned by the next queue entrant; the unrelated ticket now has a verdict. Relaunch uses a transient user service so ticket ownership survives blocked-turn child reaping while still running the canonical controller.

Final receipt: landed and deployed as `b59a7f7f`. The canonical file initially contained two duplicated synthetic `owner@example.com` fixture rows with mode `0664`; preserved under the registry lock as `conversations.fixture-contamination-20260811T100346.jsonl`, then ran `--bootstrap-registry` exactly once. Bootstrap imported 64 unique conversations, all owned by `chatgpt@alex.org.il`; required fields are valid, target `6a79c500-04bc-83ec-99e6-24bb70fb8de1` is present, ownership has no conflicts, and registry/lock modes are `0600`. Installed `--list`, `--search '.*'`, and `--list-all` returned 3/64/64 rows; syscall traces contained no network calls and stderr was empty.

## Next executable action

None. Outcome is installed, bootstrapped, and owner-visible.
