Skip to content

Make session operations independent of transcript history - #76

Merged
ashwin-pc merged 3 commits into
mainfrom
fix/session-performance-contract
Jul 31, 2026
Merged

Make session operations independent of transcript history#76
ashwin-pc merged 3 commits into
mainfrom
fix/session-performance-contract

Conversation

@ashwin-pc

@ashwin-pc ashwin-pc commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • replace SessionManager.list() in the interactive list path with bounded shallow JSONL projections
  • resolve open/delete targets through deterministic filename scans instead of corpus-wide lists
  • preserve post-restart bookmarked-session discovery with a bounded directory/filename fallback that reads only the matching header
  • patch rename/delete rows from authoritative realtime events and remove redundant end-event refreshes
  • time out client list requests while preserving single-flight cleanup and one trailing refresh
  • omit exact message counts for cold sessions while retaining them for live sessions

The shallow lister reads at most a 32 KiB head and 8 KiB tail from each session, so list cost depends on directory entry count rather than accumulated transcript bytes.

Deliberate semantic deltas

  • modified is file mtime, so metadata writes such as rename/model changes can slightly affect ordering.
  • firstMessage has whitespace collapsed rather than preserving upstream raw whitespace.
  • A first user message beyond the 32 KiB head may be unavailable or found in the bounded tail. An entry straddling a read boundary is deliberately omitted.
  • Exact messageCount is omitted for cold sessions because calculating it requires parsing the transcript body; live sessions retain an exact count.

Closes #74

Validation

  • npm run typecheck
  • npm run build
  • npx vitest run tests/shallow-session-list.test.ts tests/session-list-lifecycle.test.ts tests/session-refresh-contract.test.ts tests/session-service.test.ts tests/api.test.ts (69 passed)
  • regression coverage now asserts transcript-inflation independence, bounded growing-corpus/cwd work, turn refresh fanout and row patches, and hung-request retry
  • PR CI passed
  • npm test was run until the 300-second harness timeout; completed suites passed, with unrelated retried/flaky artifact-preview and Git E2E cases and no session-related failures

Deploy follow-up

After merge, restart the supervised child on the long-running instance and re-measure /api/sessions TTFB and pinned-tab switching against #74 (warm list p95 <100 ms; trivial endpoints p95 <50 ms during refresh).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

for (const resolvedCwd of new Set([resolve(cwd), ...this.knownCwds()])) {

P1 Badge Search all workspace directories when resolving session IDs

After a server restart, a URL containing the ID of a session created in a non-global working directory returns 404 because knownCwds() initially contains only the global cwd and the URL carries no cwd. The removed SessionManager.listAll() fallback previously supported this documented bookmark/copied-link flow; the filename scan therefore also needs a bounded fallback across all workspace session directories rather than stopping after the process-local known set.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let sessionName: string | undefined;
let firstMessage: string | undefined;
for (const entry of entries) {
if (entry?.type === "session_info") sessionName = typeof entry.name === "string" && entry.name ? entry.name : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve renames that fall outside the bounded windows

For a cold session renamed after its first 32 KiB and followed by more than 8 KiB of transcript, the authoritative session_info entry is in the unread middle of the file. This loop consequently reports an older head-window name or no name at all, so a renamed session reverts in the drawer after restart; the latest name needs storage or indexing that remains accessible without parsing the transcript body.

Useful? React with 👍 / 👎.

@ashwin-pc

Copy link
Copy Markdown
Owner Author

Reviewed against the #74 contract — this is the right shape: Phases 1–2 with no worker and no cache, a net-simpler hot path, and CI is green. Requesting the following before merge; all four are small.

1. Narrow the findSessionInfoById return type (server/session/service.ts:~831)

The filename-suffix path returns { id, path, cwd } cast to the full SessionManager info type. Both current consumers are safe (cwdForSessionId reads .cwd, delete reads .path), but the cast lies about name/created/modified/messageCount. Please return an honest { id: string; path: string; cwd: string } (rename to findSessionLocationById if clearer) so a future caller can't read fields that were never populated.

2. Record the accepted semantic deltas in the PR description

These match the #74 contract but should be documented as deliberate:

  • modified is now file mtime — ordering can shift slightly (e.g. a rename/model-change bumps a session).
  • firstMessage is whitespace-collapsed vs. upstream's raw text.
  • A first user message beyond the 32 KiB head window falls back to whatever the tail sees (measured p90 offset is ~1 KB, so rare).

3. Comment the head/tail boundary loss (server/session/shallowList.ts boundedContents)

An entry straddling the 32 KiB head cut is silently dropped — including the contiguous 32–40 KiB case, where the tail's "discard first partial line" throws away the continuation of the head's last line. parseLines tolerates it, but it deserves a one-line comment so nobody "fixes" the contiguous case into double-parsing, and so a rename lost at the boundary is a known trade-off.

4. Don't let Closes #74 drop the contract's regression net

The acceptance list in #74 includes tests this PR doesn't add:

  • corpus-independence: list latency unchanged when transcript bodies are inflated 10× (the new unit test asserts correctness, not cost);
  • turn fanout: a basic turn produces no full-transcript parse and ≤1 shallow refresh; zero on rename/delete;
  • hung-first-request retry e2e (the Prevent a hung session-list request from freezing future refreshes #64 scenario — the 15 s abort path is currently untested);
  • uptime soak: budgets hold as corpus + visited-cwd set grow.

Either add them here or split them into a follow-up issue and reference it from #74 before this auto-closes — otherwise the budgets exist only in prose.

Also worth a deploy note: after merge, restart the supervised child on the long-running instance and re-measure /api/sessions TTFB and pinned-tab switching against the #74 budgets (p95 < 100 ms warm list; trivial endpoints < 50 ms during refresh).

@ashwin-pc

Copy link
Copy Markdown
Owner Author

Addressed in 18cf576 and the updated PR description:

  1. Renamed/narrowed the lookup to findSessionLocationById, returning only { id, path, cwd } with no cast to full session metadata.
  2. Documented the accepted modified, firstMessage, boundary, and cold messageCount semantic deltas.
  3. Added an explicit comment describing deliberate head/tail boundary entry loss.
  4. Opened Session performance regression and soak harness #77 for the remaining corpus benchmark, fanout instrumentation, timeout retry E2E, and uptime soak, and linked it from Session performance contract: constant-time opens, shallow reconciliation, and isolated history work #74 before this PR closes it.

Also addressed the Codex review: unknown bookmarked session IDs now fall back to scanning workspace directory names and filename suffixes, then perform one bounded read of the matching header to recover/validate cwd. This restores post-restart copied-link discovery without parsing transcript bodies.

Validation after the changes: typecheck passed; 65 focused tests passed.

@ashwin-pc

Copy link
Copy Markdown
Owner Author

Correction to my previous response: the requested regression coverage is now included in this PR at 6040de7, rather than deferred. The accidentally opened #77 was immediately closed, and its reference was removed from #74 and this PR. Added coverage proves bounded bytes under 10× transcript inflation, growing corpus/cwd bounded work, turn fanout plus rename/delete row patching, and hung-request abort followed by successful retry. Typecheck, build, and all 69 focused tests pass.

@ashwin-pc

Copy link
Copy Markdown
Owner Author

Re-reviewed at 6040de7. All four requested items are resolved, and CI is green:

  1. findSessionLocationById now returns an honest { id, path, cwd } — no cast, both call sites safe by construction.
  2. ✅ Semantic deltas recorded in the description.
  3. ✅ Boundary-loss comment in boundedContents explicitly covers the contiguous 32–40 KiB case.
  4. ✅ Regression net landed in-PR: bytes-read invariance under 10× transcript inflation (≤40 KiB/file), bounded work across a growing corpus/cwd sweep (200 sessions/20 cwds), turn-fanout contract (only message_end refreshes), and the Prevent a hung session-list request from freezing future refreshes #64 hung-request abort + trailing-retry test.

Also good catch on the regression my first pass missed: dropping listAll() would have broken bookmarked sessions in cwds never visited by this process. The new fallback in resolveSessionLocation restores that discovery boundedly — a directory-name sweep + filename-suffix match + a single bounded header read, validated against defaultSessionDir(headerCwd) before trusting the file. Worst case is O(session dirs) readdirs with zero transcript reads, which preserves the #74 contract.

One optional nit, non-blocking: the source-text assertions in session-refresh-contract.test.ts are brittle to refactors (they grep realtime.ts). They're fine as a contract lock for now; a behavioral harness can replace them later.

Merge-ready from my side. Post-merge on the long-running instance: POST /api/restart, then record /api/sessions TTFB and pinned-tab switch latency against the #74 budgets (list p95 < 100 ms warm; trivial endpoints < 50 ms during refresh) to close the loop with real data.

@ashwin-pc
ashwin-pc merged commit fdec3f7 into main Jul 31, 2026
1 check passed
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.

Session performance contract: constant-time opens, shallow reconciliation, and isolated history work

1 participant