Skip to content

feat(storage): preserve retention evidence for later evaluation#333

Closed
wenchanghan wants to merge 13 commits into
ReflexioAI:mainfrom
wenchanghan:feat/retention-archive
Closed

feat(storage): preserve retention evidence for later evaluation#333
wenchanghan wants to merge 13 commits into
ReflexioAI:mainfrom
wenchanghan:feat/retention-archive

Conversation

@wenchanghan

@wenchanghan wenchanghan commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Reflexio bounds live SQLite tables by deleting old rows. This PR optionally preserves those rows in a bounded JSONL FIFO so older sessions remain available for later evaluation without allowing either storage path to grow forever.

  • Live SQLite retention remains the priority and never waits on archive recovery.
  • New evidence is written first; oldest complete archive segments are then evicted until total JSONL size is under the configured ceiling.
  • Each segment keeps one retention batch together, including cascade-deleted rows.
  • Interaction rows retain every current evaluation input; only embeddings are excluded.
  • The publish schema accepts bounded injection-time learning snapshots so later evaluation does not substitute edited learning text.

Flow

flowchart LR
    A["SQLite selects up to 1,000 old target rows"] --> B{"Does the batch fit?"}
    B -->|Yes| C["Write one JSONL segment"] --> D["Delete the same live rows"]
    B -->|No| E["Split into smaller complete batches"] --> C
    C --> F["If over the ceiling, evict oldest segments"]
    F --> G["Next PR: read live + JSONL into evaluator"]
Loading

Archive contract

Setting Behavior
REFLEXIO_RETENTION_ARCHIVE=1 Enable archive-before-delete for SQLite. Default is off.
REFLEXIO_RETENTION_ARCHIVE_MAX_BYTES=10737418240 Set total completed-segment ceiling. Default is 10 GiB.

Archive files always live in an archive directory beside the SQLite database—for example, ~/.reflexio/data/archive beside the default database.

Each archive-enabled SQLite transaction selects at most 1,000 retention-target rows. This is a work-size bound, not a failure threshold. A timestamped *.jsonl segment contains those rows and any related rows deleted with them. For example, deleting an old requests row also deletes and archives the interactions rows that reference its request_id; many targets have no related rows and remain single-table batches.

If a batch expands beyond the byte ceiling, Reflexio recursively splits it into smaller complete segments instead of dropping the entire batch. FIFO then retains the newest complete groups that fit. Only one indivisible target plus its related rows can be skipped if that group alone exceeds the entire archive ceiling. Archive I/O failures are also logged. In all cases, live retention continues so learning and application behavior remain available and SQLite stays bounded.

Example records

One segment may contain several tables; every line identifies its table explicitly.

{"table":"requests","archived_at":1783792800,"row":{"request_id":"req-123","user_id":"project-7","session_id":"session-42","created_at":1783789200,"source":"agent","agent_version":"v1"}}
{"table":"interactions","archived_at":1783792800,"row":{"interaction_id":987,"request_id":"req-123","role":"Assistant","content":"Use pathlib here.","retrieved_learnings":"[{\"kind\":\"user_playbook\",\"learning_id\":\"11\",\"snapshot\":{\"title\":\"Filesystem discipline\",\"content\":\"Use pathlib for filesystem work.\",\"trigger\":\"writing Python filesystem code\",\"rationale\":\"Path objects are easier to compose.\"}}]"}}

Why the complete interaction row is retained

This is evaluation evidence, not an additional application log. Existing evaluation paths use more than message text:

  • agent-success evaluation uses content, roles, tool calls, user actions, and the final image;
  • shadow evaluation uses shadow_content;
  • learning attribution uses citations and retrieved_learnings.

Dropping those fields here would make some retroactive evaluations impossible. The large vector embedding column is excluded because no evaluator needs it. Tests enforce “all interaction columns except embedding.”

The learning snapshot contains the injection-time source fields associated with that response. It lets a later evaluator avoid substituting a learning that has since been edited.

What comes next

The evaluator follow-up only needs to:

  1. Read request and interaction records from live SQLite and all JSONL segments.
  2. Merge and deduplicate them by stable database keys.
  3. Reconstruct the bounded session and use captured learning snapshots when present.
  4. Run the existing evaluators and persist results normally.

Verification

  • Full suite: 4,757 passed, 124 skipped; 82.65% coverage.
  • Focused retention/FIFO suite: 19 passed.
  • Ruff and Pyright pass on changed Python files.
  • FIFO test proves the oldest segment is removed, the newest remains, and total size stays within the ceiling.
  • Cascade test proves one segment keeps request and dependent interaction rows together.
  • A 1,200-row integration case leaves 960 rows live, archives 240, and reconstructs all 1,200 IDs.

Boundaries

  • FIFO intentionally provides a recent bounded evidence window, not permanent history.
  • JSONL is a separate export: deleting a user from SQLite does not rewrite already archived lines.
  • A partial operating-system failure can leave duplicate evidence; the reader must deduplicate by table key.
  • Archive export is implemented for SQLite. Other storage backends continue their existing retention behavior if archive export is unavailable.

Preserve every retention-trimmed SQLite row as append-only JSONL, including cascade dependents, while keeping the feature default-off and failing closed on archive errors. Document the evidence lifecycle and cover the archive contract with integration tests.
Hold SQLite's writer guard across row selection, archive writes, and deletion so concurrent children cannot be removed without evidence. Cover both the race and the automatic over-limit cleanup path.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds opt-in JSONL archiving before retention deletion, SQLite transaction and row-fetch hooks, cascade archival, configurable archive directories, bounded cleanup batches, and integration coverage. It also adds bounded retrieved-learning snapshots and omits null citation fields during interaction persistence.

Changes

Retention archive

Layer / File(s) Summary
Archive contract and persistence
reflexio/server/services/storage/retention_archive.py, README.md
Adds environment-controlled segmented JSONL archiving, atomic writes, FIFO ceiling enforcement, and operational documentation.
Retention deletion integration
reflexio/server/services/storage/retention_mixin.py, reflexio/server/services/storage/sqlite_storage/base/_deletion.py, reflexio/server/services/generation_service.py
Archives target and cascade rows inside a SQLite transaction guard before deletion, fetches non-embedding rows by key, and uses bounded cleanup batches when archiving is enabled.
Retention archive validation
tests/server/services/storage/test_retention_archive.py
Covers configuration, ceiling behavior, cascades, concurrency, failures, row contents, batching, composite keys, directory overrides, and unsupported backends.

Retrieved-learning snapshots

Layer / File(s) Summary
Snapshot schema and validation
reflexio/models/api_schema/domain/entities.py, tests/models/api_schema/test_input_size_bounds.py
Adds optional retrieved-learning snapshots with bounded text fields and a 10 MiB aggregate UTF-8 size limit.
Snapshot API and storage propagation
tests/server/api_endpoints/test_api_routes.py, tests/server/services/storage/test_storage_contract_retrieved_learning_evals.py
Verifies snapshot payload propagation through publishing and exact storage round trips.

Citation serialization

Layer / File(s) Summary
Citation payload serialization
reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py
Omits null citation fields in both interaction insertion paths.

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

Sequence Diagram(s)

sequenceDiagram
  participant RetentionCleanup
  participant SQLiteDeletionMixin
  participant retention_archive
  participant SQLiteDatabase
  RetentionCleanup->>SQLiteDeletionMixin: select oldest retention keys
  SQLiteDeletionMixin->>SQLiteDatabase: fetch target and cascade rows
  RetentionCleanup->>retention_archive: append_archive_batch(...)
  retention_archive-->>RetentionCleanup: enforce archive max_bytes
  RetentionCleanup->>SQLiteDeletionMixin: delete retention rows
  SQLiteDeletionMixin->>SQLiteDatabase: complete guarded transaction
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: adding storage retention archiving to preserve evidence for later evaluation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Comment thread docs/adr/0001-retention-archives-rows-before-deleting.md Outdated
Cap total JSONL storage at 10 GiB by default and stop trimming when full, while batching archive reads and fencing cleanup across SQLite connections. Add high-volume and ceiling-path coverage, and remove client-specific design docs from the OSS change.
@wenchanghan

Copy link
Copy Markdown
Contributor Author

Review findings (critical pass; the archive suite is green locally — 11/11 in test_retention_archive.py).

1. Consider splitting the snapshot feature into its own PR.
b23091d (RetrievedLearningSnapshot) is a wire-schema/evaluation change riding a storage-retention PR, and it reverses the field's documented design intent ("deliberately minimal — just the identity pair… callers should not need to supply injection-time debug fields"). It is already an isolated commit; splitting keeps this PR single-purpose and lets the schema change be weighed on its own merits (it denormalizes learning content into every interaction row — the growth this retention work exists to manage).

2. Accepted payload bytes are unbounded.
snapshot.content allows 1 MB, and validate_retrieved_learnings_total caps count (1000/publish) but not bytes → roughly 1 GB accepted per publish request, all persisted into the interaction row. Real learning contents are a few KB; suggest a much tighter content cap (e.g. 100 KB) or an aggregate-bytes validator.

3. Flag-on cleanup holds the global write lock across file I/O — costly at default caps.
With REFLEXIO_RETENTION_ARCHIVE=1, select → fetch → JSONL append → delete all run inside BEGIN IMMEDIATE plus the storage lock. At the default 250 000-row cap a single pass dooms ~50 000 rows, meaning hundreds of MB written under a global write fence — and _retention_fetch_rows does SELECT *, dragging sizable embedding blobs out of SQLite only for append_archive_rows to discard them. Fine for low-cap deployments; at defaults it is a multi-second stall for every writer. Suggest documenting the tradeoff in the PR body at minimum; cheap wins: select explicit columns (skip embedding) and/or bound rows archived per pass.

4. Simplification opportunities.

  • The REFLEXIO_RETENTION_ARCHIVE_WARN_BYTES machinery — about half of retention_archive.py, a process-global _warned_archive_dirs set, and three tests — exists to emit one log line, and emits it at logger.error for what is semantically a warning threshold. Logging the archive size at info on each append gets most of the value for ~5 lines.
  • Triple-layer chunking: the mixin chunks keys (chunked(parent_ids), chunked(keys, rows_per_batch)) and _retention_fetch_rows chunks again internally (_select_in_chunks / its own rows_per_chunk). Letting the hook own chunking removes the outer loops and the rows_per_batch math from the mixin.

5. Gaps.

  • The three new env vars are undocumented.
  • The composite-key fetch path (e.g. a retrieved_learning_evaluation cleanup) and the non-SQLite fail-closed behavior (mixin defaults raising NotImplementedError) are untested.
  • parent_ids = [(key[0],) for key in keys] assumes single-key cascade parents — true today and documented on CascadeRef, but an assert would make a future violation loud instead of a silent mis-archive.

Also verified good in this pass: fail-closed holds end-to-end (fetch errors re-raise as StorageError, the guard rolls back, rows survive archive failure); flag-off behavior is byte-identical, with a test asserting the guard is never acquired; all real-data cascade dependents are covered by RETENTION_CASCADES (fts/vec rows are derived data); union-invariant, cascade, concurrency, and dir-override tests all pass.

🤖 Generated with Claude Code

@wenchanghan

Copy link
Copy Markdown
Contributor Author

Incorporated the actionable review items in f02f21d:

  • capped snapshot content at 100 KB and aggregate snapshot text at 10 MiB per publish;
  • preserved the original retention amount while splitting archive work into 1,000-row SQLite transactions;
  • stopped selecting/loading embeddings during archival;
  • removed redundant outer fetch chunking and added a loud assertion for cascade-key assumptions;
  • documented all archive environment variables and the SQLite lock/batching tradeoff;
  • added composite-key, unsupported-backend, aggregate-size, and bounded-batch coverage.

I kept the warning at error severity because the agreed product behavior is a loud operational signal once the unbounded JSONL archive crosses its threshold. I also kept the snapshot commit in this connected PR set: capturing injection-time content is required now so the follow-up can focus solely on reading JSONL into the evaluator. Full suite: 4,751 passed.

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

🧹 Nitpick comments (1)
reflexio/server/services/storage/sqlite_storage/base/_deletion.py (1)

69-103: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Apply the safe identifier quoting consistently across all statements.

escaped_table is computed and correctly doubled-and-quoted, but it is only used in the PRAGMA (Line 73). The SELECT on Lines 86 and 103 interpolate the raw table_name, and the WHERE clauses on Lines 87 and 97 interpolate unquoted key_columns, while the projected columns are quoted. Inputs come from trusted RetentionTarget config today, so there is no live injection, but the quoting invariant is only half-enforced and will silently break if any table/key identifier ever becomes dynamic. Reusing escaped_table and quoting key_columns the same way as column_names makes the invariant hold and clears the OpenGrep S608 finding on Lines 72-74.

🛡️ Suggested consistent quoting
     columns_sql = ", ".join(quoted_columns)
+    quoted_keys = [
+        '"' + column.replace('"', '""') + '"' for column in key_columns
+    ]
     if len(key_columns) == 1:
         rows = self._select_in_chunks(
-            f"SELECT {columns_sql} FROM {table_name} "  # noqa: S608
-            f"WHERE {key_columns[0]} IN ({{placeholders}})",
+            f'SELECT {columns_sql} FROM "{escaped_table}" '  # noqa: S608
+            f"WHERE {quoted_keys[0]} IN ({{placeholders}})",
             [key[0] for key in keys],
         )
         return [dict(row) for row in rows]
 
     params_per_key = len(key_columns)
     rows_per_chunk = max(1, RETENTION_DELETE_CHUNK // params_per_key)
     rows: list[Any] = []
     for key_chunk in chunked(keys, rows_per_chunk):
         where = " OR ".join(
-            "(" + " AND ".join(f"{column} = ?" for column in key_columns) + ")"
+            "(" + " AND ".join(f"{column} = ?" for column in quoted_keys) + ")"
             for _ in key_chunk
         )
         params = [value for key in key_chunk for value in key]
         rows.extend(
             self.conn.execute(
-                f"SELECT {columns_sql} FROM {table_name} WHERE {where}",  # noqa: S608
+                f'SELECT {columns_sql} FROM "{escaped_table}" WHERE {where}',  # noqa: S608
                 params,
             ).fetchall()
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@reflexio/server/services/storage/sqlite_storage/base/_deletion.py` around
lines 69 - 103, Apply identifier quoting consistently in the deletion query
flow: reuse the escaped, quoted table identifier derived for the PRAGMA instead
of interpolating raw table_name in both SELECT statements, and create
escaped/quoted key-column identifiers before building the single-key and
composite-key WHERE clauses. Update the SQL construction around
_select_in_chunks and the composite-key rows loop while preserving their
existing parameter binding and chunking behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@reflexio/server/services/storage/sqlite_storage/base/_deletion.py`:
- Around line 69-103: Apply identifier quoting consistently in the deletion
query flow: reuse the escaped, quoted table identifier derived for the PRAGMA
instead of interpolating raw table_name in both SELECT statements, and create
escaped/quoted key-column identifiers before building the single-key and
composite-key WHERE clauses. Update the SQL construction around
_select_in_chunks and the composite-key rows loop while preserving their
existing parameter binding and chunking behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 901abca1-60a3-4d2f-afba-bbe0aa732a80

📥 Commits

Reviewing files that changed from the base of the PR and between a3dbda0 and f02f21d.

📒 Files selected for processing (11)
  • README.md
  • reflexio/models/api_schema/domain/entities.py
  • reflexio/server/services/generation_service.py
  • reflexio/server/services/storage/retention_archive.py
  • reflexio/server/services/storage/retention_mixin.py
  • reflexio/server/services/storage/sqlite_storage/base/_deletion.py
  • reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py
  • tests/models/api_schema/test_input_size_bounds.py
  • tests/server/api_endpoints/test_api_routes.py
  • tests/server/services/storage/test_retention_archive.py
  • tests/server/services/storage/test_storage_contract_retrieved_learning_evals.py
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • reflexio/server/services/storage/retention_mixin.py

@wenchanghan wenchanghan changed the title feat(storage): preserve rows removed by retention feat(storage): preserve retention evidence for later evaluation Jul 11, 2026

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

🧹 Nitpick comments (1)
reflexio/server/services/storage/retention_archive.py (1)

113-148: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the dead archive-state tracking
_warned_archive_dirs has no readers anywhere in the repo, and projected_bytes += len(encoded_line) has no effect after the write loop. Remove both unless a future warning path still needs the set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@reflexio/server/services/storage/retention_archive.py` around lines 113 -
148, Remove the unused _warned_archive_dirs tracking, including its discard/add
operations, unless an existing warning path reads it. In the archive write block
around the retention archive flow, remove the ineffective projected_bytes +=
len(encoded_line) update after each write while preserving the write and
ceiling-check behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@reflexio/server/services/storage/retention_archive.py`:
- Around line 113-148: Remove the unused _warned_archive_dirs tracking,
including its discard/add operations, unless an existing warning path reads it.
In the archive write block around the retention archive flow, remove the
ineffective projected_bytes += len(encoded_line) update after each write while
preserving the write and ceiling-check behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 92bc8255-a415-454f-ada1-628e5857a25f

📥 Commits

Reviewing files that changed from the base of the PR and between f02f21d and 31499fe.

📒 Files selected for processing (3)
  • reflexio/server/services/storage/retention_archive.py
  • reflexio/server/services/storage/retention_mixin.py
  • tests/server/services/storage/test_retention_archive.py

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@reflexio/server/services/storage/retention_archive.py`:
- Around line 98-105: Update the archive entry flow around the segment creation
and cleanup logic to sweep stale .tmp files in archive_dir before calculating
size or evicting archived segments. Ensure orphaned temporary files are removed
or included in retention accounting so repeated crashes cannot bypass max_bytes,
while preserving the existing atomic write-and-replace behavior.
- Around line 107-123: Update the archive eviction loop over segments to
tolerate files disappearing between discovery, stat, and unlink. Handle
missing-file errors around the affected operations as no-ops, continuing the
sweep without incrementing eviction counters or failing the retention pass,
while preserving existing behavior for other filesystem errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7905c8de-82a2-42f2-a70b-da701202f787

📥 Commits

Reviewing files that changed from the base of the PR and between 31499fe and 9e61f5d.

📒 Files selected for processing (4)
  • README.md
  • reflexio/server/services/storage/retention_archive.py
  • reflexio/server/services/storage/retention_mixin.py
  • tests/server/services/storage/test_retention_archive.py
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • reflexio/server/services/storage/retention_mixin.py
  • tests/server/services/storage/test_retention_archive.py

Comment thread reflexio/server/services/storage/retention_archive.py
Comment thread reflexio/server/services/storage/retention_archive.py Outdated
@wenchanghan

Copy link
Copy Markdown
Contributor Author

Re-review of the follow-up commits (f02f21d..8d2196b).

Prior findings all addressed:

  • Payload bounds — snapshot content cap 1 MB → 100 KB plus a 10 MiB aggregate snapshot-bytes validator per publish, with tests.
  • Lock hold — archive-enabled cleanup now loops in 1000-parent batches (test proves 50 × 1000 calls for a 50 k delete), and _retention_fetch_rows projects columns via PRAGMA table_info so embedding never leaves SQLite (test asserts the exact column set).
  • Simplifications/gaps — chunking flattened into the hook, single-key cascade assert added, composite-key fetch and non-SQLite paths now tested, README documents the three env vars.

The revision also changes two policies rather than just fixing findings; both are implemented and documented consistently, but they invert the earlier contract, so flagging them explicitly for reviewers:

  1. Fail-open. Archive failure now logs "evidence was not preserved" and deletion proceeds (the previous test asserting rows survive a failed archive was deliberately inverted). This removes the wedge risk — an unwritable dir or full disk can no longer stall retention — at the cost of a new worst case: a permanently misconfigured archive dir silently destroys evidence on every cleanup with only server-log visibility. Non-blocking suggestion: surface archive write failures somewhere more visible than logs, since this is now the primary evidence-loss mode.
  2. Bounded FIFO. The archive is now per-batch segment files with oldest-whole-segment eviction at REFLEXIO_RETENTION_ARCHIVE_MAX_BYTES. Mechanics look sound: atomic tmp-then-rename install, the newest segment is never evicted, one segment = one retention batch so parent and cascade rows never split, zero-padded sortable names, eviction covered by tests. Consumers should note the invariant is now "most recent ~10 GiB of evictions," not "full history."

Two minor residuals, neither blocking:

  • Cascade rows are unbounded per batch: 1000 parents can fan out to arbitrarily many dependent rows, all materialized in memory and encoded as a single segment under the write guard. Today's realistic fan-out (~2 interactions per request) makes this theoretical; a fan-out note or cap could come later.
  • A crash between segment write and rename leaves a .tmp file that is never cleaned and never counted toward the ceiling (the glob is *.jsonl), so it lingers as junk bytes.

Verified locally at 8d2196b: 31/31 across test_retention_archive.py + test_input_size_bounds.py (the coverage-percentage failure is the repo-wide gate applied to a partial run).

The snapshot schema remains bundled with the archive change — previously noted, maintainers' call. Otherwise ready for lead review from my side.

🤖 Generated with Claude Code

@wenchanghan

wenchanghan commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Final archive-contract clarification:

  • 1,000 is a work-size cap, not a failure threshold. Archive-enabled selection is capped at 1,000 retention-target rows per transaction.
  • A batch may expand through related rows (for example, a request and its interactions). If its encoded segment exceeds the byte ceiling, Reflexio now recursively splits it into smaller complete segments instead of dropping the whole batch. Only one indivisible target plus its related rows can still be skipped if that group alone exceeds the entire ceiling.
  • The archive location is intentionally fixed at <SQLite database directory>/archive; the unused directory override was removed.
  • Segments may contain multiple tables, with every JSONL line carrying its table name.

@wenchanghan

wenchanghan commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Closing — superseded by a direction change

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.

1 participant