Skip to content

Import Codex cloud chats and record their token usage - #2261

Open
cpcloud wants to merge 301 commits into
mainfrom
codex-full-trace-analysis
Open

cpcloud wants to merge 301 commits into
mainfrom
codex-full-trace-analysis

Conversation

@cpcloud

@cpcloud cpcloud commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

ChatGPT dot, Codex web, and Codex cloud task chats live only on OpenAI's servers, so AgentsView never showed them or their cost. A new opt-in codex-cloud source imports them with a local Codex home's ChatGPT login, listens in on running chats to record token usage, and uses OpenAI's plan usage report for chats it did not hear.

Users get these chats as searchable sessions, with their cost in usage totals where AgentsView has token counts.

How

  • Import. The sync job lists changed chats through OpenAI's Codex chat service, stores a compressed copy under sources/codex-cloud/, and parses it like any other provider.
  • Live tokens. OpenAI reports token counts only to a client connected to a running chat. Every 30 seconds the daemon connects to chats updated in the last 5 minutes whose turn is still running, up to 10 per account, and records each token update. It is off unless live_tokens = true.
  • Plan usage. A chat with no token counts gets OpenAI's figures instead: its share of the weekly and five-hour limits, plus credits. AgentsView keeps asking until 7 days after the chat's last change.
  • Storage. Each backend gets a session_plan_usage table. The DuckDB mirror rebuilds once. Session export moves to schema 7, where model_usage is null when token counts are unknown. Artifact folder sync writes plan usage and capture coverage into a v7 manifest, only for sessions that have them.
  • Status. The status bar and agentsview serve status show each account's import, live tokens, and plan usage state, with a next step for every failure.

Fragile parts and tradeoffs

  • Private endpoints. OpenAI documents none of the three: the chat service, the thread/resume request, and the usage report. When OpenAI changes a reply shape, import or capture for that account stops with "OpenAI's service answered in a way this version of AgentsView can't read". Imported sessions stay. Expect a follow-up release each time OpenAI changes one.
  • Listening in shows on OpenAI's side, and can start a turn. While connected, the task reports AgentsView as its client. Connecting just after a turn ends loads the stopped chat, starts its MCP servers, and can move the chat to the top of the user's list. Open-source Codex also runs the chat's idle step on every resume, which continues an active goal and sends queued messages. AgentsView reads the goal first and skips a chat whose goal is active. It cannot see queued messages, so a chat whose turn the user stopped with messages queued can still send them. That remaining case is why live capture is opt-in. Nobody has tested whether an approval prompt shown while AgentsView is connected comes back afterwards. While AgentsView listens in, a detached daemon does not exit on idle, for up to 6 hours per chat.
  • Token counts are incomplete by design. Only turns heard live count. The 5-minute window depends on the local clock, the 10-connection cap can miss turns, and AgentsView does not capture running sub-agent chats. One update can cover several model calls, and AgentsView prices all of them at base rates, which can understate the cost of long-context calls. A counter restart AgentsView does not notice also undercounts. Sessions with missed turns stay in totals with a "partly captured" note. Totals leave out sessions with no counts and report them as plan usage.
  • Plan usage is OpenAI's measure, not tokens. AgentsView shows credits as OpenAI sends them. Figures can change for 7 days after a chat's last change, and longer while the account is paused.
  • Logins. AgentsView reads auth.json only. A home that keeps its login in the system keyring needs cli_auth_credentials_store = "file", which leaves the token in a plain file, and the status names that cost when it suggests the setting. AgentsView accepts endpoint overrides only for OpenAI hosts and loopback, and neither client follows redirects.
  • One machine only. Every machine gives a chat the same session ID. PostgreSQL keeps the first machine's copy, ClickHouse keeps the last push, and a hosted hub can count a chat twice.
  • Unsaved token records. When saving keeps failing, token updates wait in memory, compacted to about three per turn, and a restart loses them. The status line warns while any are waiting.

Reviewer notes

  • Where to look. Live capture and the in-memory queue live in internal/codexcloud/monitor.go. Plan usage scheduling and settling live in usage_step.go. Import and status text live in pass.go and status.go. The parser is internal/parser/codex_cloud*.go. The rules that keep a saved chat after its mirror file is lost live in internal/ingest/codex_cloud_history.go, shared by sync and the hosted hub.
  • Upgrade steps. A least-privilege PostgreSQL serve role needs SELECT on session_plan_usage. Consumers of agentsview export sessions must accept a null model_usage. Artifact importers must be upgraded to read v7 manifests; older ones hold them.
  • Design reference. docs/internal/codex-cloud-source.md explains how sync, live capture, and plan usage work, what was rejected, and which probes are still open.
  • Shared zstd frames. The mirror and the DeepSeek harness reader now share frame handling in internal/zstdframe, so DeepSeek parsing goes through new code.

🤖 Generated with Claude Code

@roborev-ci

roborev-ci Bot commented Oct 14, 2026

Copy link
Copy Markdown

roborev: Combined Review (3666802)

Verdict: Changes require fixes for 11 findings.

High

  • internal/codexcloud/monitor_test.go:819: TestLeavingWithAQueuedUnreadableNoteStopsCapture injects invalid JSON directly into the notification queue, bypassing Conn.readLoop's enforced validation boundary at client.go:266–282.

    Fix: Use syntactically valid JSON with an invalid token counter, such as inputTokens: 1.5, to exercise a reachable queued notification that stops capture.

  • internal/codexcloud/state_test.go:94: TestStateAtomicWrite creates an unrelated temporary file between successful writes but never interrupts or fails a write. It would also pass with direct os.WriteFile calls, providing false confidence in atomicity.

    Fix: Remove TestStateAtomicWrite; atomicity belongs to the imported atomicfile implementation.

  • internal/codexcloud/pass_test.go:406: TestFullResyncKeepsStateAndFetchesNothing runs Syncer.Run twice without performing a full resync, so it cannot detect Engine.ResyncAll discarding fetcher state. Its unchanged-pass behavior is already covered by TestCardinalityScaling.

    Fix: Invoke the actual archive full resync between fetcher passes, then verify that state survives and history requests are not repeated.

  • internal/zstdframe/zstdframe_test.go:73: TestDecoderRoundTrip exercises tiny frames through third-party codec wrappers without testing repository-owned resource limits or frame-validation policy. Real mirror operations already cover the codec wiring.

    Fix: Remove TestDecoderRoundTrip and its unused io import; retain the Inspect tests for repository-owned frame handling.

  • internal/service/plan_usage_parity_pgtest_test.go:158: TestPlanUsageCountsParity includes SQLite in the comparison stores even though SQLite supplies the expected results, causing both comparison loops to compare that store with itself.

    Fix: Remove the sqlite entry from stores while retaining independent SQLite count expectations and comparisons against DuckDB, PostgreSQL, and ClickHouse.

Medium

  • internal/codexcloud/monitor.go:704; internal/codexcloud/monitor.go:712: Live discovery, joining, protocol-error, and notification-decoding paths can pass unredacted errors to logging or stopCapture. Errors that echo the access token can persist credentials in debug.log. Reviewers identified monitor.go:704 and monitor.go:712, plus discovery.go's direct RPC-error log.

    Fix: Redact errors with the connection's login token before every live-path logging or stopCapture call, including discovery.go. Extend token canary tests to cover echoed RPC errors and malformed notifications.

  • internal/codexcloud/pass.go:565: After a thread's first history fetch fails, retryLater commits the original ThreadState with CreatedAt still zero, discarding the creation time populated by fetchThread. Subsequent plan usage requests use 1970-01-01, potentially indefinitely for oversized threads.

    Fix: Preserve CreatedAt from the listing when committing metadata after a failed fetch without advancing the history cursor, and test the resulting plan usage request date.

  • internal/db/plan_usage_counts.go:111: Plan-only counts require a session_plan_usage row. Tokenless imported chats whose initial usage requests fail have no stored usage outcome and disappear from omitted-session counts and per-model markers during backoff across all four backends.

    Fix: Include tokenless Codex Cloud sessions before their first stored usage outcome while preserving existing filters and backend parity.

  • internal/codexcloud/pass.go:114: A monitor-triggered authentication wait can exist without a home status row. Sync skips the waiting home, and Keep removes its account row, causing the configured source to disappear from status for up to an hour without sign-in guidance.

    Fix: Read the wait reason with HomeWait and create the corresponding status row whenever sync skips a waiting home. Cover a monitor-triggered wait followed by a sync pass.

  • internal/parser/codex_cloud.go:267: Resume-only mirrors created by live capture ignore token observation timestamps when deriving ended_at. Without item history, active chats retain their creation time as the end time, producing incorrect activity filters, ordering, and exported duration while import is delayed or failing.

    Fix: Use the newest token observation time when deriving the end time of a resume-only mirror, and add a parser test with resume metadata and live token records but no items.

  • internal/codexcloud/pass.go:184: For a daemon using a custom CODEX_HOME, the primary home's recovery command remains bare codex login. Running it in a shell without the daemon's environment signs into ~/.codex instead, leaving the configured home unusable.

    Fix: Include the target CODEX_HOME in recovery commands for the primary home as well, so the displayed command works independently of the shell environment.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 38s | Total: 37m50s

@mariusvniekerk

Copy link
Copy Markdown
Collaborator

fancy!

@cpcloud
cpcloud force-pushed the codex-full-trace-analysis branch from 3666802 to 0a4ed41 Compare October 14, 2026 18:38
@roborev-ci

roborev-ci Bot commented Oct 14, 2026

Copy link
Copy Markdown

roborev: Combined Review (0a4ed41)

Verdict: Changes require fixes for 12 findings.

High

  • internal/codexcloud/pass.go:565: If an imported chat's mirror is deleted or truncated and history refetch fails, the retry path recreates a metadata-only mirror. WithForceReplace then deletes archived messages and usage, potentially permanently if subsequent fetches keep failing.

    Fix: Preserve the archived transcript while rebuilding a lost mirror. Publish a replacement only after complete history recovery, while allowing metadata-only first imports.

  • internal/codexcloud/job_test.go:338: TestTokenCanary searches compressed .jsonl.zstd bytes for plaintext secrets, so it can miss credentials present in decoded mirror records.

    Fix: Decompress mirror files before searching their JSON contents. Retain the plaintext state-file, log, and status checks.

  • internal/codexcloud/monitor_test.go:819: TestLeavingWithAQueuedUnreadableNoteStopsCapture injects incomplete JSON that Conn.readLoop rejects before enqueueing Params. The test does not exercise a notification that can reach Monitor.leave in production.

    Fix: Use valid JSON with an invalid field type, such as {"threadId":123}, to exercise a reachable queued-notification decoding failure.

  • internal/codexcloud/listing_test.go:222: TestActiveUpdateFoundWhileArchivedIsNewer supplies separate watermarks directly and uses single-page listings. It still passes if production incorrectly shares the newer archived watermark.

    Fix: Exercise Syncer.Run with the changed active thread on a later listing page, so using the archived watermark would hide it.

  • internal/codexcloud/pass_test.go:406: TestFullResyncKeepsStateAndFetchesNothing calls Syncer.Run twice without performing a full resync. It duplicates unchanged-pass coverage and would pass if Engine.ResyncAll discarded fetcher state.

    Fix: Delete this redundant test and retain TestCardinalityScaling for unchanged-pass request behavior.

  • internal/codexcloud/state_test.go:99: TestStateAtomicWrite creates an unrelated temporary file between successful saves without interrupting a write. All assertions would pass with a non-atomic os.WriteFile implementation.

    Fix: Delete TestStateAtomicWrite; the fabricated sibling file does not test the atomicity supplied by the atomicfile dependency.

  • internal/zstdframe/zstdframe_test.go:73: TestDecoderRoundTrip checks that the dependency decodes frames produced by its encoder, but does not verify the repository's concurrency, memory, or window policy. Removing those NewDecoder options would leave it passing.

    Fix: Delete TestDecoderRoundTrip and its unused io import. Keep the frame-inspection tests that exercise repository-owned parsing.

  • internal/service/plan_usage_parity_pgtest_test.go:158: TestPlanUsageCountsParity includes the baseline SQLite store among comparison targets, causing both comparison loops to compare SQLite against itself without adding coverage.

    Fix: Remove the sqlite entry from stores. Retain the independent baseline assertions and comparisons against DuckDB, PostgreSQL, and ClickHouse.

Medium

  • cmd/agentsview/codex_cloud_jobs.go:107: The daemon work lease ends when Discover returns, while joined chats continue capturing in separate goroutines. With no external requests, the default 20-minute idle timeout can close an active capture and lose remaining tokens and completion.

    Fix: Hold a daemon work lease for each joined connection until capture ends, and cover idle shutdown during an active join.

  • internal/codexcloud/pass.go:564-565: After an initial history-fetch failure, the metadata retry path queues plan usage without persisting the listing's creation time in ThreadState.CreatedAt. Usage requests therefore send 1970-01-01T00:00:00Z until history fetching succeeds.

    Fix: Persist the listing's creation time before queuing plan usage on the metadata-only retry path, and test the outgoing request timestamp.

  • internal/codexcloud/pass.go:114: When live capture records a login wait after a refresh request or HTTP 401, the skipped home receives no status row. Status.Keep(shown) then removes its account row, hiding the login problem and recovery instructions while import and capture are paused.

    Fix: Read the wait reason with HomeWait and create or refresh the home's error row before skipping it.

  • internal/db/plan_usage_counts.go:111: With live capture disabled and the initial plan-usage request failing or receiving 401, imported chats have neither usage events nor a session_plan_usage row. They disappear from omitted-session counts and model totals, with no unsupported-usage warning explaining the zero totals.

    Fix: Include uncaptured Codex Cloud chats without plan rows in omitted-session reporting, or expose a separate unknown-usage warning. Apply consistent behavior across all backends.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 27s | Total: 37m50s

@roborev-ci

roborev-ci Bot commented Oct 14, 2026

Copy link
Copy Markdown

roborev: Combined Review (531f536)

Verdict: Changes require fixes for 15 findings.

High

  • internal/codexcloud/pass.go:565: When an archived mirror disappears or is truncated and history recovery fails, a metadata-only replacement is published. ForceReplace then overwrites the archived transcript and usage with incomplete data; persistent fetch failures can make the loss permanent.

    Fix: Preserve archived messages and usage during mirror recovery. Publish replacements only after complete history recovery, or mark incomplete recovery files so ingestion preserves archived content. Continue allowing metadata-only imports for previously unseen chats.

  • internal/codexcloud/job_test.go:335; internal/codexcloud/job_test.go:338: TestTokenCanary searches compressed mirror bytes for plaintext credentials. Compression can hide secrets persisted in decoded records, so the test does not verify its credential-persistence guarantee.

    Fix: Decode mirror files with the repository's mirror reader and scan decoded records for every canary secret. Retain plaintext checks for state files, logs, and status.

  • internal/codexcloud/monitor_test.go:819: TestLeavingWithAQueuedUnreadableNoteStopsCapture injects malformed JSON directly into the notification queue, bypassing Conn.readLoop, which rejects malformed JSON before enqueueing it.

    Fix: Use valid JSON with an invalid field type, such as {"threadId":123}, to exercise a reachable queued-notification decoding failure.

  • internal/codexcloud/listing_test.go:222: TestActiveUpdateFoundWhileArchivedIsNewer supplies separate watermarks instead of exercising production watermark selection. Both listings fit on one page, so the test passes even if production incorrectly shares the archived watermark.

    Fix: Exercise Syncer.Run across passes with the changed active thread on a later listing page, where an incorrectly shared archived watermark would hide it.

  • internal/codexcloud/pass_test.go:406: TestFullResyncKeepsStateAndFetchesNothing calls Syncer.Run twice without performing a full resync. It duplicates unchanged-pass coverage and would pass if Engine.ResyncAll discarded fetcher state.

    Fix: Delete the redundant test and retain TestCardinalityScaling.

  • internal/codexcloud/state_test.go:99: TestStateAtomicWrite creates an unrelated sibling file between successful saves without interrupting a write. Its assertions would pass with non-atomic os.WriteFile.

    Fix: Delete TestStateAtomicWrite because the fabricated sibling file does not test atomicity.

  • internal/codexcloud/state_test.go:20: TestStateRoundTrip saves fully populated values and reads them through the same JSON types, without an independent format fixture or recovery, isolation, or initialization contract. Changing field names on both sides still passes.

    Fix: Delete TestStateRoundTrip and retain focused tests for corruption recovery, missing-state initialization, permissions, and unsafe IDs.

  • internal/zstdframe/zstdframe_test.go:73: TestDecoderRoundTrip verifies that the compression dependency decodes its own encoder's output. It does not exercise repository-owned decoder memory, window, or concurrency policy and passes if those options are removed.

    Fix: Delete TestDecoderRoundTrip and its unused io import. Retain the frame-inspection tests that exercise repository-owned parsing.

  • internal/service/plan_usage_parity_pgtest_test.go:158: TestPlanUsageCountsParity includes the baseline SQLite store among comparison targets, causing both comparison loops to compare SQLite against itself without independent failure detection.

    Fix: Remove the sqlite entry from stores. Retain independent baseline assertions and comparisons against DuckDB, PostgreSQL, and ClickHouse.

Medium

  • cmd/agentsview/codex_cloud_jobs.go:107: The daemon work lease ends when Discover returns, while joined captures continue in separate goroutines. The default 20-minute idle timeout can shut down an active capture, disconnecting it before remaining token updates and completion arrive.

    Fix: Hold a daemon work lease for each joined capture until its monitor finishes, releasing it on every exit path. Cover idle shutdown during an active join.

  • internal/codexcloud/pass.go:114: A recorded login wait causes the home to be skipped without creating an error status row. Status.Keep can then remove the account row, hiding the login failure and recovery instructions while imports are paused.

    Fix: Read the recorded reason through HomeWait and create or update the home's error status row before skipping it.

  • internal/codexcloud/pass.go:569: After an initial history-fetch failure, the metadata-only fallback queues plan usage without initializing ThreadState.CreatedAt from the listing. UsageGroupFor consequently sends created_at as 1970-01-01T00:00:00Z.

    Fix: Persist the listing's creation time before committing metadata and scheduling plan usage. Avoid requesting plan usage when creation time is unknown, and test the outgoing timestamp after a history-fetch failure.

  • internal/db/plan_usage_counts.go:111: With live capture disabled and the first plan usage request failing or receiving 401, imported Codex Cloud chats have no session_plan_usage row. Inner joins exclude them from omitted-session counts and model totals, allowing reports to show zero totals without explaining unknown usage. Mirror backends have the same gap.

    Fix: Expose an unknown-usage count or warning for uncaptured Codex Cloud sessions without plan rows, consistently across backends and report surfaces.

  • internal/db/session_batch.go:570: Artifact folder sync does not export or import the new plan-usage row. Imported Codex Cloud sessions consequently lose plan figures, capture coverage, and counted-under references.

    Fix: Include plan usage and capture coverage in the bounded artifact loader, versioned manifest, and import translation. Remap counted-under IDs to the imported origin and cover the artifact round trip.

  • frontend/src/lib/utils/planUsage.ts:154: A final left_out row without figures displays 'not reported yet' because outcomeMessage checks missing figures before the final flag. The UI implies a pending report even though retries have stopped.

    Fix: Handle final left-out rows without figures as settled while preserving other explicit outcome explanations. Add coverage for final=true with outcome_kind=left_out and no figures.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 41s | Total: 26m17s

@cpcloud
cpcloud force-pushed the codex-full-trace-analysis branch from 531f536 to d86e574 Compare October 14, 2026 21:44
@roborev-ci

roborev-ci Bot commented Oct 14, 2026

Copy link
Copy Markdown

roborev: Combined Review (d86e574)

Verdict: Changes require fixes for 8 findings.

High

  • internal/codexcloud/monitor_test.go:819: TestLeavingWithAQueuedUnreadableNoteStopsCapture queues syntactically invalid JSON, but Conn.readLoop validates complete messages before enqueueing notifications, making this test input unreachable in production.

    Fix: Use valid JSON with an invalid payload shape, such as {"threadId":1}, to exercise a reachable queued-notification decoding failure.

  • internal/codexcloud/job_test.go:335: TestTokenCanary searches compressed .jsonl.zstd bytes for plaintext secrets, allowing credentials persisted in compressed mirror records to evade detection.

    Fix: Decode mirror files with mirror.ReadAll and scan their decoded records. Retain plaintext checks for state files, logs, and status output.

  • internal/codexcloud/pass_test.go:406: TestFullResyncKeepsStateAndFetchesNothing never performs a full resync; it calls Syncer.Run twice. Its unchanged-pass behavior is already covered by TestCardinalityScaling, whose zero turn reads also prevent item reads.

    Fix: Remove this duplicate test because it adds no full-resync failure detection.

  • internal/codexcloud/pass_test.go:277: TestOversizedTurnListingUnderFullFailsAtOnce forces turnView to Full, although production initializes it to NotLoaded and never changes it. The fake embeds large item bodies that the production request excludes.

    Fix: Remove the unsupported-view case or return an oversized turn-list response while preserving the actual NotLoaded request.

  • internal/zstdframe/zstdframe_test.go:73: TestDecoderRoundTrip only checks klauspost/zstd encoding and decoding through thin wrappers. It does not exercise repository frame inspection, checksum requirements, or decoder resource limits, so removing those policies would leave it passing.

    Fix: Delete TestDecoderRoundTrip and its now-unused io import. Keep the frame-inspection and mirror-behavior tests.

  • internal/service/plan_usage_parity_pgtest_test.go:158: TestPlanUsageCountsParity includes the baseline SQLite store in stores, causing both comparison loops to recompute expected results with the same store and production function. These self-confirming cases cannot detect incorrect SQLite results.

    Fix: Exclude SQLite from stores. Keep independent SQLite count expectations and comparisons against the other backends.

Medium

  • internal/codexcloud/pass.go:565: When a newly discovered chat's first history fetch fails with a per-thread error, retryLater commits metadata with CreatedAt still zero, discarding the creation time parsed in fetchThreadPaged. linkUsage then schedules the root, causing UsageGroupFor to send created_at="1970-01-01T00:00:00Z" to query_v2. This incorrect metadata persists if the chat retires without a successful history fetch.

    Fix: Persist CreatedAt from the listing when committing metadata-only retries and retirement, independently of history progress. Add coverage asserting the outgoing usage timestamp after a failed first history fetch.

  • internal/codexcloud/monitor.go:614: When mirror writes fail for more than 256 token updates, compaction can merge captured requests into an aggregate increase. The parser classifies that increase as session usage, bypassing request pricing bands and permanently underpricing long-context requests after saving recovers.

    Fix: Preserve the request information needed for pricing bands during compaction. Add a regression comparing priced costs before and after compaction.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 21s | Total: 22m7s

@roborev-ci

roborev-ci Bot commented Oct 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (a11e5d5)

Verdict: Changes require fixes for 12 findings.

High

  • internal/codexcloud/job_test.go:337: TestTokenCanary searches compressed mirror bytes for plaintext credentials, allowing credentials persisted inside compressed records to evade detection.

    Fix: Decode mirror files with mirror.ReadAll and inspect decoded records. Retain plaintext checks for state files, logs, and status output.

  • internal/codexcloud/monitor_test.go:819: TestLeavingWithAQueuedUnreadableNoteStopsCapture injects invalid JSON directly into the notification queue, but Conn.readLoop validates JSON before enqueueing notifications, making this state unreachable in production.

    Fix: Use valid JSON with an invalid payload shape, such as {"threadId":1}, to exercise a reachable queued-notification decoding failure.

  • internal/codexcloud/pass_test.go:277: TestOversizedTurnListingUnderFullFailsAtOnce forces turnView to Full, while production initializes it to NotLoaded and never changes it. The fake includes large item bodies that production requests exclude.

    Fix: Remove the unsupported-view case or simulate an oversized turn-list response while retaining the production NotLoaded request.

  • internal/codexcloud/listing_test.go:222: TestActiveUpdateFoundWhileArchivedIsNewer supplies independent watermarks instead of exercising their selection or persistence in pass.threads. Both listings fit on one page, so the test would pass even if active listings incorrectly used the archived watermark.

    Fix: Exercise the sync pass with different listing watermarks and put the active update on a later page where the archived watermark would hide it.

  • internal/codexcloud/state_test.go:99: TestStateAtomicWrite creates an unrelated temporary file and performs successful reads and writes without interrupting the write path. A non-atomic overwrite would still pass.

    Fix: Remove TestStateAtomicWrite; atomic staging and replacement belong to the atomicfile dependency, and this fixture tests no additional repository-owned contract.

  • internal/zstdframe/zstdframe_test.go:73: TestDecoderRoundTrip only verifies klauspost/zstd encoding and decoding through thin wrappers. Removing repository decoder resource limits or encoder checksum policy would leave it passing.

    Fix: Delete TestDecoderRoundTrip and its unused io import. Retain frame-inspection and mirror-behavior tests.

  • internal/codexcloud/pass_test.go:406: TestFullResyncKeepsStateAndFetchesNothing calls Syncer.Run twice without performing a full resync. Its unchanged-pass behavior is already covered by TestCardinalityScaling, including prevention of turn and item reads.

    Fix: Remove the duplicate test because it adds no full-resync failure detection.

  • internal/service/plan_usage_parity_pgtest_test.go:158: TestPlanUsageCountsParity includes the baseline SQLite store in stores, causing both comparison loops to compute expected and actual results with the same store and production function. These SQLite cases cannot detect incorrect results.

    Fix: Exclude SQLite from stores. Retain independent SQLite count expectations and comparisons against other backends.

Medium

  • internal/codexcloud/monitor.go:619: When failed saves accumulate more than 256 token updates, compaction merges individual requests into aggregate increases. The parser assigns source "session", bypassing request pricing bands and permanently underpricing captured long-context calls after saving recovers.

    Fix: Preserve request pricing information during compaction and add a regression comparing priced costs before and after compaction.

  • internal/codexcloud/pass.go:114: When the live monitor requests a login wait after a successful sync, the empty branch creates no home status row. If this is the only configured home, Status.Keep removes the account row, hiding Codex Cloud while import and capture remain paused for up to an hour.

    Fix: Read the recorded HomeWait failure and create a home error row with login recovery instructions.

  • internal/db/plan_usage_counts.go:111: Imported chats awaiting their first plan report have no session_plan_usage row. Starting the count from that table excludes them, so failed, rate-limited, or deferred initial requests silently omit tokenless chats from the missing-usage count and UI/CLI warning.

    Fix: Include tokenless codex-cloud sessions before a plan row exists, preserving report filters across all backends.

  • internal/codexcloud/pass.go:565: When a newly discovered chat's first history fetch fails, retryLater commits its listing with CreatedAt still zero because the creation time parsed inside fetchThreadPaged is discarded with the failed result. linkUsage then schedules a plan request with created_at="1970-01-01T00:00:00Z", which persists if history never imports successfully.

    Fix: Persist CreatedAt from listing metadata before scheduling plan usage, including metadata-only retry and retirement paths.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 20s | Total: 19m11s

@roborev-ci

This comment has been minimized.

@roborev-ci

roborev-ci Bot commented Oct 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (ef0845f)

Verdict: Changes require fixes for 6 findings.

High

  • internal/parser/codex_cloud_provider.go:94: When an imported chat's mirror is lost and history refetch fails, retryLater creates a metadata-only mirror. WithForceReplace then replaces archived messages and usage events with empty content. Even a successful history refetch cannot restore previously captured token records.

    Fix: Treat recreated or incomplete mirrors as incomplete sources and preserve archived messages and captured usage during recovery. Add coverage for mirror loss followed by a failed history fetch and archive sync.

  • internal/codexcloud/monitor_test.go:294: TestServerRequestsAreNeverAnswered duplicates the client policy test TestServerRequestsNeverAnswered. Conn.readLoop consumes approval requests before Monitor receives them, so the joined-thread setup adds no distinct request-handling coverage.

    Fix: Delete TestServerRequestsAreNeverAnswered while retaining the client policy test and existing monitor lifecycle tests.

  • internal/parser/codex_cloud_usage_test.go:433: The nil-token case in TestCoverageLastCaptureIsNewestTokenRecord exercises a helper state unreachable through production: applyUsage returns before calling codexCloudCoverage when there are no token records. Existing coverage already checks the observable behavior.

    Fix: Remove the nil-token call and its two assertions; retain the newest-capture-time checks.

Medium

  • internal/parser/codex_cloud.go:103: Delegated dot tasks receive a subagent relationship even though their orchestrator parent is never imported, excluding them from ordinary session lists and exports. Using --include-children still excludes them because those callers leave IncludeOrphans false.

    Fix: Promote missing-parent tasks to roots in list and export selection while preserving parent provenance. Cover both default and --include-children queries.

  • internal/codexcloud/fetch.go:452: When a mirror disappears during live capture, a monitor flush can recreate it while retaining ResumeTurnID/ResumeItemID and saving the new file's Offset. The next sync's mirrorLost check then passes, so fetching resumes after the old anchor and can permanently omit earlier history and thread metadata.

    Fix: Persist a rebuild requirement whenever a writer discovers a missing or shortened mirror. Clear it only after sync restores metadata and fetches history from the beginning.

  • internal/codexcloud/monitor.go:653; internal/codexcloud/monitor.go:642: After failed saves accumulate more than 256 updates, compaction retains only the peak request and its predecessor while merging other long-context requests within a turn. codexCloudUsageEvents classifies merged increases as session usage and applies base rates, permanently understating cost.

    Fix: Preserve every captured request's pricing information during compaction, either through request boundaries or an equivalent representation. Add a regression comparing total priced cost before and after compaction with several requests above the long-context threshold.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 23s | Total: 27m21s

@roborev-ci

roborev-ci Bot commented Oct 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (f661d2a)

Verdict: Changes require fixes for 8 findings.

High

  • internal/parser/codex_cloud_provider.go:94: When an imported mirror disappears and history refetch fails, retryLater creates a metadata-only mirror. WithForceReplace then replaces archived messages and usage events with empty results. Previously captured token records cannot be restored from stored history.

    Fix: Track incomplete mirror recovery and preserve archived messages and captured usage. Require complete history before replacing stored content, and cover mirror loss followed by failed refetch and archive sync.

  • internal/codexcloud/monitor_test.go:294: TestServerRequestsAreNeverAnswered duplicates the client policy test. Conn.readLoop consumes approval requests before Monitor receives them, so joining a thread adds no distinct request-handling coverage.

    Fix: Delete TestServerRequestsAreNeverAnswered; retain the client policy test and monitor lifecycle tests.

  • internal/parser/codex_cloud_usage_test.go:433: The nil-token case in TestCoverageLastCaptureIsNewestTokenRecord bypasses a production precondition: applyUsage returns before calling codexCloudCoverage when no token records exist. An existing test already checks the observable behavior.

    Fix: Remove the nil-token call and its two assertions; retain the newest-capture-time checks.

  • internal/codexcloud/state_test.go:20: TestStateRoundTrip checks ordinary JSON serialization through thin persistence wrappers. Fully populated collections bypass allocation and recovery logic, and matching incompatible changes to persisted field names would still pass.

    Fix: Delete TestStateRoundTrip and its now-unused jsontext import; retain the state recovery, permissions, locking, and path-validation tests.

  • internal/parser/codex_cloud_plan_usage_test.go:170: TestOutcomesSurviveReparse repeats the identical parse and assertion without changing input or state. Because each parse constructs fresh state, the second assertion adds no distinct failure detection.

    Fix: Remove only the second parse assertion; retain the outcome-selection cases and their first assertions.

Medium

  • internal/codexcloud/fetch.go:452: A monitor flush or plan-usage write can recreate a missing mirror while retaining obsolete resume cursors and saving the replacement file's offset. The next mirrorLost check then passes, so sync skips earlier history and may leave thread metadata missing.

    Fix: Persist a rebuild requirement whenever any writer detects a missing or shortened mirror. Clear it only after sync restores metadata and fetches history from the beginning.

  • internal/codexcloud/state.go:269: Recreating account.json restores only plan usage, leaving CheckAgain and Unfinished empty. Chats awaiting delayed items or same-second updates can still match their stored updatedAt and never be fetched again, leaving final output missing indefinitely.

    Fix: Reconstruct outstanding transcript work from thread state, or enqueue one recovery fetch per stored thread when account state is recreated.

  • internal/parser/codex_cloud.go:103: Dot-delegated tasks receive a subagent relationship to an orchestrator that is never imported. Ordinary session-list and export queries exclude them, including --include-children because those callers leave IncludeOrphans false, although the sidebar promotes orphans.

    Fix: Promote missing-parent Codex Cloud tasks consistently in list and export root selection while preserving parent provenance. Cover default exports and --include-children.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 21s | Total: 22m59s

@cpcloud
cpcloud force-pushed the codex-full-trace-analysis branch from f661d2a to 14367cc Compare October 15, 2026 12:23
@roborev-ci

roborev-ci Bot commented Oct 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (14367cc)

Verdict: Changes require fixes for 6 findings.

High

  • internal/parser/codex_cloud_usage_test.go:248: TestUsageKeepsEqualTotalsFromDifferentJoins duplicates TestUsageIncrementsAndBaselines. Both cover a join change, a counter reset, and a return to previously observed totals, but the newer test only sums input tokens and adds no distinct failure detection.

    Fix: Delete TestUsageKeepsEqualTotalsFromDifferentJoins and retain TestUsageIncrementsAndBaselines.

Medium

  • internal/db/session_batch.go:22: Artifact folder sync omits plan usage and capture coverage. ArtifactExportData and the manifest carry only messages and usage events, so exported Codex Cloud sessions lose their figures, counted-under references, and coverage. Plan-only sessions also disappear from destination plan-only counts.

    Fix: Carry SessionPlanUsage through artifact export and import, including origin-aware counted-under references, and add a behavioral round-trip test.

  • internal/codexcloud/roots.go:270: After account.json is lost or corrupted, recovery restores every root with an available report as a refresh root, including roots made pending by subsequent activity. If recovery occurs seven days after that activity, planUsage removes the refresh entry without requesting updated usage, leaving stale figures indefinitely. Refetching unchanged history does not restore the pending mark.

    Fix: Preserve or recover the successful report's observation time and restore roots as pending when their group changed afterward. Keep unchanged roots on the refresh schedule.

  • internal/codexcloud/state.go:269: Recovery from missing or corrupt account.json rebuilds only plan usage scheduling. Unfinished and check-again sets remain empty while per-thread updatedAt values survive, so idle-listed chats awaiting delayed items appear unchanged and are never fetched again unless their timestamp changes, leaving incomplete transcripts.

    Fix: Restore outstanding fetch work from per-thread state during account recovery, including idle chats with unfinished turns or pending save-lag checks. Add a regression covering delayed items after account-state loss.

  • internal/codexcloud/selection.go:58: Due retries retain listing or ID order. With live_tokens=false, repeated timeouts can raise both the account wait and thread retry delay to one hour, allowing the same first retry to time out and end every pass before other due retries run. A recovered chat can remain unimported until the failing chat is retired after seven days.

    Fix: Order due retries fairly, such as by oldest NextAttempt, while keeping them after non-retry work. Test multiple due retries where the first repeatedly times out and another recovers.

  • internal/codexcloud/usage_step_fixes_test.go:298: TestLostAccountStateSkipsUnreadableRoots uses chmod(0) to make badmirror unreadable, but Windows permits reading it and root bypasses Unix permission bits. Recovery then queues badmirror, breaking the expected request list and skipped-root assertions. Windows CI includes this package.

    Fix: Use malformed mirror bytes to trigger a portable read failure, or restrict the permission-specific case to non-root Unix environments.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 21s | Total: 24m51s

@cpcloud
cpcloud force-pushed the codex-full-trace-analysis branch from 80e18ce to ae98abd Compare October 15, 2026 14:00
@roborev-ci

This comment has been minimized.

@roborev-ci

This comment has been minimized.

@roborev-ci

roborev-ci Bot commented Oct 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (4535b4a)

Verdict: No findings at or above medium severity.


Reviewers: 3x codex, codex (security) | Synthesis: codex | Total: 23m48s

@cpcloud
cpcloud force-pushed the codex-full-trace-analysis branch from 4535b4a to 933aee2 Compare October 15, 2026 16:52
@cpcloud
cpcloud marked this pull request as ready for review October 15, 2026 16:52
@roborev-ci

roborev-ci Bot commented Oct 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (933aee2)

Verdict: Changes require fixes for 4 findings.

High

  • internal/codexcloud/pass.go:583: If an imported chat’s mirror is lost or truncated and history refetch fails, retryLater publishes an incomplete mirror. WithForceReplace() then allows archive sync to replace stored transcripts and usage with empty or partial data, potentially causing permanent loss.

    Fix: Prevent incomplete mirror recovery from replacing archived messages and usage until history is restored. Add regression coverage for a lost mirror, failed refetch, and subsequent archive sync.

  • internal/codexcloud/fetch_test.go:303: TestTurnEndingBetweenCallsNotFinished changes the turn only after capturing the inProgress turns/list reply. Incorrect items-before-status ordering would receive the same reply and still pass, so the fixture does not detect its intended regression.

    Fix: Move the completion transition into an items/list handler after capturing its reply, so incorrect ordering observes the completed turn and fails the assertions.

  • internal/codexcloud/monitor_test.go:294: TestServerRequestsAreNeverAnswered duplicates client_test.go’s TestServerRequestsNeverAnswered: both use a real ModeMonitor connection and the same approval method. Conn.readLoop consumes requests before Monitor receives them, so the warm join adds no distinct coverage.

    Fix: Delete TestServerRequestsAreNeverAnswered and the redundant no-reply loop in TestRefreshRequestFlushesClosesAndWaits. Preserve the latter test’s flush, connection-close, and home-wait assertions.

Medium

  • internal/codexcloud/roots.go:106: When a chat’s resolved parent changes between already mirrored parents, LinkParent adds it to the new root without removing it or its descendants from the old root. UsageGroupFor forwards these lists unchanged, causing duplicate attribution and incomplete usage for the new root.

    Fix: Move the chat’s full subtree out of its previous root, update counted-under records, and refresh both affected roots. Add coverage for changes between two mirrored parents.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 11s | Total: 27m8s

@roborev-ci

roborev-ci Bot commented Oct 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (2268165)

Verdict: Changes require fixes for 4 findings.

High

  • internal/codexcloud/pass.go:583: When an archived chat’s mirror is deleted or truncated and history refetch fails, retryLater publishes an incomplete mirror. Force-replace archive sync can then delete stored messages and usage absent from that mirror; historical live token counts cannot be recovered from server history.

    Fix: Preserve archived messages and usage during incomplete mirror recovery. Add regression coverage for mirror loss, failed refetch, and subsequent archive sync.

  • internal/codexcloud/fetch_test.go:303: TestTurnEndingBetweenCallsNotFinished captures an inProgress turns/list response before marking the turn completed. Fetching items before statuses would still receive that response and pass, so the test cannot detect its intended request-order regression.

    Fix: Move the completion transition into the items/list handler after capturing its response, so reading statuses afterward fails the assertions.

  • internal/codexcloud/monitor_test.go:294: TestServerRequestsAreNeverAnswered duplicates client_test.go’s TestServerRequestsNeverAnswered: both exercise a real ModeMonitor connection with the same approval request, which Conn.readLoop consumes before Monitor receives it. The warm join adds no distinct failure detection, and the no-reply loop in TestRefreshRequestFlushesClosesAndWaits repeats the same coverage.

    Fix: Delete TestServerRequestsAreNeverAnswered and the redundant no-reply loop in TestRefreshRequestFlushesClosesAndWaits. Preserve the latter test’s flush, connection-close, and home-wait assertions.

Medium

  • internal/codexcloud/roots.go:106: When a grouped chat moves between already mirrored parents, it is added to the new root without removal from the previous root. Its descendants remain attributed to the old root because the chat’s own descendants list was removed during earlier grouping, causing overlapping usage groups and missing descendants in the new group.

    Fix: Move the entire subtree out of its previous root, update all affected counted-under records, and refresh both roots. Add coverage for reparenting between mirrored parents.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 14s | Total: 22m10s

@roborev-ci

This comment has been minimized.

@cpcloud
cpcloud force-pushed the codex-full-trace-analysis branch from cc1a39e to 798468c Compare October 15, 2026 18:12
@roborev-ci

This comment has been minimized.

@roborev-ci

roborev-ci Bot commented Oct 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (6a89b34)

Verdict: Changes require fixes for 4 findings.

High

  • internal/sync/engine.go:18786: Transcript preservation runs only in local sync. Hosted raw publication uses ingest.ReconcileProviderHistory without Codex Cloud handling, allowing a truncated mirror to replace archived history and a complete refetch to discard captured usage events.

    Fix: Move Codex Cloud history and usage preservation into shared ingestion reconciliation and test shortened and refetched mirrors through hosted raw processing.

Medium

  • internal/sync/engine.go:18796: Rebuilding a lost mirror without live token records restores usage events but discards archived token totals, peak context, token-presence flags, and capture coverage. Replacement persists the empty values, causing usage APIs to report zero totals and has_token_data:false. Stored plan information absent from the rebuilt mirror is also discarded.

    Fix: Preserve archived aggregate token fields and capture coverage when restoring usage events, and retain stored plan information when no replacement is available. Extend recovery tests to verify these values.

  • internal/codexcloud/fetch.go:511: When a plan-usage append recreates a deleted mirror, forgetHistory clears the resume cursor but retains UpdatedAt. An unchanged listing supplies no metadata, and subsequent fetching does not restore the saved listing because mirrorLost is false. The rebuilt file remains unparseable; finished unchanged chats may never be selected for rebuilding.

    Fix: Persist a rebuild requirement that selection consumes and restore saved listing metadata during rebuilding. Verify successful parsing for unfinished and finished unchanged chats.

  • internal/service/direct.go:991: Comparison ranking passes empty plan-usage counts although UsageSummary includes plan-only models. These selectable models fail usagePairwiseSideInRanking, so fallback_to_default can silently replace a valid selection or clear the comparison after a range change or reload.

    Fix: Read GetPlanUsageSessionCounts with rankingFilter and pass the result to buildUsageSummary, matching UsageSummary.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 15s | Total: 21m20s

@roborev-ci

This comment has been minimized.

@roborev-ci

This comment has been minimized.

@cpcloud
cpcloud force-pushed the codex-full-trace-analysis branch from 09ea292 to 0eeb45c Compare October 15, 2026 22:47
@roborev-ci

roborev-ci Bot commented Oct 15, 2026

Copy link
Copy Markdown

roborev: Combined Review (0eeb45c)

Verdict: Changes require fixes for 3 findings.

High

  • internal/sync/engine.go:18796: After a mirror is lost, recovered history with additional messages bypasses token preservation. Because server history contains no live token counts, replacement permanently deletes archived usage events and clears token totals.

    Fix: Preserve archived token totals and usage events independently of message count when recovered history lacks live counts, while allowing the transcript to advance. Cover recovery after messages were added during a live-capture disconnect.

Medium

  • internal/codexcloud/roots.go:158: Rebuilding a deleted or truncated sub-agent mirror loses its counted_under outcome. Existing membership in the parent's descendant list skips the callback that restores it, causing archived attribution to be replaced with an empty plan-usage row and remain missing.

    Fix: Restore the counted_under outcome whenever a rebuilt descendant mirror lacks it, even when the descendant already belongs to the root's group.

  • internal/codexcloud/fetch.go:512: When a plan-usage write detects a deleted mirror for a finished chat, forgetHistory resets cursors but retains UpdatedAt and queues no history fetch. Chats outside the unfinished/check-again sets are then considered unchanged, leaving metadata-only mirrors whose newer plan reports archive reconciliation rejects.

    Fix: Persist a history-refetch work item when a non-history writer detects mirror loss, and retain it until the full history fetch succeeds.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 11s | Total: 18m56s

@cpcloud
cpcloud force-pushed the codex-full-trace-analysis branch from 0eeb45c to 8c42170 Compare October 16, 2026 14:28
cpcloud and others added 11 commits October 17, 2026 09:09
…irror

Two gaps after a mirror file is lost, both found in review:

- A refetched mirror with new messages but no token records replaced the
  archived chat, dropping its usage events, token totals, and capture
  coverage. Sync now imports the new messages and copies those from the
  archive, plus the plan usage row when the rebuilt file has none.
  Turns that ran while the mirror was missing are not counted as
  uncaptured.
- A plan usage or live-token write that rebuilt a lost mirror reset the
  resume point but did not queue the chat. An idle, finished chat with
  an unchanged listing was never fetched again, so its mirror stayed
  metadata-only. The write now marks the chat to check again; the mark
  clears once a pass finds nothing new.

The saved-copy log line now says "local copy" and names what replaces it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The goal read ran after the last 5-minute recency check, and a call can
take up to 2 minutes. A chat updated just under 5 minutes ago could pass
the check, then get resumed after the window closed, which is the cold
resume the window exists to prevent. The recheck now runs after the goal
reply, right before the resume.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The opt-in edits left long lines in three docs the format hook skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A read-only probe sent thread/goal/get for the newest chats on a real
login. Both returned {"goal": null}, so the goal check before a resume
reaches a method the hosted server supports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an figures

Review found four gaps after a mirror file is lost:

- A rebuilt file with no plan records parses as "plan usage not checked
  yet", and that empty row replaced the chat's saved plan figures. A
  chat with only plan usage lost them the same way. The archived figures
  now stay until newer plan usage arrives.
- The rule that kept the whole saved chat when a refetch had the same
  messages and no token counts also dropped newer plan usage from the
  rebuilt file. The token carry-over already keeps the counts, so the
  rule is gone and the write goes through.
- After the first new live record, peak context came only from the
  records left in the file. The larger of the archived and new peak now
  stays when the archived row is current.
- A hosted hub projecting raw captures had none of these rules, so a
  rebuilt file uploaded before its history was refetched replaced the
  hub's transcript and usage.

The rules now live in internal/ingest/codex_cloud_history.go. Sync calls
them with the archive's session, usage events, and plan row; the hub
calls them with the previous raw projection, which counts as current at
the same processing version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ial loss

Review found more ways a damaged mirror could drop saved usage:

- A file cut short after its newest token record still parses with
  token counts, just fewer, so it replaced the saved events and totals.
  Records only accumulate, so a parse whose output total is below a
  current saved copy now keeps the saved counts.
- A rebuilt file holding only a plan usage outcome, such as "left out",
  replaced the saved report figures. The saved figures now stay under
  the new outcome, as the parser does for a whole file. "Counted under"
  still clears them.
- The check-again mark for a rebuilt mirror lived only in the caller's
  account state until the caller saved it, after the rebuilt file and
  its offset were already saved. A crash or a later failure lost it.
  The mark is now saved first on every path.
- The 5-minute join check now runs before the goal read as well as
  before the resume, so a stale chat gets no goal read.

The sync log lines say where the counts come from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A turn that shows no progress for a day is marked stalled, and a stalled
chat is fetched again only when its listing changes. When a live-token or
plan usage write found the mirror file of such a chat missing, it marked
the chat to check again but left the stall mark in place. Selection skips
check-again chats that are stalled, so the rebuilt file held only the
listing and the usage record until the chat happened to change on the
server, and sync kept the archived copy the whole time.

The write now clears the stall mark along with setting the check-again
mark, in the saved account state and in the caller's copy, as a pass
already does before it commits a thread. If the turn still shows no
progress, the next pass marks it stalled again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mirror file can lose its newest frames and still hold older token and
plan usage records. The carry rules caught this only when the output
total came out smaller. A cut after a record that added only input or
cache tokens left the output total unchanged, so the parse replaced the
saved token counts with fewer usage events. A cut after the newer of two
plan reports left an older report in the file, and the parse replaced
the saved, newer figures with it.

A current saved copy now also wins when the parse holds only some of its
usage events. The check matches events by their dedup key, because a
file rebuilt after a loss starts a new running total under new keys; a
plain event count would keep the saved counts after every rebuild and
never take the new total. For plan usage, a parse whose report is older
than the saved one keeps the saved figures. Two copies share no file, so
the check compares report and outcome times where the parser compares
places in the file. A parse's outcome still applies when it is newer
than both the saved report and the saved outcome.

The hub now records that the previous projection contributed when it
keeps the saved transcript or carries anything from it, as other
providers already do, so the provenance rows say where the data came
from. The log lines now name the mirror file and the saved copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The refused-join lease test ended by turning the server's draining flag
back off and checking that a second join succeeds. The server only ever
turns that flag on, at shutdown, so the step tested a path production
cannot take. The rest of the test already shows that the refused join
holds no lease and closes its connection.

The direct test of the plan usage date formatter repeated the year case
that the new-year usage pass test already covers through a full pass,
which also checks that no new request goes out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is lost

A sub-agent chat counts toward the chat that started it, and plan usage
asks OpenAI about the parent with the child listed as a descendant. A
parent counted as known only while its mirror file existed. If that file
went missing and the child then changed, the child became its own root:
the usage step asked about the child alone and wrote that report to the
child's file. When the parent's file came back, the child was already in
the parent's descendants list, so no new "counted under" record was
written. The child kept its own figures for good, and the parent's
report counted it again.

A thread with saved state now stays known while its mirror file is
missing. Only a fetch's commit creates that state, so a thread that was
never mirrored still waits as before. A plan usage write to the parent
during the gap takes the existing lost-mirror path, which rebuilds the
file and fetches the parent's history again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…me report

When plan usage settles a chat, it writes the newest report again marked
final, with the same report time, and can add a left_out outcome after
it. A file cut short before those records still holds the first copy of
the report. The previous rule kept the saved figures only when the saved
report was strictly newer, so at equal times the parse won, and sync and
the hosted hub dropped the final mark and the outcome. Session details
then showed the figures as still open and lost the left_out warning.

At equal report times the parse now keeps the final mark of either copy
and the newer outcome. Equal times also cover a whole file that has just
gained its final copy, so the parse's own mark still applies. The rule
reports the saved copy as contributing only when it changed the row;
otherwise every resync of a settled chat would log a carry and mark the
hub's provenance row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cpcloud
cpcloud force-pushed the codex-full-trace-analysis branch from 84fbde6 to 0a435f1 Compare October 17, 2026 13:24
cpcloud and others added 2 commits October 17, 2026 09:31
The usage step writes left_out when OpenAI's answer leaves a chat out. A
later answer can bring the chat back with the same report time, when
OpenAI has not refreshed its figures. Its row follows the outcome in the
mirror file, so the parse correctly shows no outcome. The equal-time
rule added for cut files took any newer saved outcome, so sync and the
hub put the stale left_out warning back on every resync of an intact
file.

The saved outcome now returns at equal times only when the saved copy
is final and the parse is not. That is the pair settling writes: the
report again marked final, then its outcome. A file cut after a
left_out written while the chat was still pending loses the warning
until the next usage answer; telling that apart from a repeated report
would need the time each row was written, which the archive does not
keep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The equal-totals test checked that a second connection's record with
the same running total as the first connection's still counts after a
drop. The baselines test already runs that sequence and checks every
event it produces, so the extra test could not fail on its own.

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

roborev-ci Bot commented Oct 17, 2026

Copy link
Copy Markdown

roborev: Combined Review (aa6b7e1)

Verdict: Changes require fixes for 3 findings.

Medium

  • internal/ingest/codex_cloud_history.go:155: A later successful response with the same data_as_of clears a settled root's earlier left_out outcome, but this branch restores the saved final flag and outcome. Even with an intact mirror, the UI and exports continue reporting an obsolete failure until the report timestamp changes.

    Fix: Use record provenance or fetch order to distinguish a truncated mirror from a later repeated report. Preserve the saved final state only when the parsed copy predates it, and test reopening a settled root with a repeated report.

  • internal/service/session_usage_subagents.go:216: Completed Codex Cloud descendants that were never captured have HasCoverage=false and are skipped. A fully captured parent can therefore report zero uncaptured turns and complete priced cost while omitting descendant tokens. Ordinary live discovery can produce this because it skips recognized sub-agent threads.

    Fix: Include tokenless descendants in combined-usage completeness and warnings. Preserve finished-turn counts independently of live capture, or explicitly warn about plan-only descendants. Test a captured parent with a completed, tokenless child.

  • internal/codexcloud/pass.go:190: When the daemon uses a custom CODEX_HOME, DiscoverHomes places it first, leaving its recovery command as plain codex login. Running that command from a terminal without the daemon's CODEX_HOME signs into a different home, leaving the failed login unchanged and imports paused.

    Fix: Use the explicit homeLoginCommand for the first home too; its position does not establish the user's terminal environment.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 12s | Total: 21m11s

cpcloud and others added 3 commits October 17, 2026 10:22
Three rounds of rules tried to keep newer saved plan figures when a
mirror file was cut short after a newer report. Each rule compared the
report times OpenAI sends, and each round found an intact file the rule
overrode: an answer that repeats a report time after a left_out
outcome, a chat that resumes after settling before OpenAI refreshes its
figures, and an outcome-only parse taking an outcome older than the
saved report. Report times repeat across answers, so they cannot tell a
cut file from a whole one, and the cost of guessing wrong landed on
ordinary resyncs.

A parse that has a plan report now keeps its own figures. A file cut
short after a newer report shows the older one until the next usage
answer replaces it. Files lose whole records only through outside
damage, so that window is rare and closes on its own. The rules that do
not depend on report times stay: a file with no plan records keeps the
saved figures, and the token counts still use the event-key check. An
outcome-only parse keeps the saved figures and takes its outcome only
when that outcome is newer than the saved report, as the parser does
for a whole file.

The sync log line for a carry now names no cause, since a carry can
come from token counts, peak context, or plan figures.

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

When account.json is lost, AgentsView rebuilds the plan usage queue from
the thread files. A thread with saved state now counts as known even
without its mirror file, so the rebuild can reach a root whose mirror
file is also missing. If that root's newest plan record was final or an
outcome, the rebuild read the mirror file to place it, failed to open
it, logged an error, and left the root out until its chat changed.

A missing mirror file now reads as holding no plan records. The root
goes back to pending, and its next plan usage write takes the
lost-mirror path, which rebuilds the file and fetches the history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both the pass's own commit and the lost-mirror write mark a thread to
check again, and both must clear its stall mark, because selection
skips stalled threads. Each kept its own copy of that rule, so a change
to one could miss the other and leave a stalled chat that never gets
fetched again.

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

This comment has been minimized.

cpcloud and others added 3 commits October 17, 2026 10:57
Live capture never joins a sub-agent chat, so every Codex cloud
sub-agent has no token records and no capture coverage row. The
combined usage of a chat and its sub-agents added coverage only from
rows that exist. A captured parent with a finished sub-agent therefore
reported zero missed turns and complete totals, while the sub-agent's
tokens were missing from them.

Once the combined result has coverage, from the parent or any
sub-agent, each Codex cloud sub-agent without a coverage row now adds
its user messages as missed turns. One user message stands for one
turn, so the count is close, but the "partly captured" warning is
what the reader needs. Giving tokenless chats parser coverage would be
exact, but it would change how every tokenless chat counts in the plan
usage totals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n home

A status message for a signed-out home tells the user to run `codex
login`. The message printed the plain command for the first Codex home
the daemon reads, on the assumption that it is ~/.codex. A daemon
started with its own CODEX_HOME reads that home first, so the user was
told to run a plain `codex login` in a terminal that signs in ~/.codex,
and imports stayed paused.

The plain command now appears only for ~/.codex itself. Every other
home, the daemon's own CODEX_HOME included, gets the command that sets
CODEX_HOME for the login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second parse read the same file with fresh parser state and
checked the same value, so it could fail only when the first check
already had.

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

roborev-ci Bot commented Oct 17, 2026

Copy link
Copy Markdown

roborev: Combined Review (c36347a)

Verdict: Changes require fixes for 2 findings.

Medium

  • internal/codexcloud/roots.go:378: When account.json and a mirror file are lost, a cached non-final complete report can prevent history recovery. For chats unchanged for over seven days, planUsage removes the refresh, and unchanged listing timestamps prevent another history fetch.

    Fix: Check for missing or truncated mirrors before trusting cached reports, and force a full history refetch regardless of report availability or settle age. Add coverage for a lost mirror with an older complete report.

  • internal/postgres/raw_projection.go:202: Hosted projection derives signals before CarryCodexCloudUsage restores retained token totals and peak context. Rebuilt mirrors without token records therefore persist HasContextData=false and stale context-pressure signals, making health and context analytics inconsistent.

    Fix: Restore retained usage before deriving signals, or recompute context-dependent signals afterward. Extend the hosted rebuilt-mirror test to verify retained context signals and the health-score basis.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 9s | Total: 20m15s

For a mirror file that holds a plan usage outcome but no report, the
carry kept the saved report figures and took the new outcome only when
the outcome's time was later than the saved report's data_as_of. The
outcome's time comes from the local clock and data_as_of from OpenAI's,
so a local clock running behind dropped a real new outcome, such as
account_unavailable written just after a fresh report. The check only
served a file cut short after an older outcome, a case the carry no
longer tries to recover.

The outcome now always applies over the saved figures, as before. A
file cut after an older outcome shows it until the next usage answer.

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

This comment has been minimized.

cpcloud and others added 4 commits October 17, 2026 18:34
The plan-only flags test seeds a Codex cloud sub-agent with no coverage
row. Combined usage now counts such a sub-agent's user messages as
missed turns, so the combined path reports the root's two missed turns
plus the sub-agent's two. The per-session path still reports two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home discovery cleans each path, which turns /srv/codex into \srv\codex
on Windows, so the test's literal path never matched there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hub computed a session's signals in Finalize, then carried saved
token counts and peak context onto a parse of a rebuilt mirror. The
context signals read the peak, so a rebuilt mirror with new messages
but no token records was stored as having no context data, which also
feeds the health score. Sync carries before it computes signals and was
not affected.

The hub now derives the signals again whenever the carry changed the
session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When account.json is lost, AgentsView rebuilds what to fetch again from
the thread files. A chat whose mirror file went missing too was marked
only if it had changed recently. An idle chat's listing never changes,
so its history waited for its next change, which might never come.

The rebuild now marks any chat whose mirror file is missing to check
again. A file that is still there but damaged is left to the pass that
next selects the chat, so one damaged file does not pause imports for
the whole account on every rebuild.

The design doc also names a known limit: a mirror rebuilt after a token
counter reset starts below the saved total, and the archive cannot tell
a new counter from a continuation of the old one.

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

roborev-ci Bot commented Oct 17, 2026

Copy link
Copy Markdown

roborev: Combined Review (f8689c0)

Verdict: Changes require fixes for 2 findings.

Medium

  • internal/ingest/codex_cloud_history.go:61: Comparing entire usage histories by output totals loses usage after a token-counter reset and mirror rebuild. A rebuilt current baseline can be rejected despite containing new usage, or replace the archive and discard earlier baselines. This affects local sync and hosted projection.

    Fix: Preserve baseline provenance and reconcile increases with retained earlier baselines. Add coverage for recovery after an observed counter reset.

  • internal/codexcloud/roots.go:286: Account-state reconstruction detects missing mirrors but not files shorter than st.Offset. An old idle chat with matching listing timestamps and a cached complete report can remain truncated indefinitely because no history fetch is scheduled, affecting subsequent raw captures.

    Fix: Compare mirror size with st.Offset during reconstruction and schedule shortened mirrors for another fetch. Extend lost-account recovery coverage to include a truncated mirror for an old idle chat.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 9s | Total: 15m57s

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants