Skip to content

fix(codex): preserve transcript cursors across commit and compaction - #4191

Open
t0saki wants to merge 2 commits into
volcengine:mainfrom
t0saki:fix/codex-cursor-retention
Open

fix(codex): preserve transcript cursors across commit and compaction#4191
t0saki wants to merge 2 commits into
volcengine:mainfrom
t0saki:fix/codex-cursor-retention

Conversation

@t0saki

@t0saki t0saki commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Description

Builds on @7487's fix in #4113 (cherry-picked as the first commit, authorship preserved) and closes the gaps that fix opened.

The original bug: the Codex plugin tracks capture progress with a numeric capturedTurnCount, and two paths threw it away. commitAndClear() in session-start-commit.mjs deleted the whole state file after a successful commit, so resuming the same codex session_id — which DESIGN.md documents as keeping the same id and the same transcript — recreated the cursor at 0 and re-posted the entire history to the same deterministic cx-<codex-session-id> OV session. And auto-capture.mjs reset the cursor to 0 whenever the transcript shrank, so a /compact rewrite re-uploaded the compacted summary plus the retained tail. This contradicted the plugin's own model: PreCompact already releases ovSessionId while keeping the cursor, and DESIGN.md ("Commit-then-resume", "State file schema") defines ovSessionId: null as "committed, awaiting next Stop". The SessionStart path was the one place that didn't follow it.

#4113 fixes both paths, and the second commit here addresses what that leaves behind:

Nothing deletes state files any more. With both sweep call sites requiring a live ovSessionId, clearState() lost its last caller and the clear_no_ov branch became unreachable. Since main() writes a state file for every new session_id at SessionStart, even sessions that never capture a turn now leak one file forever, and listStates() reads every file on every SessionStart. The sweep now retires cursor-only states in the same pass: a real cursor is kept for resume until OPENVIKING_CODEX_COMMITTED_TTL_MS (default 30 days, past the life of the codex rollout it indexes), and a state that never captured anything goes on the idle schedule — which is exactly what the old sweep did with it.

Releasing the session id bumped lastUpdatedAt. A committed session looked freshly active to the next SessionStart, and the field no longer meant "last transcript activity" — which both the active window and the new retention rule depend on. saveState() now takes touch: false for writes that aren't transcript activity.

Cursor-only sessions stopped counting as concurrent. Requiring a live ovSessionId to be "recently active" hides a session PreCompact just committed, which may still be running — so the 1 active branch could seal a sibling session mid-flight where the old code correctly took the ≥2 branch. Concurrency is counted on activity again; only a state with a live session is actually committed.

The shrink predicate needed a name. role === "user" is not "a human turn" here: normalizeCaptureRole() maps tool results onto the user role too, and the inline check relied on their parts happening to be typed tool rather than text. It is now findLastHumanTurnIndex() in capture-utils.mjs with that coupling documented and unit-tested, and the -1 case (no human turn survived the rewrite, so we replay the whole transcript) is logged instead of silently folded into Math.max(0, ...).

Human Involvement

  • A human participated in the implementation or review loop
  • This PR was generated entirely by AI agents without human participation in the loop

Related Issue

Fixes #4058
Supersedes #4113

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement
  • Test update

Changes Made

  • Keep the transcript cursor when a SessionStart commit succeeds, and resume a shrunken transcript at the latest human turn instead of replaying compacted history (fix(#4058): [Bug]: Codex memory plugin replays historical turns after resume or transcript compaction #4113).
  • Retire cursor-only state files: OPENVIKING_CODEX_COMMITTED_TTL_MS (default 30 days) for real cursors, the idle TTL for states that never captured anything.
  • Add saveState(state, { touch: false }) so releasing ovSessionId doesn't rewrite lastUpdatedAt.
  • Count recently-active sessions on activity alone; commit only states that still have a live ovSessionId.
  • Extract findLastHumanTurnIndex() and log the full-transcript fallback when no human turn survives compaction.
  • Update DESIGN.md (new §6 "Cursor retention", heuristic and shrink trade-off notes), README.md, VERIFICATION.md (§6d-2), and the zh/en Codex integration docs with the new env var; bump the plugin to 0.7.6.

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this on the following platforms:
    • Linux
    • macOS
    • Windows

New tests: cursor-only retention (expired cursor and never-captured state retired, in-window cursor kept, no commit issued), deferral to idle TTL when a cursor-only sibling is still active, and findLastHumanTurnIndex() against tool results mapped onto the user role plus the empty case. Both new hook tests were confirmed RED against the #4113 commit alone.

Full CI memory-plugin suite on Node 24.13.0: 335 tests, 334 passed, 1 skipped, 0 failed. node --check on every changed script, bash -n on the six installer scripts CI parses, and stage-memory-plugin-marketplace.sh staging build all pass.

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

Additional Notes

The shrink path trades duplication for a bounded drop: turns older than the latest human turn are assumed captured, so if earlier Stop hooks failed to reach OpenViking and compaction happened before they were retried, those turns are dropped rather than replayed. That trade-off is now written down in DESIGN.md under "Post-compact transcript shrink".

7487 and others added 2 commits August 21, 2026 15:41
Preserving the transcript cursor after a commit stops the replay, but it also
means nothing deletes state files any more: clearState() lost its last caller,
so every codex session — including ones that never captured a turn — leaves a
file behind, and listStates() reads all of them on every SessionStart.

The sweep now retires cursor-only states in the same pass: a real cursor is
kept for resume until OPENVIKING_CODEX_COMMITTED_TTL_MS (default 30 days, past
the life of the codex rollout it indexes), and a state that never captured
anything goes on the idle schedule, which is what the old sweep did with it.

Releasing ovSessionId also wrote lastUpdatedAt, making a committed session look
freshly active; saveState() takes touch:false so the field keeps meaning "last
transcript activity" for both the active window and retention.

Requiring a live ovSessionId to count as recently-active made the heuristic
miss sessions PreCompact had just committed, which can still be running: the
count is back on activity alone, and only a state with a live session is
committed.

Also name the shrink predicate: role === "user" covers tool results too
(normalizeCaptureRole maps them onto the user role), so findLastHumanTurnIndex
requires a text part, and the no-human-turn fallback to a full replay is now
visible in the log instead of silent.
Copilot AI lite review requested due to automatic review settings August 21, 2026 07:51

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

[Bug]: Codex memory plugin replays historical turns after resume or transcript compaction

3 participants