Skip to content

feat: add ccrecall import --repair-gaps to fix stale-tail sessions - #212

Merged
NodeJSmith merged 14 commits into
mainfrom
206
Sep 17, 2026
Merged

NodeJSmith merged 14 commits into
mainfrom
206

Conversation

@NodeJSmith

@NodeJSmith NodeJSmith commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

ccrecall import --repair-gaps

ccrecall import's fast-path skip check trusts a stat match (file_size/file_mtime equal to the stored import_log row) as proof the DB already holds every message from a transcript. When a session's DB rows fall behind its JSONL for any other reason, that stat match is a false positive — ccrecall import silently no-ops forever, even though ccrecall status --check-ingestion already knows exactly which sessions are stale_tail or ingestion_gap. Confirmed live against a real DB: 16 sessions, 334 turns unrepaired after running ccrecall import.

  • ingestion_status.py gains classify_sessions(), a generator extracted from summarize_ingestion()'s per-session loop, so the aggregate counts and the new per-session repair candidate list can't drift on what counts as recoverable. find_repairable_sessions() filters that generator to stale_tail/ingestion_gap candidates (session id, project id, on-disk file paths); reclassify_session() re-checks a single session's outcome after a repair attempt without a full-DB rescan.
  • New module hooks/import_repair.py (repair_sessions()) force-reimports each candidate, bypassing the stat/hash skip gate, and classifies the outcome into three distinct buckets: repaired (now ok), unrepairable (still stale_tail/ingestion_gap after a full force-reimport — the source transcript genuinely lacks the missing messages, so re-attempting won't help), and failed (an operational error during processing). Each candidate gets its own SAVEPOINT, so a poison transcript at candidate 12 of 16 doesn't lose the other 15 repairs.
  • ccrecall import gains an opt-in --repair-gaps flag, wired through cli/commands.py and hooks/import_conversations.py. It shares the same PID guard as the SessionStart background auto-import and skips (with a printed notice, not an error) if that guard is already held — --repair-gaps holds a longer transaction than a normal import, so racing a concurrent import raises the odds of a busy_timeout expiry mid-batch.
  • Repair is scoped DB-wide regardless of --project, and runs even when the project-scoped import step in the same invocation fails or exits early (e.g. an unsafe --project path) — the two are independent steps by design.

Known issues carried forward

  • Decompose repair/ingestion functions exceeding 50-line guideline (016 follow-up) #210 — several functions newly added or substantially grown by this feature (repair_sessions(), _run(), _sync_branches_and_messages(), classify_sessions()/summarize_ingestion()) exceed the 50-line guideline; deferred to a dedicated refactor with its own pinned-behavior tests rather than risking repair_sessions()'s carefully-reasoned durability model under time pressure.
  • De-duplicate _noop() helper between import_conversations.py and import_repair.py (016 follow-up) #211 — _noop() is duplicated between import_conversations.py and import_repair.py; de-duplicating requires resolving a circular import, which needs its own architectural call.
  • KI-002 (open, no issue filed) — find_repairable_sessions() snapshots classification once and doesn't re-validate a candidate's freshness immediately before its force-reimport. Low severity: force-reimport is idempotent and reclassify_session() re-checks the actual outcome after the fact, so a stale snapshot can't corrupt data or misreport a failed repair as successful.

Housekeeping

  • Dedupe test helpers, CLI parameter Annotated aliases, and cursor type hints surfaced during the clean-code pass.

Closes #206

Summary by CodeRabbit

  • New Features

    • Added the opt-in ccrecall import --repair-gaps option to recover sessions with stale or missing ingestion data.
    • Repairs process eligible sessions independently, continue past recoverable file issues, and report repaired, unrepaired, and failed totals.
    • Multi-file sessions are repaired as a merged transcript.
    • Runs safely skip when another import is already active.
  • Documentation

    • Added design and known-issues documentation for ingestion-gap repair behavior.

NodeJSmith and others added 7 commits September 16, 2026 09:36
…gs from ship-time challenge review

Fixes a data-integrity bug where multi-file repair candidates were force-reimported
file-by-file, silently unlinking branch_messages for sibling files while reporting
success. Also fixes a PID-guard concurrency gap, a batch-aborting SAVEPOINT failure
path, a session-id-churn message-count bug, and outcome-bucketing/observability gaps.
Post-implementation cleanup for the stale-tail import repair feature (016),
surfaced by the clean-code pass and known-issues walkthrough at the end of
orchestration:

- Hoist duplicated test helpers (`write_four_turns`, `age_past_grace_window`,
  `delete_message`) into `tests/conftest.py`; tie the grace-window offset to
  `STALE_TAIL_SECONDS` instead of a bare magic number
- Reuse the shared `_DB` CLI parameter alias in `cmd_import`/`cmd_status`
  instead of redefining it inline
- Extract `_SESSION`/`_PROJECT`/`_PATH` shared CLI parameter aliases used by
  `cmd_recent`/`cmd_search`/`cmd_search_messages`, replacing duplicated
  inline `Annotated` definitions
- Annotate `_db_coverage_fingerprint`/`_cached_ok_fingerprint`/
  `_record_ok_fingerprint`'s `cursor` parameter as `sqlite3.Cursor`

No behavior change. Also resolves stale/completed known-issues.md entries
(KI-001, KI-003, KI-004, KI-005), files the remaining structural
decomposition and `_noop()` dedup follow-ups as GitHub issues (#210, #211),
and archives the feature's task files now that all tasks are complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQiGktKrBfB5Tx5Gjrjy32
@NodeJSmith
NodeJSmith marked this pull request as ready for review September 16, 2026 18:51
@coderabbitai

coderabbitai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: b697dd69-d44c-4515-a53c-e59354d9c89e

📝 Walkthrough

Walkthrough

The change adds an opt-in ccrecall import --repair-gaps mode. It classifies repairable sessions, force-reimports single- and multi-file sessions, isolates candidates with savepoints, reports outcomes, protects concurrent imports, and adds unit and integration coverage.

Changes

Stale-tail repair

Layer / File(s) Summary
Session classification and repair candidates
src/ccrecall/ingestion_status.py, tests/conftest.py, tests/test_ingestion_status.py
Shared classification now supports repair candidate discovery and post-repair checks. Cache coverage includes active branch-message linkage counts. Tests cover pending, stale, gap, missing-source, and repaired states.
Forced import and per-session repair
src/ccrecall/hooks/import_conversations.py, src/ccrecall/hooks/import_repair.py, src/ccrecall/session_ops.py, tests/test_import_repair.py
Imports can force single-file or merged multi-file sessions. Repair candidates use savepoints, continue after poison-transcript failures, count recovered messages, and classify outcomes as repaired, unrepairable, or failed.
CLI orchestration and validation
src/ccrecall/cli/commands.py, tests/test_cli_smoke.py, tests/test_import_pipeline.py, design/specs/016-stale-tail-import-repair/*
The CLI forwards --repair-gaps. The run uses a PID guard, performs repair independently of --project, preserves another process’s marker, reports totals, and exits nonzero for repair failures. Design and known-issue records describe the behavior and tracked limitations.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ImportRun
  participant IngestionStatus
  participant ImportRepair
  participant Database
  CLI->>ImportRun: invoke import with repair_gaps
  ImportRun->>IngestionStatus: find repairable sessions
  IngestionStatus-->>ImportRun: return stale_tail and ingestion_gap candidates
  ImportRun->>ImportRepair: repair candidates
  ImportRepair->>Database: savepoint and force-import
  ImportRepair->>IngestionStatus: reclassify session
  ImportRepair-->>ImportRun: return repair totals
  ImportRun-->>CLI: print summary and exit status
Loading

Merge Risk: 🟡 Moderate · up to 1506a

The repair command can miss repairable damage or stop before processing remaining sessions, and an initialization failure can weaken concurrent-import protection. These issues should be corrected before merge.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Most changes support issue #206, including the classification extraction, repair loop, multi-file sync path, cache coverage tracking, and related tests and documentation. However, `src/ccrecall/cli/co… Remove the unrelated shared filter-alias refactor for cmd_recent, cmd_search, and cmd_search_messages, or provide a separate linked coding requirement that requires it.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 86 functions across 10 files. (2 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding the ccrecall import --repair-gaps feature to repair stale-tail sessions.
Linked Issues check ✅ Passed The PR meets the coding objective in issue #206. find_repairable_sessions selects only stale_tail and ingestion_gap sessions. repair_sessions force-reimports single-file candidates and merged …
Full details: Out of Scope Changes check

Explanation

Most changes support issue #206, including the classification extraction, repair loop, multi-file sync path, cache coverage tracking, and related tests and documentation. However, src/ccrecall/cli/commands.py also replaces parameter definitions for cmd_recent, cmd_search, and cmd_search_messages with shared aliases. The summary states that this changes no user-facing behavior and does not support stale-tail repair. This is a separate clean-code change outside issue #206.

Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 86 functions across 10 files. (2 skipped: 2 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 45462279d0

ℹ️ 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".

Comment thread src/ccrecall/hooks/import_repair.py
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-16T22:42:18.949500Z 658b2dc New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

A failed RELEASE SAVEPOINT during --repair-gaps left the savepoint open
on the connection instead of discarding it, so the next candidate's
SAVEPOINT nested inside the "failed" one rather than starting its own
independent, durably-committing transaction — breaking the per-candidate
durability model documented in import_repair.py's module docstring.

Roll back the candidate's writes and release the savepoint explicitly
before continuing the batch. If that recovery itself fails (a persistent,
not transient, problem), escalate to the same abort-the-batch handling
already used for genuine force-reimport infrastructure failures, with an
explicit log entry — rather than let an unhandled exception surface
without ever recording what actually happened.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQiGktKrBfB5Tx5Gjrjy32

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 1506a1a407

ℹ️ 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".

Comment thread src/ccrecall/hooks/import_repair.py Outdated
@NodeJSmith

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai 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.

Actionable comments posted: 6


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: bae02dce-4789-48fb-9ad4-b927a3d11097

📥 Commits

Reviewing files that changed from the base of the PR and between ce7bb83 and 1506a1a.

📒 Files selected for processing (12)
  • design/specs/016-stale-tail-import-repair/design.md
  • design/specs/016-stale-tail-import-repair/known-issues.md
  • src/ccrecall/cli/commands.py
  • src/ccrecall/hooks/import_conversations.py
  • src/ccrecall/hooks/import_repair.py
  • src/ccrecall/ingestion_status.py
  • src/ccrecall/session_ops.py
  • tests/conftest.py
  • tests/test_cli_smoke.py
  • tests/test_import_pipeline.py
  • tests/test_import_repair.py
  • tests/test_ingestion_status.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread design/specs/016-stale-tail-import-repair/design.md
Comment thread src/ccrecall/hooks/import_conversations.py Outdated
Comment thread src/ccrecall/hooks/import_conversations.py
Comment thread src/ccrecall/hooks/import_repair.py Outdated
Comment thread src/ccrecall/ingestion_status.py Outdated
Comment thread src/ccrecall/ingestion_status.py Outdated
NodeJSmith and others added 4 commits September 16, 2026 15:48
repair_sessions() called sort_session_files(filepaths) before the
per-candidate try/except/SAVEPOINT block. A file that disappears
between find_repairable_sessions()'s snapshot and this call raised
outside that guard, aborting the whole remaining batch instead of
counting just this candidate as failed.

import_session_group() already sorts its own input internally, and a
single-file candidate has nothing to sort, so the pre-sort was both
redundant and unguarded. Use filepaths directly instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQiGktKrBfB5Tx5Gjrjy32
run()'s finally block deleted PID_KEY_IMPORT unconditionally unless
_run() explicitly reported the acquisition was denied. But when
repair_gaps=True and _run() raises before it even reaches its own
try_acquire_pid_file() attempt (e.g. load_settings()/setup_logging()
fail first), repair_lock_denied never got updated from its default
False — so finally deleted a marker this invocation never acquired,
potentially un-guarding a genuinely live holder such as the
SessionStart-spawned background import.

Default repair_lock_denied to repair_gaps instead of False: whenever
repair_gaps=True, assume 'not yet acquired' until _run() proves
otherwise by returning. repair_gaps=False is unaffected — _run() never
touches PID_KEY in that case, so unconditional deletion on exit stays
the existing, correct contract with _spawn_background.

If _run() does acquire the lock and then raises later, this same
default causes this invocation's own marker to be left behind too —
deliberately not distinguished, since try_acquire_pid_file's liveness
probe reaps a dead PID's stale marker on the next acquisition attempt.

Adds a regression test simulating the early-exception race.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQiGktKrBfB5Tx5Gjrjy32
Two related robustness fixes to session classification:

- _db_coverage_fingerprint()'s link half only counted branch_messages
  rows per active branch, so a same-count substitution (one link
  deleted, a different one added) left the fingerprint unchanged and
  hid the corruption behind a cached 'ok' verdict — the exact class of
  regression (Finding 1/6) this fingerprint exists to catch. Switched
  to fingerprinting actual per-branch linked-message UUID membership.
  A follow-up fix closes a gap in that first pass: the membership
  query is an INNER JOIN starting from branch_messages, so an active
  branch with zero links (including a newly created one) contributed
  no row and was invisible to the fingerprint. Added a separate
  branch_part listing active-branch ids independently of link
  membership.

- classify_sessions() read transcript files again (_expected_uuids())
  and stat'd them (_is_contiguous_suffix branch) after
  _source_fingerprint() had already confirmed they existed — a file
  deleted in that window raised FileNotFoundError uncaught, aborting
  the whole classification generator and therefore every session
  summarize_ingestion()/find_repairable_sessions()/reclassify_session()
  was still trying to classify, not just the one with the vanished
  file. Now contained per-session, yielding missing_source and
  continuing, matching _source_fingerprint()'s existing pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQiGktKrBfB5Tx5Gjrjy32
design.md's Status is archived, so its Approach section body is
frozen per repo convention rather than rewritten to match the final
implementation. Appended a dated Addendum entry instead, noting two
places the shipped control flow diverges from what the doc describes:
savepoint-recovery failures also abort the repair batch (not just
force-reimport failures), and the PID guard is acquired before the
per-project import loop, not after it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQiGktKrBfB5Tx5Gjrjy32

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 24cf4e9cf7

ℹ️ 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".

Comment thread src/ccrecall/ingestion_status.py
classify_sessions()'s existing_msg_uuids check compared expected
active-path UUIDs against any message row for the session
(SELECT uuid FROM messages WHERE session_id = ?), not messages
actually linked to the active branch via branch_messages. A message
row surviving while its active-branch link was dropped or
substituted — the exact corruption class find_repairable_sessions()
exists to catch — still counted as 'present', so the session
classified as ok and got cached that way, permanently hiding the
corruption from --repair-gaps.

existing_msg_uuids is now scoped through branch_messages -> branches
(is_active = 1) -> messages, matching the join pattern
_db_coverage_fingerprint() already uses for the same reason.

Required updating test_ingestion_status.py's shared _seed_session()
fixture to actually create a matching active branch + links (it
previously inserted only into messages, so no test in the file could
distinguish 'row exists' from 'row is reachable from the active
branch') — added link_active_branch=False plus two small helpers
(_link_active_branch, _link_messages_to_branch) for tests that build
custom branch topology, switched two tests to conftest.py's existing
delete_message() (a bare DELETE now violates the branch_messages FK),
and rewrote test_branch_link_substitution_invalidates_ok_cache to
model a realistic same-count/different-membership corruption. Added
test_present_but_unlinked_message_is_not_counted_as_ok as a direct
regression test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQiGktKrBfB5Tx5Gjrjy32

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c48ab19bd

ℹ️ 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".

Comment thread src/ccrecall/hooks/import_conversations.py Outdated
Comment thread src/ccrecall/hooks/import_repair.py Outdated
…nt, dangling refs

- Check PID file ownership before cleanup: run()'s finally block now
  compares the marker's stored PID against os.getpid() so a concurrent
  holder's marker is preserved instead of unconditionally deleted.

- Contain reclassify_session failures per-candidate: a transcript that
  becomes unreadable between import and reclassification counts as failed
  instead of aborting the remaining batch. sqlite3.OperationalError still
  escalates to abort, matching the force-reimport's own policy.

- Strip dangling 'Finding N' cross-references from comments/docstrings
  across src/ and tests/. The explanatory text is self-contained without
  them.

- Add tests for PID marker preservation during plain import, reclassify
  failure containment, and SAVEPOINT acquisition failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQiGktKrBfB5Tx5Gjrjy32

@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

Here are some automated review suggestions for this pull request.

Reviewed commit: 658b2dc046

ℹ️ 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".

Comment thread src/ccrecall/hooks/import_conversations.py
@NodeJSmith
NodeJSmith merged commit 6e60dcb into main Sep 17, 2026
10 checks passed
@NodeJSmith
NodeJSmith deleted the 206 branch September 17, 2026 12:15
NodeJSmith pushed a commit that referenced this pull request Sep 17, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.25.0](v0.24.2...v0.25.0)
(2026-09-17)


### Features

* add ccrecall import --repair-gaps to fix stale-tail sessions
([#212](#212))
([6e60dcb](6e60dcb))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: nodejsmith-release-please[bot] <273458268+nodejsmith-release-please[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

ccrecall import never repairs a stale-tail gap once the file's stat matches import_log

1 participant