Skip to content

feat: publish retrieved learning links#128

Merged
yyiilluu merged 14 commits into
ReflexioAI:mainfrom
wenchanghan:claude/evidence-loss-linking-d9a453
Jul 12, 2026
Merged

feat: publish retrieved learning links#128
yyiilluu merged 14 commits into
ReflexioAI:mainfrom
wenchanghan:claude/evidence-loss-linking-d9a453

Conversation

@wenchanghan

@wenchanghan wenchanghan commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Why

Reflexio already knows how to store and evaluate retrieved_learnings, but claude-smart does not currently tell it which preferences or skills were shown before an Assistant response. The interaction arrives without that link, so later evaluation cannot attribute the response to the retrieved learning.

Before / changed / after

Behavior
Before claude-smart records that a learning was shown locally, then publishes the Assistant response without the learning ID.
Changed The shared publish path attaches each shown learning's {kind, learning_id} pair to the first eligible Assistant response.
After Reflexio stores the response-to-learning link and its existing on-demand evaluator can evaluate linked traffic.
flowchart LR
    A["Learning is shown"] --> B["Local registry keeps its ID"]
    B --> C["Next Assistant response"]
    C --> D["Publish kind + learning ID"]
    D --> E["Reflexio stores the link"]
    E --> F["On-demand evaluation"]
Loading

Published data

The only new interaction field is an identity list:

{
  "role": "Assistant",
  "content": "Use pathlib here.",
  "retrieved_learnings": [
    {"kind": "user_playbook", "learning_id": "11"},
    {"kind": "profile", "learning_id": "profile-1"}
  ]
}

No learning text or custom snapshot is copied into this field.

Reliability

  • The existing session buffer records the identity pairs at injection time. Event order maps them to the next Assistant response, so there is no timestamp guess or cross-file join and same-second events remain exact. It stores no learning text.
  • Each publish attempt freezes its record range before the network call. Overlapping publishers re-read the session state and converge on the same range and request ID. A failure retries that exact range before any newer turns; the high-water mark advances only after success.
  • Repeated IDs are kept once per response, in first-seen order.
  • A defensive request-wide cap keeps the first 1,000 links and logs any excess. Normal responses contain far fewer links.
  • Registry rows without a valid identity pair are ignored. A publish with no links stays on the normal typed client path.
  • Some installed Reflexio clients predate the field and would strip it during typed serialization. Only a publish containing actual links uses the same client's authenticated low-level request and a stable request ID.
  • If that compatibility path is unavailable before sending, claude-smart uses the normal client to publish the base interaction without links. If the enriched request is rejected or its response is lost, it retries the base payload through the same path with the same stable request ID; Reflexio's existing duplicate-request fence prevents storing the interaction twice.

Scope and what follows

This PR records identity pairs for new traffic. It does not add snapshots, an archive, a dashboard surface, or another linkage store.

Reflexio's merged upstream evaluator support stores and judges the field on demand. The companion Reflexio PR makes those evaluations follow an ID through later merges or supersession. After both changes are released, opted-in machines begin retaining the complete response-to-learning identity data needed for future analysis. Separate read-only tools reconstruct the limited pre-writer window and report evidence quality; no historical identity is guessed when lineage is absent.

Verification

  • Full Python suite: 662 passed.
  • Focused linking and three-host integration suite: 156 passed.
  • Ruff and Pyright on every changed Python file: passed.
  • Dashboard ESLint and production build: passed.
  • Locked-project and standalone-lock checks: passed.
  • git diff --check: passed.

Summary by CodeRabbit

  • New Features

    • Published assistant interactions can now include relevant retrieved learning references.
    • Learning references are deduplicated, ordered, and capped to keep payloads manageable.
    • Publishing now uses stable request identifiers for safer retries and concurrent publishing.
  • Bug Fixes

    • Improved publish watermark handling prevents duplicate or missing interactions.
    • Failed or interrupted publishes can retry consistently while preserving newly added turns.
    • Unsupported publishing paths gracefully omit optional learning references.

Attach each injected profile or playbook to one successfully published Assistant interaction with retry-safe persisted state. Add a read-only live/archive health report for evidence windows, link resolution, and session joins.
Coordinate registry readers with writers and retain the last incomplete JSONL line for retry, preventing a publish watermark from permanently skipping an injected learning.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Retrieved Learning Publishing

Layer / File(s) Summary
Incremental retrieval state wiring
plugin/src/claude_smart/state.py, tests/test_state.py, tests/test_events.py
Injected learning entries are normalized into metadata records and attached, deduplicated, and capped on eligible Assistant turns.
Deterministic publish batching
plugin/src/claude_smart/publish.py, tests/test_publish.py, tests/host_learning_harness.py
Publishing selects stable frozen ranges, generates deterministic request IDs, handles watermark updates, and validates retries, concurrency, ordering, and caps.
Adapter payload preservation
plugin/src/claude_smart/reflexio_adapter.py, tests/test_adapter.py
Retrieved-learning payloads use raw publishing when available, with stable-request retries and field-stripping fallbacks.

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

Sequence Diagram(s)

sequenceDiagram
  participant Session
  participant PublishFlow
  participant ReflexioAdapter
  participant ReflexioAPI
  Session->>PublishFlow: provide frozen unpublished interactions
  PublishFlow->>ReflexioAdapter: publish with request_id
  ReflexioAdapter->>ReflexioAPI: POST retrieved learning payload
  ReflexioAPI-->>ReflexioAdapter: publish result
  ReflexioAdapter-->>PublishFlow: success or fallback result
  PublishFlow->>Session: advance published watermark
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.15% 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 summarizes the main change: publishing retrieved learning links.
✨ 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 scripts/eval_link_report.py Outdated

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

🧹 Nitpick comments (4)
scripts/eval_link_report.py (1)

69-84: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Prefer line-by-line iteration over read_text().splitlines() for large archive and injected files.

Both archive_rows and the registry-reading loop in build_report load entire files into memory via read_text().splitlines(). Archive files for high-volume tables (e.g., interactions, requests) can grow significantly, risking OOM in constrained operator environments. Iterating line-by-line with open() is equally readable and avoids holding the full file in memory.

♻️ Proposed refactor for `archive_rows`
 def archive_rows(archive_dir: Path, table: str) -> list[dict[str, Any]]:
     path = archive_dir / f"{table}.jsonl"
     if not path.is_file():
         return []
     rows: list[dict[str, Any]] = []
-    for line in path.read_text().splitlines():
-        if not line.strip():
-            continue
-        try:
-            record = json.loads(line)
-        except json.JSONDecodeError:
-            continue
-        row = record.get("row") if isinstance(record, dict) else None
-        if isinstance(row, dict):
-            rows.append(row)
+    with path.open("r", encoding="utf-8") as fh:
+        for line in fh:
+            if not line.strip():
+                continue
+            try:
+                record = json.loads(line)
+            except json.JSONDecodeError:
+                continue
+            row = record.get("row") if isinstance(record, dict) else None
+            if isinstance(row, dict):
+                rows.append(row)
     return rows
♻️ Proposed refactor for registry reading in `build_report`
     registry_entries: list[dict[str, Any]] = []
     if sessions_dir.is_dir():
         for path in sessions_dir.glob("*.injected.jsonl"):
-            for line in path.read_text().splitlines():
-                try:
-                    entry = json.loads(line)
-                except json.JSONDecodeError:
-                    continue
-                if isinstance(entry, dict):
-                    registry_entries.append(entry)
+            with path.open("r", encoding="utf-8") as fh:
+                for line in fh:
+                    try:
+                        entry = json.loads(line)
+                    except json.JSONDecodeError:
+                        continue
+                    if isinstance(entry, dict):
+                        registry_entries.append(entry)

Also applies to: 197-206

🤖 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 `@scripts/eval_link_report.py` around lines 69 - 84, Update archive_rows and
the registry-reading loop in build_report to open their JSONL files and iterate
over them line by line instead of using read_text().splitlines(). Preserve the
existing blank-line skipping, JSON decoding, filtering, and row-processing
behavior while avoiding loading entire files into memory.
plugin/src/claude_smart/state.py (2)

254-256: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Dedup check is O(n) per entry against a growing list.

candidate in retrieved linearly scans the per-interaction list (up to _RETRIEVED_LEARNINGS_WIRE_CAP items), making the loop quadratic in the worst (capped) case. Bounded by the 1000-item cap so real-world impact is small, but a set of (wire_kind, real_id) tuples would make this O(1) with minimal code churn.

♻️ Optional dedup optimization
-    remaining: list[dict[str, Any]] = []
+    remaining: list[dict[str, Any]] = []
+    seen: set[tuple[str, str]] = set()
     for entry in [*pending, *injected_entries]:
         ...
         interaction = interactions[assistant_interaction_indexes[target]]
         retrieved = interaction.setdefault("retrieved_learnings", [])
         candidate = {"kind": wire_kind, "learning_id": real_id}
-        if candidate in retrieved:
+        key = (wire_kind, real_id)
+        if key in seen or candidate in retrieved:
             continue
         if attached_total >= _RETRIEVED_LEARNINGS_WIRE_CAP:
             truncated += 1
             continue
         retrieved.append(candidate)
+        seen.add(key)
         attached_total += 1

Note: seen must be scoped per interaction (or keyed by interaction index) if dedup should remain per-interaction rather than global.

🤖 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 `@plugin/src/claude_smart/state.py` around lines 254 - 256, Replace the linear
candidate membership check in the interaction retrieval loop with a
per-interaction set of `(wire_kind, real_id)` keys, and check/add each key as
candidates are processed. Keep the `seen` set scoped to each interaction so
deduplication remains local, while preserving the existing `retrieved` list
output and cap behavior.

187-272: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No direct unit tests for attach_retrieved_learnings/retrieved_learning_watermark in this file.

Coverage currently comes only indirectly through tests/test_publish.py. Given the complexity here (bisect-based eligibility, cap enforcement, kind resolution, pending carryover) and the malformed-retrieved_pending/mismatched-length edge cases discussed above, direct unit tests in tests/test_state.py would make regressions easier to pinpoint.

🤖 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 `@plugin/src/claude_smart/state.py` around lines 187 - 272, Add direct unit
tests in tests/test_state.py for attach_retrieved_learnings and the
retrieved_learning_watermark flow, covering bisect-based eligibility, wire-cap
enforcement, valid and invalid kind resolution, pending carryover, malformed
retrieved_pending values, and mismatched record/interaction lengths. Keep the
existing indirect publish tests unchanged and assert both mutated interactions
and returned watermark/pending state.
plugin/src/claude_smart/publish.py (1)

65-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Blind except Exception (Ruff BLE001) — likely intentional but worth acknowledging.

The broad catch matches the stated "best-effort metadata must never block a publish" intent, so it's a defensible use of a wide catch. As noted in the state.py review, this also means a partial mutation of interactions inside attach_retrieved_learnings can slip through silently before the offset bookkeeping is discarded — the try/except boundary alone doesn't guarantee atomicity of the mutation it wraps. If keeping the broad catch, consider a short inline comment/# noqa: BLE001 to signal the deviation is deliberate rather than an oversight.

🤖 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 `@plugin/src/claude_smart/publish.py` around lines 65 - 78, Keep the broad
exception handling around the best-effort retrieved-learning attachment, but
explicitly suppress Ruff BLE001 at the catch site or add a concise inline
comment documenting that the broad catch is deliberate because metadata failures
must not block publishing. Preserve the existing warning and publish flow.

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.

Inline comments:
In `@plugin/src/claude_smart/state.py`:
- Around line 199-261: The attach_retrieved_learnings flow currently mutates
interactions before all pending and injected entries are validated, allowing
partial updates when malformed data raises. Stage validated attachments and
duplicate/capacity decisions without mutating interactions, then commit the
staged retrieved_learnings changes only after processing the entire loop
succeeds; preserve the existing counters, skipping, truncation, and
target-assignment behavior.

---

Nitpick comments:
In `@plugin/src/claude_smart/publish.py`:
- Around line 65-78: Keep the broad exception handling around the best-effort
retrieved-learning attachment, but explicitly suppress Ruff BLE001 at the catch
site or add a concise inline comment documenting that the broad catch is
deliberate because metadata failures must not block publishing. Preserve the
existing warning and publish flow.

In `@plugin/src/claude_smart/state.py`:
- Around line 254-256: Replace the linear candidate membership check in the
interaction retrieval loop with a per-interaction set of `(wire_kind, real_id)`
keys, and check/add each key as candidates are processed. Keep the `seen` set
scoped to each interaction so deduplication remains local, while preserving the
existing `retrieved` list output and cap behavior.
- Around line 187-272: Add direct unit tests in tests/test_state.py for
attach_retrieved_learnings and the retrieved_learning_watermark flow, covering
bisect-based eligibility, wire-cap enforcement, valid and invalid kind
resolution, pending carryover, malformed retrieved_pending values, and
mismatched record/interaction lengths. Keep the existing indirect publish tests
unchanged and assert both mutated interactions and returned watermark/pending
state.

In `@scripts/eval_link_report.py`:
- Around line 69-84: Update archive_rows and the registry-reading loop in
build_report to open their JSONL files and iterate over them line by line
instead of using read_text().splitlines(). Preserve the existing blank-line
skipping, JSON decoding, filtering, and row-processing behavior while avoiding
loading entire files into memory.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a6cc658-9165-4b5e-be0c-e89506fcdf7a

📥 Commits

Reviewing files that changed from the base of the PR and between 540a338 and fcfa809.

📒 Files selected for processing (6)
  • plugin/src/claude_smart/publish.py
  • plugin/src/claude_smart/state.py
  • scripts/eval_link_report.py
  • tests/test_eval_link_report.py
  • tests/test_publish.py
  • tests/test_state.py

Comment thread plugin/src/claude_smart/state.py Outdated
Preserve retrieved-learning fields across the 0.2.28 client compatibility window, stage attachment changes atomically, and stream health-report inputs. Persist publish and linkage progress together to close the local crash window.

@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 (3)
scripts/eval_link_report.py (1)

68-70: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Static-analysis SQL-injection hint here is a false positive.

columns() (and the other f-string queries at Lines 64, 121, 149, 153) interpolate only table/column names sourced from the module constants RETENTION_TARGETS/LEARNING_IDS or hardcoded literals — never external input. SQLite also can't bind identifiers/PRAGMA targets as parameters, so an f-string is unavoidable here. Optionally, asserting table against a known allowlist before interpolation would silence the linter and add defense-in-depth.

🤖 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 `@scripts/eval_link_report.py` around lines 68 - 70, The SQL-injection warning
in columns and the related f-string queries is a false positive because
identifiers come only from trusted module constants or literals. Preserve the
current identifier interpolation, and optionally validate table/column values
against the known allowlist before executing the queries to provide defense in
depth and satisfy static analysis.

Source: Linters/SAST tools

tests/test_adapter.py (1)

286-311: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Decouple this test from Reflexio’s runtime schema. tests/test_adapter.py:286-311 depends on reflexio.models.api_schema.service_schemas.InteractionData.model_fields at runtime, so the branch under test changes with the reflexio-ai version present. Stub the import/helper here so the raw-request path stays deterministic.

🤖 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 `@tests/test_adapter.py` around lines 286 - 311, Decouple
test_pinned_client_sends_retrieved_learnings_without_model_stripping from the
installed Reflexio runtime schema by stubbing the InteractionData model-field
import or helper used by the adapter during this test. Configure the stub so the
raw-request path is selected deterministically, while preserving the existing
assertions for retrieved_learnings and the published request.
plugin/src/claude_smart/reflexio_adapter.py (1)

140-157: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Move the kwargs build into the non-raw branch. The raw publish path bypasses client.publish_interaction, so this block is unused there and just adds a bit of noise.

🤖 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 `@plugin/src/claude_smart/reflexio_adapter.py` around lines 140 - 157, Move the
construction of kwargs into the non-raw branch that calls
client.publish_interaction. Keep the raw _make_request path unchanged and ensure
kwargs is only built immediately before its client.publish_interaction(**kwargs)
use.
🤖 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 `@plugin/src/claude_smart/reflexio_adapter.py`:
- Around line 140-157: Move the construction of kwargs into the non-raw branch
that calls client.publish_interaction. Keep the raw _make_request path unchanged
and ensure kwargs is only built immediately before its
client.publish_interaction(**kwargs) use.

In `@scripts/eval_link_report.py`:
- Around line 68-70: The SQL-injection warning in columns and the related
f-string queries is a false positive because identifiers come only from trusted
module constants or literals. Preserve the current identifier interpolation, and
optionally validate table/column values against the known allowlist before
executing the queries to provide defense in depth and satisfy static analysis.

In `@tests/test_adapter.py`:
- Around line 286-311: Decouple
test_pinned_client_sends_retrieved_learnings_without_model_stripping from the
installed Reflexio runtime schema by stubbing the InteractionData model-field
import or helper used by the adapter during this test. Configure the stub so the
raw-request path is selected deterministically, while preserving the existing
assertions for retrieved_learnings and the published request.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 56d22196-566f-4ad9-b7af-e04bd6127f05

📥 Commits

Reviewing files that changed from the base of the PR and between fcfa809 and 4525973.

📒 Files selected for processing (6)
  • plugin/src/claude_smart/publish.py
  • plugin/src/claude_smart/reflexio_adapter.py
  • plugin/src/claude_smart/state.py
  • scripts/eval_link_report.py
  • tests/test_adapter.py
  • tests/test_state.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/test_state.py
  • plugin/src/claude_smart/publish.py
  • plugin/src/claude_smart/state.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.

🧹 Nitpick comments (1)
plugin/dashboard/app/dashboard/page.tsx (1)

41-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import ArchiveStatus from @/lib/archive-status instead of redefining it.

The interface is already exported from archive-status.ts (line 14). Redefining it here risks drift if fields change. Use import type — it's erased at compile time, so no node:fs/node:os modules will leak into the client bundle.

♻️ Proposed refactor
 import type {
   AgentPlaybook,
   PlaybookApplicationStat,
   SessionSummary,
   UserPlaybook,
   UserProfile,
 } from "`@/lib/types`";
+import type { ArchiveStatus } from "`@/lib/archive-status`";

Then remove the local interface definition:

-interface ArchiveStatus {
-  enabled: boolean;
-  entryCount: number;
-  sizeBytes: number;
-  warningBytes: number;
-  exceeded: boolean;
-}
-
 export default function DashboardPage() {
🤖 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 `@plugin/dashboard/app/dashboard/page.tsx` around lines 41 - 47, Replace the
local ArchiveStatus interface in the dashboard page with a type-only import from
"`@/lib/archive-status`", and remove the duplicate definition. Keep all existing
ArchiveStatus usages unchanged.
🤖 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 `@plugin/dashboard/app/dashboard/page.tsx`:
- Around line 41-47: Replace the local ArchiveStatus interface in the dashboard
page with a type-only import from "`@/lib/archive-status`", and remove the
duplicate definition. Keep all existing ArchiveStatus usages unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e216a2b-e3d7-4f7d-adca-7e20dd5b22ef

📥 Commits

Reviewing files that changed from the base of the PR and between 4525973 and 829745c.

📒 Files selected for processing (5)
  • plugin/dashboard/app/api/archive-status/route.ts
  • plugin/dashboard/app/dashboard/page.tsx
  • plugin/dashboard/components/common/stat-card.tsx
  • plugin/dashboard/lib/archive-status.ts
  • plugin/dashboard/lib/format.ts

@wenchanghan

Copy link
Copy Markdown
Contributor Author

Review findings (critical pass against the workstream design; the touched suites are green at head — 102 passed locally).

1. Every publish takes the raw _make_request path — retrieved_learnings: [] is stamped on all Assistant turns.
attach_retrieved_learnings writes the key even when nothing attaches (verified: zero injected entries still yields {"retrieved_learnings": []} on the turn), and _needs_raw_retrieved_learning_publish checks key presence, not truthiness. Net effect: the typed publish_interaction path is dead code until the vendor bump, and the raw path silently skips the client's post-publish cache invalidation (get_profiles / get_agent_playbooks) — harmless today only because hook processes are short-lived. One-line fix: set the key only when non-empty (or check truthiness in _needs_raw_retrieved_learning_publish), so the raw path fires only when there is something to carry.

2. Snapshot fields are not clamped to server caps → poison-pill publish after the vendor bump.
Server-side caps are title ≤ 1000, content ≤ 1 MB, trigger/rationale ≤ 10 K, while the injection registry is uncontrolled input (historical files, arbitrary sizes). One oversized field → 422 on the raw path (or client-side ValidationError on the typed path post-bump) → the publish fails and retries forever, and that session's interactions never publish. Low probability, high impact; a few-line clamp where the snapshot candidate is built closes it permanently.

3. Minor: dashboard threshold parsing diverges from the backend.
warningBytes uses Number(...), so "1e9" is accepted; Python's int("1e9") raises and falls back to 10 GiB — the tile can display a different threshold than retention enforces. Mirroring the integer-only parse keeps the two in agreement.

Decision note: removing scripts/eval_link_report.py stands (checked-in code needs an owner; this was a rollout instrument). Link-health validation will instead be executed ad-hoc and recorded at the three rollout checkpoints — pre-vendor-bump, post-bump, post-archive-enable. The script remains recoverable at fcfa809 if needed.

Also verified good in this pass: byte-offset watermark + pending list persisted atomically with published_up_to; staged attachment (no partial mutation on failure, test-proven); partial-final-line offset rewind in read_injected_entries; dedup keeps earliest; both wire caps enforced; the raw path preserves wait_for_response=False semantics (query param, omitted on both paths).

🤖 Generated with Claude Code

@wenchanghan

Copy link
Copy Markdown
Contributor Author

Incorporated the latest review in 478f6cd:

  • Assistant turns with no attached learnings no longer get retrieved_learnings: [], so normal publishes stay on the typed client path;
  • snapshot fields are clamped to server limits, with a 10 MiB aggregate guard that omits excess snapshots loudly instead of poison-pill retrying forever;
  • dashboard threshold parsing now matches Python integer parsing (1e9 falls back to 10 GiB);
  • the dashboard imports the shared ArchiveStatus type;
  • the raw-path test is deterministic and no longer depends on the installed Reflexio schema;
  • raw-only kwargs construction was removed from the normal path.

The one-off report remains removed as agreed. Full suite: 659 passed; dashboard lint/build passed.

@wenchanghan

Copy link
Copy Markdown
Contributor Author

Re-review of the follow-up commits (478f6cd..df73152).

All three prior findings are resolved, each with a regression test:

  1. Empty-list stampingretrieved_learnings is now set only when non-empty (popped otherwise), and _needs_raw_retrieved_learning_publish checks truthiness, so link-free publishes stay on the typed client path. Locked by test_publish_without_injected_learnings_keeps_typed_payload.
  2. Snapshot clamping — per-field clamps match the server caps exactly (1000 / 100 000 / 10 000 / 10 000) plus a 10 MiB aggregate budget mirroring the new server-side validator; over-budget snapshots degrade to ID-only links (with a warning) instead of dropping the link or poisoning the publish. Locked by test_snapshot_fields_are_clamped_to_server_caps.
  3. Threshold parsing — integer-only regex now matches the Python parse, and the tile is constant-cost (stat-only; countEntries streaming removed).

Also improved beyond the findings: attached_total now counts pre-staged entries toward the publish cap (matches the server validator's accounting), which made the per-interaction cap check redundant — correctly removed, since the publish cap is ≤ the per-interaction cap.

One non-blocking observation: once FIFO eviction starts on the reflexio side, archive size sits at ≈100 % of the ceiling permanently, so the tile will be red ("Storage ceiling reached") in steady state. If red is meant to signal "oldest evidence is rotating out," fine; if red should mean "action needed," consider amber for the at-ceiling steady state and reserving red for archive write failures — which, after the fail-open change upstream, are now the silent evidence-loss mode worth surfacing.

Verified locally at df73152: 657 passed; the 2 failures (test_npx_install_reads_managed_env, test_opencode_dist_matches_typescript_sources) are environmental in my sandbox (npm subprocess exit 127) and live in files this PR does not touch.

Ready for lead review from my side.

🤖 Generated with Claude Code

@wenchanghan wenchanghan changed the title feat: link injected learnings to published sessions feat: publish retrieved learning links Jul 12, 2026
@wenchanghan

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@wenchanghan

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@yyiilluu yyiilluu merged commit 8ca1bb6 into ReflexioAI:main Jul 12, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants