Skip to content

chore: catch fork main up to upstream v0.21.0-rc2 (47 commits behind) - #220

Merged
100yenadmin merged 49 commits into
mainfrom
chore/catch-up-upstream-v0.21.0-rc2
Aug 7, 2026
Merged

100yenadmin merged 49 commits into
mainfrom
chore/catch-up-upstream-v0.21.0-rc2

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Aug 7, 2026 •

Copy link
Copy Markdown
Owner

Why this exists

Fork main had never merged upstream. Its merge history is entirely internal (r1/r2 fix rounds, PR ports) — no upstream/main merge anywhere. Contribution flowed the other way: upstream b6288eb merged our own 100yenadmin/upstream-wave-1 (stephenschoettler#436). Nothing ever came back down.

The Teams branch merged upstream/main directly, which made the fork look current. Only the branch was. main sat 47 behind / 56 ahead.

What it brings

Fork-only work is preserved: 48 commits by patch-id, mostly docs(bench) plus session_expand_v1, fts_prose_mode, and profile-scoped trajectory embeddings.

Conflicts: 13 files, 45 hunks — how each was resolved

git merge-tree reported clean; the real merge did not. (My dry-run grepped for <<<<<<< markers, which that output form doesn't emit — the predicate was wrong, not the merge.)

11 of 13 files took the resolution from teams/lcm-teams-v1. That branch already merged these exact two sides at 65df063, and those 11 files carry zero Teams tokens there — so its version is the correct non-Teams resolution, already gate-verified (battery 21/21, default-off A/B 22/22).

The 2 Teams-bearing files were resolved by hand:

  • vector_store.py (4 hunks) — kept fork main's _scanned_knn_result helper over upstream's inline KNNResult construction. Verified the helper subsumes upstream's logic (same scanned/total policy; deadline_expired where upstream said stopped_early), and that fork main's helper is byte-identical to the gate-verified one on the Teams branch.
  • tools.py (1 hunk) — took upstream's session_dates=getattr(engine, "_session_occurrence_dates", None) over fork main's engine=engine. Confirmed against the callee: reasoning.py:889 accepts session_dates, not engine. Keeping our side here would have been a TypeError at runtime.

Teams stays parked

Verified on the merge result: 0 occurrences of access_scope / TeamsPolicy / teams_enabled_v1, and no teams/, access_context/, or access_policy/ directories. This changes nothing about #215, which remains deliberately unmerged.

Verification

  • Whole tree compiles under 3.13.
  • Local suite gives no signal: this laptop reports 57 collection errors (cannot import name 'LCMConfig') — and the pre-merge baseline reports the identical 57. Environment artifact, unchanged by the merge, and it's a collection failure so it proves nothing either way. CI across 3.11–3.14 is the gate here.

Deployment impact: none

No customer consumes this fork. evaos-runtime.json pins owner_repo: stephenschoettler/hermes-lcm at v0.19.0 @ df9239ef with pin_mode: git_ref_and_commit. Verified on David Dorman's box today — the installed tree carries that exact commit and tree hash.

Summary by CodeRabbit

  • New Features

    • Added asynchronous, non-blocking background maintenance for improved startup responsiveness.
    • Improved evidence and retrieval accuracy with historical cutoffs, temporal grounding, semantic validation, and safer handling of incomplete results.
    • Added Unicode-safe token chunking with strict token-budget enforcement.
  • Bug Fixes

    • Improved SQLite startup resilience during lock contention.
    • Prevented stale retrieval results and unsafe assertion-state reporting.
    • Strengthened numeric, date, negation, and schema validation.
  • Documentation

    • Updated upgrade, configuration, verification, packaging, changelog, and release documentation for v0.21.0-rc2.

stephenschoettler and others added 30 commits July 23, 2026 16:03
Resolves PR stephenschoettler#436's CONFLICTING state. Upstream main was only ONE commit ahead — 49e99a2 'chore: prepare
v0.20.0 release candidate' — and only two files genuinely conflicted, both documentation.

Both resolved as a UNION, not by picking a side:
- upstream's tool list is the accurate one (it adds lcm_recall and lcm_recent, which DO exist in wave-1's code:
  31 and 20 non-doc files respectively — they were simply missing from wave-1's docs);
- wave-1's skill-discovery lines are additional accurate content upstream had dropped.
So the merged docs list all ten tools AND retain the plugin-qualified skill-loading note.

Version strings take upstream's v0.20.0 throughout (plugin.yaml, README, docs/packaging.md, tests).
No code conflicts — wave-1's 259 commits and upstream's release-candidate bump touched disjoint code.
…phenschoettler#361)

Converting a rollback-journal database to WAL needs the exclusive lock,
and SQLite can return SQLITE_BUSY for that upgrade without consulting
the busy handler while sibling connections are mid-setup on the same
file. Concurrent process startup (gateway + CLI + sub-agents) on a
not-yet-WAL database therefore crashed sporadically with 'database is
locked' from configure_connection.

Wrap the journal_mode pragma in a bounded exponential-backoff retry
(budget = SQLITE_BUSY_TIMEOUT_MS) and set busy_timeout first. Once the
database is in WAL mode the pragma is a plain read, so steady state is
unaffected.

Also harden the concurrent-migration regression test that exposed this:
its barrier had no timeout, so the pre-barrier failure parked the seven
surviving threads forever and deadlocked the whole test run instead of
reporting the error. The barrier now times out and aborts on failure,
join is bounded, and the assert surfaces the root-cause exception.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
tests/test_tokens_encoder_loading.py::test_encoder_adopted_after_background_load
fails intermittently (observed on CI py3.13 while 3.11/3.12/3.14 passed) with
  assert started.wait(timeout=2.0)  ->  False
i.e. the patched loader was never CALLED, not that it was slow.

Root cause is a cross-test race, not timing. test_count_tokens_does_not_block_on_hung_loader
leaves its loader thread parked in release.wait(timeout=30); that thread outlives the test
body. _encoder_loader() sets _encoder_ready = True even when _load_encoder() raises -- which
is deliberate, a failed/hung tiktoken fetch must not be retried on every count_tokens() call --
so when the leftover thread finally wakes and raises, it re-marks the encoder ready. If that
write lands AFTER the next test's autouse reset, _get_encoder() short-circuits at
'if _encoder_ready: return _encoder' and never spawns a loader, so the next test's patched
loader never runs and its start event never fires.

Fix is test-only: join any live encoder thread in the reset fixture's teardown, so no loader
thread can outlive the test that started it. No production behaviour changes -- setting
_encoder_ready on failure is the intended no-retry contract.

NOT YET VERIFIED LOCALLY: a Phase 1A latency measurement is using the machine and running the
suite would contend with it. Verify on py3.13 before this is pushed anywhere.
Conflicts resolved:
- README/operator-guide tool count: both sides stale (10/8); resolved to 15
  per the merged get_tool_schemas()
- CHANGELOG: upstream's cut v0.20.0 (07-23) entry stands; the train's work
  moved under Unreleased as the consolidated release entry (version number
  is the maintainer's call)
Also: fork-review working docs relocated from repo root to
bench/release-kit/review-logs/ (transparency kept, root kept clean).
- evidence_compiler: finite coverage counts DISTINCT grounded exact_refs
- query_view: hit CAS verifies generation/rowcount + re-reads readiness
- adaptive_retrieval: persisted slot refs intersect the selected set
- trajectory: schema version rejected before repair DDL runs
- vector_store: deadline-expired paths skip COUNT(*); totals stay
  meaningful from the enumerated candidate set

All validated against source before fixing (triage: 5 mechanical of 14
real findings; the 9 architecture items are decided/tracked separately —
see bench/DECISIONS-R3-UPSTREAM-ARCH.md on the fork docs branch for the
first three). Full suite exact-name parity; 9 focused regressions.
…y remaining unguarded site of the lease-release class (upstream review batch 2, comment 3673094016; pattern matches evidence_compiler/rollup_builder)
…-maintenance

fix: defer temporal rollup maintenance from session start
…r#440) into upstream-wave-1

One conflict, engine.py module scope: both sides added independent
definitions (wave's assertion-extraction process slot / main's
_RollupMaintenanceScheduler) — resolution keeps both verbatim.
Full suite on the merged tree: 2723 passed; the 35 failures are the
known environment-sensitive baseline (packaging/focus/host/path),
distribution identical pre/post merge, none in merge-touched surfaces.
100yenadmin and others added 18 commits August 2, 2026 03:04
…ve-1

[wave 1 · R2] Trajectory/experience-memory subsystem + scaling fixes + citable delivery + benchmark evidence
…se/v0.21.0-rc1-prep

chore: prepare v0.21.0-rc1 release candidate
…pr491-backup-recall-contract

docs: correct backup and pre-answer provider contracts
…jk-safe-tiktoken-chunking

fix: preserve Unicode boundaries in tiktoken state chunks
…se/v0.21.0-rc2

chore: prepare v0.21.0-rc2 release candidate
Fork main had never pulled upstream down. Contribution flowed the other
way -- upstream b6288eb merged our own stephenschoettler#436 wave -- so main sat 47 behind
while the Teams branch had already merged upstream directly and looked
current. Only the branch was.

Brings v0.20.0 + v0.21.0-rc1/rc2 release prep, Tosko4's async-rollup
maintenance train (stephenschoettler#440), CJK-safe tiktoken chunking (stephenschoettler#492), and the WAL
lock-contention retry (stephenschoettler#361), plus our stephenschoettler#436 wave in upstream's shape.

Auto-merges clean: 20 files touched on both sides, zero conflicts.

# Conflicts:
#	adaptive_retrieval.py
#	query_view_store.py
#	reasoning.py
#	requirements_compiler.py
#	tests/test_adaptive_retrieval.py
#	tests/test_evidence_contract.py
#	tests/test_query_view_store.py
#	tests/test_trajectory_state_semantic_expansion.py
#	tests/test_trajectory_store.py
#	tests/test_vector_store.py
#	tools.py
#	trajectory_store.py
#	vector_store.py
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR updates release delivery for curated RC notes, revises evidence and reasoning behavior for trusted dates and bounded arithmetic, hardens assertion and retrieval consistency, moves rollup maintenance to an asynchronous leased scheduler, tightens trajectory schema and chunking checks, and adds validation and regression coverage.

Changes

Release and runtime behavior

Layer / File(s) Summary
Curated release delivery and upgrade documentation
.github/workflows/release.yml, .github/release-notes/*, CHANGELOG.md, README.md, docs/*, plugin.yaml, .github/ISSUE_TEMPLATE/bug_report.yml, tests/test_release_workflow.py, tests/test_lcm_*, tests/test_packaging_install.py
The release workflow now requires tag-specific curated notes and derives prerelease/latest flags from the tag. Version references, changelog entries, release notes, plugin metadata, upgrade guidance, and release-surface tests now target v0.21.0-rc2.
Temporal evidence and numeric reasoning
__init__.py, answer_contract.py, evidence_pack.py, store.py, reasoning.py, requirements_compiler.py, selective_compiler.py, tools.py, tests/test_answer_contract.py, tests/test_evidence_*, tests/test_reasoning.py, tests/test_selective_compiler.py, tests/test_packaging_install.py
Evidence grounding now uses trusted session dates and historical cutoffs. Reasoning adds bounded Decimal arithmetic, stricter temporal and cardinality checks, named-sum and distinct-count validation, ordinal projection, negation rejection, and optional baseline-context rendering.
Assertion and retrieval consistency
adaptive_retrieval.py, query_view_store.py, assertion_extraction.py, assertion_state.py, tools.py, tests/test_adaptive_retrieval.py, tests/test_assertion_*
Retrieval state now captures a corpus snapshot and reuses it when publishing query views. Assertion extraction now requires exact cited-span support and explicit state-change cues, while truncated relation results surface unknown lifecycle and active-state values.
Asynchronous rollup maintenance and concurrency
engine.py, command.py, db_bootstrap.py, escalation.py, tests/test_rollup_*, tests/test_crash_safe_wal.py, tests/test_lcm_core.py, tests/test_tokens_encoder_loading.py, tests/test_embedding_backfill.py, tests/test_vector_store.py
Rollup maintenance now runs through a process-wide bounded scheduler with deduplication, follow-up passes, leases, and owner-scoped draining. Operator rebuilds respect maintenance leases, WAL conversion retries lock contention, and spend reservations become atomic under concurrency.
Trajectory schema and UTF-8 chunking
trajectory_store.py, tests/test_trajectory_*
Trajectory store initialization now distinguishes legacy unversioned corpora from malformed schemas and validates full read-only schema objects. Token chunking now rejects nonpositive budgets and splits only at Unicode-safe boundaries that stay within the token budget.
Validation and review records
FINDINGS-VERDICTS-UP1.md, bench/release-kit/review-logs/*
The repository adds review packets, regression reports, and findings verdict documents that record validated fixes, evidence artifacts, and focused or full test outcomes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Session
  participant LCMEngine
  participant RollupScheduler
  participant SummaryDAG

  Session->>LCMEngine: bind lifecycle session
  LCMEngine->>RollupScheduler: schedule rollup maintenance
  RollupScheduler->>SummaryDAG: run maintenance with private DAG
  SummaryDAG-->>RollupScheduler: completion or failure
  RollupScheduler-->>LCMEngine: owner/key drain state
Loading

Possibly related PRs

  • 100yenadmin/hermes-lcm#202: Overlaps with claim_build, retrieval snapshot handling, reasoning changes, rollup scheduling, release workflow, and related tests.
  • 100yenadmin/hermes-lcm#190: Touches the same reasoning, temporal grounding, retrieval, finite-event evidence, and trajectory-store paths.
  • 100yenadmin/hermes-lcm#175: Shares trajectory_store.py token-budget and chunking behavior, plus matching regression coverage.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: synchronizing the fork's main branch with upstream through v0.21.0-rc2.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/catch-up-upstream-v0.21.0-rc2

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

…gine

CI caught this on all four Python versions -- 3 failures in the compute
anchor/sidecar path (test_reasoning, test_lcm_recall), each falling back
instead of computing.

I resolved the tools.py conflict as either/or and took upstream's
`session_dates=`. It is a UNION: fork main added `engine=`, upstream added
`session_dates=`, and `ground_evidence` (reasoning.py:1197) declares both.
Dropping `engine=` cost the sidecar its occurrence-time resolution, so
compute reported 'fallback'.

The keep-ours/keep-theirs method is what produced this. Re-audited the
other two ground_evidence callers (evidence_pack.py:764,
requirements_compiler.py:1213/1932) against the gate-verified resolution
on teams/lcm-teams-v1: both byte-identical, no other drops.
@100yenadmin

Copy link
Copy Markdown
Owner Author

Sign-off

CI green on e422efa, all six jobs: lint · workflow-lint · test (3.11) · test (3.12) · test (3.13) · test (3.14). This is the first fully-green single-commit run this fork has had in a while — every job on one SHA, none cancelled.

What CI earned here

The first push failed on all four Python versions — 3 failures in the compute anchor/sidecar path. I had resolved the tools.py conflict as either/or and taken upstream's session_dates=, when it was a union: fork main contributed engine=, upstream contributed session_dates=, and ground_evidence (reasoning.py:1197) declares both. Dropping engine= cost the sidecar its occurrence-time resolution and compute reported fallback.

Local testing could not have caught this. This laptop reports 57 collection errors (cannot import name 'LCMConfig') and the pre-merge baseline reports the identical 57 — an environment artifact that makes the local suite blind in both directions.

After the fix I re-audited the other two ground_evidence callers (evidence_pack.py:764, requirements_compiler.py:1213/1932) against the gate-verified resolution on teams/lcm-teams-v1 — both byte-identical, no other drops.

Resolution audit

  • 11 of 13 conflicted files took the teams/lcm-teams-v1 resolution verbatim. That branch already merged these exact two sides at 65df063, and carries zero Teams tokens in those 11 files — so its version is the correct non-Teams merge, already gated (battery 21/21, default-off A/B 22/22).
  • vector_store.py (4 hunks) kept fork main's _scanned_knn_result helper, verified byte-identical to the gated one and semantically subsuming upstream's inline construction. Diffed against the Teams branch afterwards: every remaining difference is an access_scope column or an owner-predicate comment — i.e. Teams-only, no dropped upstream work.
  • tools.py (1 hunk) — the union above.

Teams stays parked

0 occurrences of access_scope / TeamsPolicy / teams_enabled_v1 on the merge result; no teams/, access_context/, access_policy/. Nothing here changes #215.

Deployment impact: none

No customer consumes this fork. Verified on a live box today: evaos-runtime.json pins owner_repo: stephenschoettler/hermes-lcm at v0.19.0 @ df9239ef, and the installed tree carries that exact commit and tree hash.

Merging.

@100yenadmin
100yenadmin merged commit 0dce04d into main Aug 7, 2026
6 of 7 checks passed
@100yenadmin
100yenadmin deleted the chore/catch-up-upstream-v0.21.0-rc2 branch August 7, 2026 15:12
100yenadmin pushed a commit that referenced this pull request Aug 7, 2026
…m-teams-v1

main just caught up to upstream (#220) after never having done so. Pulling
it back in immediately so integration debt does not re-accumulate -- the
last time this branch let it build, the merge was 52 hunks through the
retrieval core and hid a leak in code that never conflicted.

Expected to be near-trivial: the branch already contained both parents'
content, and #220's own resolutions were taken FROM this branch.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
requirements_compiler.py (1)

1213-1219: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pass engine to ground_evidence.

Line 1213 omits engine=engine. The finite-enumeration path and tools.py pass it. Engine-dependent grounding can fail on the normal sum, difference, date-interval, and order paths.

Proposed fix
     grounding = ground_evidence(
         raw_operands,
         messages=engine._store,
         assertions=getattr(engine, "_assertions", None),
         as_of=question_date_as_of_epoch(contract.question_as_of),
         session_dates=getattr(engine, "_session_occurrence_dates", None),
+        engine=engine,
     )
🤖 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 `@requirements_compiler.py` around lines 1213 - 1219, Update the
ground_evidence call in the relevant requirements compilation path to pass the
current engine via engine=engine, matching the finite-enumeration path and
tools.py. Preserve all existing operands, messages, assertions, date, and
session-date arguments.
reasoning.py (1)

1495-1501: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Recheck order hides the empty-window reason.

The cardinality recheck at lines 1495-1497 runs before the if not selected check at line 1498. When a plan declares exact_operands and the temporal filter removes every operand, the caller receives "date_filter requires exactly N operands" instead of the more specific "no grounded evidence falls inside the resolved date window".

Both are correct fallbacks. The second is more actionable. Consider moving the empty check first.

🤖 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 `@reasoning.py` around lines 1495 - 1501, Move the `if not selected` fallback
check before the `_cardinality_error(plan, selected)` recheck so an empty
temporal-filtered selection returns “no grounded evidence falls inside the
resolved date window”; preserve the cardinality fallback for non-empty
selections.
assertion_state.py (1)

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

Extract the relation-limit constant.

relations_truncated compares against the literal 500, which must stay equal to the limit=500 passed at line 115. The two literals now drive a published correctness contract: if one changes and the other does not, truncation is either never detected or always reported.

Define one module constant and use it in both places.

♻️ Single source for the relation limit
+_MAX_RELATIONS_PER_STATE_QUERY = 500
     raw_relations = (
-        store.query_relations(assertion_ids=ids, as_of=as_of, limit=500)
+        store.query_relations(
+            assertion_ids=ids, as_of=as_of, limit=_MAX_RELATIONS_PER_STATE_QUERY
+        )
         if ids
         else []
     )
-    relations_truncated = len(raw_relations) == 500
+    relations_truncated = len(raw_relations) == _MAX_RELATIONS_PER_STATE_QUERY

reasoning.py line 925 passes the same limit=500 and should import this constant too.

🤖 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 `@assertion_state.py` at line 119, Define a module-level relation-limit
constant in assertion_state.py, then replace both the limit argument near
relations retrieval and the relations_truncated comparison in the relevant
function with that constant. Export or expose the constant for reasoning.py to
import and use in its corresponding limit=500 call, ensuring all relation limits
share one source of truth.
🤖 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 `@assertion_extraction.py`:
- Around line 538-554: Document the intentional source-versus-context
distinction at the _source_supports_assertion_value call site in the assertion
candidate flow: commitment cues may be found in the full message context, while
cancellation cues and token matches must be found within the cited source span.
Add a short comment without changing the existing arguments or validation
behavior.
- Around line 465-478: Update _source_supports_assertion_value to accept a depth
parameter with a small maximum bound, and reject values once that bound is
exceeded using the existing malformed-input validation path. In both Mapping and
sequence branches, pass depth=depth + 1 to recursive calls while preserving
current value checks and behavior for inputs within the limit.
- Around line 484-485: The completed branch in _source_supports_assertion_value
must validate the source span rather than unconditionally returning true.
Require an explicit completion cue in the span, matching the existing
cancellation validation pattern, while preserving the current kind restrictions
for action, event, and status.

In `@assertion_state.py`:
- Around line 181-182: Apply the truncation-aware unknown contract to conflict
fields: update the type of conflict_assertion_ids, set both unresolved_conflict
and conflict_assertion_ids to null when relations are truncated, and preserve
their current derived values otherwise. Update the corresponding lcm_query_state
handling in tools.py, including the guards near active_assertion_ids, so null
conflict fields are emitted safely.

In `@engine.py`:
- Around line 322-344: Restructure the worker loop around the job execution and
follow-up handling so the active-key cleanup always runs in a finally block,
including when job() raises a BaseException such as SystemExit. Ensure
_active_keys.discard(key), _running_follow_up reset,
_release_key_owners_if_idle_locked(key), and condition notification execute on
every exit path, while preserving follow-up job processing when available.

In `@FINDINGS-VERDICTS-UP1.md`:
- Around line 1-2: Add a blank line between the FINDINGS-VERDICTS-UP1 heading
and the Source head list item to satisfy Markdown heading-spacing requirements.

In `@README.md`:
- Around line 198-200: Update the README verification checklist’s tool list to
include all 15 tools, adding lcm_query_state, lcm_compute, lcm_compile_evidence,
lcm_evidence_pack, and lcm_retrieve to the existing entries. Ensure the
checklist and the referenced sample-output count remain consistent with the
complete list, matching docs/operator-guide.md.

In `@reasoning.py`:
- Around line 654-662: In the ordinal handling around _requested_order_ordinal,
reject contradictory plans at compile time when exact_operands is set and
order_index is greater than or equal to that exact count. Ensure this validation
reports the impossible ordinal through the appropriate cardinality/order error
path instead of allowing _cardinality_error to report only the smaller operand
count.
- Around line 803-815: Define a module-level compiled pattern named
_WORD_NUMBER_RE from _WORD_NUMBERS with re.IGNORECASE, then replace the inline
word-number pattern construction in both _explicit_numbers and the shown
mentions logic with _WORD_NUMBER_RE. Preserve the existing finditer behavior and
word-number lookup.
- Around line 1580-1644: Reduce the Decimal context precision used during unit
conversion in the arithmetic flow around _apply_arithmetic, rather than applying
_MAX_NUMERIC_DIGITS to the entire with localcontext() block. Preserve sufficient
precision for the required six-decimal reporting and bounded-result checks,
while avoiding expansion of non-terminating conversion ratios to 1000
significant digits.
- Around line 537-549: Update the regex in _requested_order_ordinal to use
re.IGNORECASE, ensuring ordinal detection works consistently for raw or
normalized question text without changing the existing matching behavior.
- Around line 1133-1139: In the source_session_day branch, keep
source_observed_at unchanged and store the synthesized end-of-day timestamp in a
distinct local variable before performing the as_of comparison. Also reuse a
small helper for this end-of-day boundary calculation and comparison in both the
source and occurrence session-date branches, preserving their existing return
behavior.
- Around line 532-534: Restrict the bare-currency fallback in the result-unit
detection logic to questions that name no competing unit. Update the `re.search`
branch returning `"usd"` so incidental `$` or “dollars” mentions do not override
computable units such as hours, while preserving explicit currency-result
detection and the existing `None` fallback.

In `@selective_compiler.py`:
- Around line 132-145: Add a concise comment immediately before the `return
max(candidates) if candidates else None` statement in `_ref_observed_at`,
documenting that the maximum across store, raw, and date-derived timestamps
enforces the latest-signal cutoff and must not be changed to `min` or
first-match.
- Around line 114-131: Update the store hydration lookup in the compiler around
store.get to catch sqlite3.Error, while retaining the existing safe fallback of
treating failed lookups as no stored value. Remove or preserve conversion guards
only as appropriate, but ensure database failures from MessageStore.get do not
escape the selective compilation path or fail the request.

In `@tests/test_assertion_extraction_state.py`:
- Around line 460-499: Add a positive case to
test_state_changing_relation_requires_semantic_cue_in_exact_quote using a quote
such as “Actually I prefer coffee now” that matches the supersedes cue pattern,
then call parse_assertion_extraction with the supersedes relation and assert the
result contains exactly one relation; retain the existing rejection case for the
cue-less quote.
- Around line 401-410: Remove the vacuous store.query_assertions assertion, or
update the test to invoke the full store.publish_source persistence path and
then verify that the expected ValueError leaves no assertion rows for
person:alice.
- Around line 502-539: Extend
test_relation_truncation_marks_lifecycle_and_active_state_unknown to assert the
truncation behavior for conflict fields: verify unresolved_conflict is unknown
and conflict_assertion_ids is unset or otherwise marked indeterminate according
to the chosen contract. Update assertion_state.py so these fields are not
published as definite when relations_truncated is true, while preserving their
existing values for complete relation results.

In `@tests/test_reasoning.py`:
- Around line 913-933: Expand test_how_long_ago_uses_one_anchored_operand to
verify the bare question sets plan.interval_unit_fixed to False, then add
fixed-unit coverage for “in years?” asserting interval_unit_fixed is True and
interval_unit is “year”. Also assert that the bare question reports a multi-year
interval in years and a three-week interval in weeks, preserving the existing
anchored-operand setup.

In `@tests/test_rollup_builder.py`:
- Around line 463-467: Widen the asynchronous timing thresholds in
tests/test_rollup_builder.py at lines 463-467 and 815-819 from 0.15 seconds to
1.0 second: update the bind_elapsed assertion after engine.on_session_start and
the shutdown_elapsed assertion in the corresponding shutdown test, preserving
the existing timing checks and regression coverage.

In `@tests/test_selective_compiler.py`:
- Around line 266-279: Add a one-line comment immediately before the direct SQL
update in test_historical_cutoff_hydrates_trusted_store_timestamp explaining
that it intentionally leaves observed_at NULL to exercise the timestamp fallback
in _ref_observed_at, rather than using _evidence(observed_at=...). Preserve the
existing private connection access and test setup.

In `@trajectory_store.py`:
- Around line 641-665: Restrict the fallback around the schema-version lookup to
genuine missing-column cases only; do not treat lock or other
sqlite3.OperationalError failures as legacy tables. Update the logic near the
lcm_trajectory_corpora schema check to distinguish the absent schema_version
column before returning, while propagating unrelated database errors unchanged,
and add a regression test covering a non-schema OperationalError such as a lock.

---

Outside diff comments:
In `@assertion_state.py`:
- Line 119: Define a module-level relation-limit constant in assertion_state.py,
then replace both the limit argument near relations retrieval and the
relations_truncated comparison in the relevant function with that constant.
Export or expose the constant for reasoning.py to import and use in its
corresponding limit=500 call, ensuring all relation limits share one source of
truth.

In `@reasoning.py`:
- Around line 1495-1501: Move the `if not selected` fallback check before the
`_cardinality_error(plan, selected)` recheck so an empty temporal-filtered
selection returns “no grounded evidence falls inside the resolved date window”;
preserve the cardinality fallback for non-empty selections.

In `@requirements_compiler.py`:
- Around line 1213-1219: Update the ground_evidence call in the relevant
requirements compilation path to pass the current engine via engine=engine,
matching the finite-enumeration path and tools.py. Preserve all existing
operands, messages, assertions, date, and session-date arguments.
🪄 Autofix

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8081adc2-e448-4b6a-8919-46a36dcd6374

📥 Commits

Reviewing files that changed from the base of the PR and between 7625b68 and e422efa.

📒 Files selected for processing (54)
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/release-notes/v0.21.0-rc1.md
  • .github/release-notes/v0.21.0-rc2.md
  • .github/workflows/release.yml
  • CHANGELOG.md
  • FINDINGS-VERDICTS-UP1.md
  • README.md
  • __init__.py
  • adaptive_retrieval.py
  • answer_contract.py
  • assertion_extraction.py
  • assertion_state.py
  • bench/release-kit/review-logs/DELTA-REVIEW-PACKET.md
  • bench/release-kit/review-logs/FINDINGS-VERDICTS-R2.md
  • bench/release-kit/review-logs/FINDINGS-VERDICTS-R3.md
  • bench/release-kit/review-logs/FINDINGS-VERDICTS.md
  • bench/release-kit/review-logs/REGRESSION-REPORT.md
  • bench/release-kit/review-logs/REVIEW-PACKET.md
  • command.py
  • db_bootstrap.py
  • docs/operator-guide.md
  • docs/packaging.md
  • engine.py
  • escalation.py
  • evidence_pack.py
  • plugin.yaml
  • query_view_store.py
  • reasoning.py
  • requirements_compiler.py
  • selective_compiler.py
  • store.py
  • tests/test_adaptive_retrieval.py
  • tests/test_answer_contract.py
  • tests/test_assertion_extraction_state.py
  • tests/test_assertion_lifecycle_tools.py
  • tests/test_crash_safe_wal.py
  • tests/test_embedding_backfill.py
  • tests/test_evidence_contract.py
  • tests/test_evidence_pack.py
  • tests/test_lcm_command.py
  • tests/test_lcm_core.py
  • tests/test_lcm_engine.py
  • tests/test_packaging_install.py
  • tests/test_reasoning.py
  • tests/test_release_workflow.py
  • tests/test_rollup_builder.py
  • tests/test_rollup_introspection.py
  • tests/test_selective_compiler.py
  • tests/test_tokens_encoder_loading.py
  • tests/test_trajectory_state_semantic_expansion.py
  • tests/test_trajectory_store.py
  • tests/test_vector_store.py
  • tools.py
  • trajectory_store.py
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: test (3.13)
🧰 Additional context used
🪛 ast-grep (0.45.0)
tests/test_packaging_install.py

[info] 937-946: use jsonify instead of json.dumps for JSON output
Context: json.dumps(
{
"hits": [
{
"exact_ref": f"lcm:{fact_id}:0-{len(fact)}",
"content": fact,
}
]
}
)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

tests/test_evidence_contract.py

[info] 694-696: use jsonify instead of json.dumps for JSON output
Context: json.dumps(
concert_result, indent=2
)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

requirements_compiler.py

[warning] 1719-1719: Regex pattern passed to re is built from a non-literal (variable, call, concatenation, or f-string) value. If that value is attacker-controlled it can introduce a malicious pattern with catastrophic backtracking (ReDoS). Use a hardcoded literal pattern, or validate/escape untrusted input with re.escape() and bound the regex complexity before compiling.
Context: re.search(pattern, normalized)
Note: [CWE-1333] Inefficient Regular Expression Complexity.

(redos-non-literal-regex-python)

reasoning.py

[warning] 809-813: Regex pattern passed to re is built from a non-literal (variable, call, concatenation, or f-string) value. If that value is attacker-controlled it can introduce a malicious pattern with catastrophic backtracking (ReDoS). Use a hardcoded literal pattern, or validate/escape untrusted input with re.escape() and bound the regex complexity before compiling.
Context: re.finditer(
r"\b(?:" + "|".join(_WORD_NUMBERS) + r")\b",
quote,
re.IGNORECASE,
)
Note: [CWE-1333] Inefficient Regular Expression Complexity.

(redos-non-literal-regex-python)

🪛 LanguageTool
FINDINGS-VERDICTS-UP1.md

[grammar] ~4-~4: Ensure spelling is correct
Context: ...IDATED/FIXED: hit CAS checks generation/rowcount and re-reads readiness before returning...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

CHANGELOG.md

[typographical] ~9-~9: To join two clauses or introduce examples, consider using an em dash.
Context: ... additional changes yet. ## v0.21.0-rc2 - 2026-08-05 ### Changed - #492 corrects...

(DASH_RULE)


[typographical] ~19-~19: To join two clauses or introduce examples, consider using an em dash.
Context: ...eplacement characters. ## v0.21.0-rc1 - 2026-08-03 ### Highlights - Add the tr...

(DASH_RULE)


[typographical] ~56-~56: To join two clauses or introduce examples, consider using an em dash.
Context: ...- or embedding-backed paths. ## v0.20.0 - 2026-07-23 Release focus: Lossless-Claw...

(DASH_RULE)

bench/release-kit/review-logs/DELTA-REVIEW-PACKET.md

[style] ~11-~11: Consider using the typographical ellipsis character here instead.
Context: ...he mode split (finding 5's real fix):** search(..., allow_operators=False) default with h...

(ELLIPSIS)

bench/release-kit/review-logs/REGRESSION-REPORT.md

[style] ~8-~8: All-uppercase text might be considered intrusive (unless these are acronyms or it is a legal text).
Context: ...y_and_uses_output_sandbox` ## Verdict GENUINE REGRESSION, FIXED. Before the fix, the stress CLI exited ...

(ALL_UPPERCASE)

bench/release-kit/review-logs/REVIEW-PACKET.md

[style] ~26-~26: Redundant conjunctions can lead to confusion; consider removing a conjunction here.
Context: ...rror or as an unintended OPERATOR (NEAR/AND/OR/NOT semantics, column filters col:, `...

(AND_OR)


[grammar] ~27-~27: Ensure spelling is correct
Context: ...` prefix)? Does term-splitting match unicode61 exactly (unicode categories, diacritics...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

bench/release-kit/review-logs/FINDINGS-VERDICTS.md

[style] ~12-~12: Consider using the typographical ellipsis character here instead.
Context: ...IRMED-FIXED | The hint consumer selects lcm_expand(node_id=...) only when from_current_session is p...

(ELLIPSIS)

🪛 markdownlint-cli2 (0.23.2)
FINDINGS-VERDICTS-UP1.md

[warning] 1-1: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

bench/release-kit/review-logs/REVIEW-PACKET.md

[warning] 7-7: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 17-17: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 23-23: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 44-44: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🪛 zizmor (1.29.0)
.github/workflows/release.yml

[info] 27-27: action functionality is already included by the runner (superfluous-actions): use gh release in a script step

(superfluous-actions)

🔇 Additional comments (106)
trajectory_store.py (2)

694-699: LGTM!


1745-1804: LGTM!

tests/test_trajectory_state_semantic_expansion.py (2)

1199-1219: LGTM!


1222-1257: LGTM!

tests/test_trajectory_store.py (2)

22-22: LGTM!


264-289: LGTM!

.github/ISSUE_TEMPLATE/bug_report.yml (1)

47-47: LGTM!

.github/release-notes/v0.21.0-rc1.md (1)

1-26: LGTM!

.github/release-notes/v0.21.0-rc2.md (1)

1-17: LGTM!

.github/workflows/release.yml (1)

18-24: LGTM!

Also applies to: 26-32

CHANGELOG.md (1)

7-62: LGTM!

README.md (1)

201-208: LGTM!

Also applies to: 247-257

docs/operator-guide.md (1)

82-113: LGTM!

Also applies to: 126-138, 172-172, 242-285

bench/release-kit/review-logs/FINDINGS-VERDICTS.md (1)

1-22: LGTM!

bench/release-kit/review-logs/REGRESSION-REPORT.md (1)

1-57: LGTM!

bench/release-kit/review-logs/REVIEW-PACKET.md (1)

1-47: LGTM!

docs/packaging.md (1)

33-33: LGTM!

plugin.yaml (1)

2-2: LGTM!

tests/test_lcm_command.py (1)

420-420: LGTM!

Also applies to: 460-460

tests/test_lcm_engine.py (1)

1142-1142: LGTM!

Also applies to: 1168-1172, 1210-1210

tests/test_packaging_install.py (3)

380-380: LGTM!


859-876: LGTM!


905-965: LGTM!

tests/test_release_workflow.py (1)

6-17: LGTM!

Also applies to: 19-24, 27-42, 45-56, 58-68, 71-80

bench/release-kit/review-logs/DELTA-REVIEW-PACKET.md (1)

1-24: LGTM!

bench/release-kit/review-logs/FINDINGS-VERDICTS-R2.md (1)

1-19: LGTM!

bench/release-kit/review-logs/FINDINGS-VERDICTS-R3.md (1)

1-21: LGTM!

engine.py (8)

148-201: LGTM!


203-248: LGTM!


250-270: LGTM!


272-311: LGTM!


607-609: LGTM!


1634-1653: LGTM!


1655-1702: LGTM!


1755-1755: LGTM!

command.py (2)

2419-2432: LGTM!


2494-2498: LGTM!

db_bootstrap.py (2)

103-108: LGTM!


127-137: 🩺 Stability & Availability

Confirm that db_bootstrap.py imports time.

Line 127 calls time.monotonic() on every configure_connection() invocation, and line 136 calls time.sleep(). The module header is not part of this review context. If the time import is absent, every connection setup raises NameError, not only the retry path.

The retry logic itself is sound: non-lock OperationalError values re-raise immediately, and the deadline check inside the except clause guarantees loop termination.

#!/bin/bash
# Verify that db_bootstrap.py imports the `time` module and defines SQLITE_BUSY_TIMEOUT_MS.
set -euo pipefail

fd -t f 'db_bootstrap.py' --exec sh -c '
  echo "=== $1 ==="
  rg -n "^\s*(import|from)\s+time\b" "$1" || echo "NO time IMPORT FOUND"
  rg -n "SQLITE_BUSY_TIMEOUT_MS\s*[:=]" "$1" || echo "SQLITE_BUSY_TIMEOUT_MS not defined locally"
  rg -n "^\s*(import|from)\s+" "$1" | head -30
' sh {}
escalation.py (4)

74-115: LGTM!


135-168: LGTM!


170-196: LGTM!


387-392: LGTM!

tests/test_crash_safe_wal.py (1)

236-261: LGTM!

tests/test_embedding_backfill.py (1)

1029-1038: LGTM!

tests/test_lcm_core.py (1)

477-503: LGTM!

tests/test_rollup_builder.py (4)

3-7: LGTM!

Also applies to: 306-306, 346-346, 848-848, 927-927, 1108-1113, 1126-1126, 1150-1166


479-539: LGTM!


541-710: LGTM!


713-753: LGTM!

tests/test_rollup_introspection.py (4)

5-11: LGTM!

Also applies to: 31-31


176-211: LGTM!


213-231: LGTM!


233-252: LGTM!

tests/test_tokens_encoder_loading.py (1)

27-36: LGTM!

tests/test_vector_store.py (1)

2012-2012: LGTM!

__init__.py (1)

189-202: LGTM!

Also applies to: 265-265

answer_contract.py (1)

383-404: LGTM!

Also applies to: 475-477

requirements_compiler.py (1)

715-734: LGTM!

Also applies to: 968-970, 1704-1804, 1845-1845, 1889-1892, 1926-1937, 2070-2220, 2305-2305

store.py (1)

18-18: LGTM!

Also applies to: 117-120

tests/test_answer_contract.py (1)

50-65: LGTM!

Also applies to: 93-108

tests/test_evidence_contract.py (1)

9-10: LGTM!

Also applies to: 168-215, 546-574, 609-747

tests/test_evidence_pack.py (1)

306-325: LGTM!

query_view_store.py (1)

657-665: LGTM!

tests/test_adaptive_retrieval.py (1)

549-573: LGTM!

tests/test_assertion_lifecycle_tools.py (1)

374-374: LGTM!

evidence_pack.py (2)

181-182: LGTM!


763-773: 🗄️ Data Integrity & Integration

Confirm the omission of engine= here is intentional.

tools.py line 706-713 calls ground_evidence with both session_dates= and engine=. This call passes only session_dates=. In reasoning.py, resolve_occurrence_time_with_trust reads the sidecar from engine._session_occurrence_dates. With engine=None, a relative_to_session operand cannot reach anchor_trust="engine_sidecar", so it is reported as low_trust even when the same map is supplied through session_dates. Grounding still succeeds because trusted_session_date supplies the anchor, so this affects trust reporting only.

If the evidence-pack wire is expected to carry the same temporal-trust classification as lcm_compute, pass engine=engine here too.

#!/bin/bash
# Compare every ground_evidence call site for session_dates/engine argument parity.
rg -nP -A 8 '\bground_evidence\s*\(' --type=py
reasoning.py (22)

13-13: LGTM!

Also applies to: 39-40


96-133: LGTM!


262-281: 🗄️ Data Integrity & Integration

Verify no consumer asserts an exact plan.as_dict() shape.

as_dict now emits two new keys, interval_unit_fixed and order_index. lcm_compute publishes this dict as plan on its public response (tools.py line 694). Any test or downstream consumer that compares the whole plan dict for equality will fail.

requirements_compiler.py line 1174-1193 constructs EvidencePlan without either field, so both take their defaults; that path is unaffected in behavior.

#!/bin/bash
# Find exact-equality assertions or consumers of the plan wire.
rg -nP -C 4 'as_dict\(\)\s*==|\["plan"\]\s*==|plan_dict\s*==' --type=py
rg -nP -C 3 'interval_unit_fixed|order_index' --type=py

417-423: LGTM!


571-591: LGTM!


690-699: LGTM!


744-769: LGTM!


772-791: LGTM!


889-889: LGTM!

Also applies to: 935-935, 979-991


1012-1022: LGTM!


1079-1083: 🗄️ Data Integrity & Integration

Confirm the engine is None conjunct in the relative-occurrence rejection.

This guard rejects an unanchored relative expression only when no engine is supplied. When an engine is supplied but its sidecar is empty and source_session_day is None, the resolver returns the same reason == "relative_expression_without_session_date", the guard does not fire, and the operand grounds with temporal_certified = False.

Both cases carry identical trust metadata, so the presence of an engine object does not add evidence about the source day. The asymmetry means the same ungrounded relative expression is rejected on one call path and accepted on another.

If the intent is "reject when no trusted source day exists", the condition should test source_session_day is None rather than engine is None.

#!/bin/bash
# Find tests that pin the current asymmetric behavior before changing the guard.
rg -nP -C 6 'relative occurrence requires a trusted source observation date|relative_expression_without_session_date' --type=py

1203-1203: LGTM!

Also applies to: 1219-1219


1260-1269: LGTM!


1290-1311: LGTM!


1328-1339: 🩺 Stability & Availability

Confirm the inline scoped-flag syntax against the declared Python floor.

Lines 1331-1332 use the scoped inline flag group (?i:...). re accepts this form from Python 3.11 onward; earlier interpreters raise re.error at pattern-compile time, which happens at import of this module and would break every consumer.

The PR description states CI runs Python 3.11 through 3.14, so this is compatible. Please confirm the repository's declared floor matches, because a requires-python of >=3.10 would make this a hard import failure rather than a runtime edge case.

#!/bin/bash
# Read the declared Python floor from the project manifests.
fd -H -t f 'pyproject.toml|setup.cfg|setup.py|\.python-version|tox.ini' -x sh -c 'echo "== $1"; rg -n "requires-python|python_requires|target-version|envlist" "$1" || true' _ {}
fd -H -t f -e yml -e yaml . .github/workflows -x sh -c 'echo "== $1"; rg -n "python-version" "$1" || true' _ {}

1342-1385: LGTM!


1388-1423: LGTM!


1440-1472: LGTM!

Also applies to: 1513-1515


1524-1524: LGTM!

Also applies to: 1544-1547


1672-1677: LGTM!

Also applies to: 1707-1707


1759-1782: LGTM!


1861-1862: LGTM!

selective_compiler.py (2)

20-20: LGTM!

Also applies to: 229-229, 239-243, 387-390


166-169: 🗄️ Data Integrity & Integration

Confirm that refs with no time signal should be dropped.

When question_cutoff is set, a ref whose _ref_observed_at returns None is skipped. A ref carries no time signal when the store lookup finds nothing (or no engine was supplied) and the raw ref has no observed_at, timestamp, or date.

The result is fail-closed, which is the right direction for a historical cutoff. It is also a behavior change: a caller that supplies bare {exact_ref, quote} refs together with a question_date now receives reason_code = "no_exact_evidence_handles" where it previously received handles.

Both new tests supply either a date or an engine-backed row, so neither covers the bare-ref case. Please confirm the pre-answer path in __init__.py always supplies an engine or dated refs.

#!/bin/bash
# Inspect how baseline refs reach prepare_selective_compiler and whether they carry time signals.
rg -nP -B 10 -A 8 'prepare_selective_compiler\s*\(' --type=py
rg -nP -C 5 'no_exact_evidence_handles' --type=py
tools.py (2)

343-347: LGTM!

Also applies to: 459-463, 492-497


706-713: LGTM!

tests/test_reasoning.py (4)

171-173: LGTM!

Also applies to: 494-518


591-662: LGTM!

Also applies to: 664-743


822-865: LGTM!

Also applies to: 867-911


1038-1039: LGTM!

Also applies to: 1052-1067, 1132-1167, 1169-1215

tests/test_selective_compiler.py (1)

238-263: LGTM!

adaptive_retrieval.py (2)

25-25: LGTM!

Also applies to: 337-337


907-911: Correct fix for the stale-publish window.

Capturing the snapshot at start and threading it into claim_build closes a real TOCTOU gap. claim_build defaults corpus_snapshot to self.corpus_snapshot(), which reads the corpus state at claim time; any rows appended during the retrieval would then be inside the token's baseline and publish_ready could not detect the advance. Taking the snapshot at retrieval start makes the comparison meaningful, and test_corpus_advance_during_retrieval_rejects_view_publication pins the stale_before_publish outcome.

Taking the snapshot before the lookup call at line 913-914 is also the safe ordering: the recorded generation is never newer than the data the lookup observed.

Also applies to: 1198-1201

assertion_extraction.py (3)

66-83: LGTM!


706-713: LGTM!


525-537: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The loop variable shadows the index parameter.

_assertion_candidate takes index as a keyword parameter and uses it at line 501 to build label. The comprehension at line 531 rebinds index in the enclosing function scope. No defect exists today because label is materialized at line 501, before the shadowing, and lines 533 and 546 read label rather than index.

The trap is that any future code in this function that reads index after line 531 gets the loop remnant, and label would then disagree with it. Rename the loop variable.

Also confirm that _canonical_subject always returns a type:name form. Line 525 indexes subject.split(":", 1)[1] unguarded, so a colon-free subject raises IndexError rather than a validation ValueError.

♻️ Rename the loop variable
         if not any(
-            source_tokens[index:index + len(identity_tokens)] == identity_tokens
-            for index in range(len(source_tokens) - len(identity_tokens) + 1)
+            source_tokens[offset:offset + len(identity_tokens)] == identity_tokens
+            for offset in range(len(source_tokens) - len(identity_tokens) + 1)
         ):
			> Likely an incorrect or invalid review comment.
assertion_state.py (1)

30-30: LGTM!

Also applies to: 175-180, 183-191, 199-203

tests/test_assertion_extraction_state.py (1)

366-386: LGTM!

Comment thread assertion_extraction.py
Comment on lines +465 to +478
if isinstance(value, Mapping):
return all(
_source_supports_assertion_value(
source, item, kind=kind, context=context
)
for item in value.values()
)
if isinstance(value, (list, tuple)):
return all(
_source_supports_assertion_value(
source, item, kind=kind, context=context
)
for item in value
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Bound the recursion depth over object_value.

_source_supports_assertion_value recurses through Mapping and sequence values with no depth limit. object_value arrives from the extraction payload, which is model-generated, so its nesting depth is not controlled by this module. A deeply nested value raises RecursionError.

RecursionError is not a ValueError, so it does not travel the validation-rejection path that every other malformed input takes. It escapes parse_assertion_extraction as an unexpected exception.

Add a depth parameter and reject beyond a small bound.

🛡️ Add a depth bound
 def _source_supports_assertion_value(
     source: str,
     value: Any,
     *,
     kind: str,
     context: str | None = None,
+    depth: int = 0,
 ) -> bool:
     context = source if context is None else context
+    if depth > 8:
+        return False
     if value is None:
         return True

Pass depth=depth + 1 in both recursive calls.

🤖 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 `@assertion_extraction.py` around lines 465 - 478, Update
_source_supports_assertion_value to accept a depth parameter with a small
maximum bound, and reject values once that bound is exceeded using the existing
malformed-input validation path. In both Mapping and sequence branches, pass
depth=depth + 1 to recursive calls while preserving current value checks and
behavior for inputs within the limit.

Comment thread assertion_extraction.py
Comment on lines +484 to +485
if kind in {"action", "event", "status"} and text == "completed":
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

completed bypasses source grounding entirely.

Every other branch in _source_supports_assertion_value checks the value against the source. This branch returns True for any action, event, or status assertion whose value is the literal "completed", with no reference to source or context.

Compare the neighbouring branches:

  • line 482-483 (commitment) requires a commit|promise|will cue in the context.
  • line 486-487 (status / canceled) requires the cancellation cue in the span.
  • line 484-485 (completed) requires nothing.

This matters because completion drives lifecycle transitions. parse_assertion_extraction line 683-689 permits a fulfills relation when from_row.kind is in {action, event, status} and to_row.kind is commitment. An extractor can therefore emit an action assertion with value completed, cite any arbitrary span, and close out a commitment. The new validation, whose stated purpose is to keep assertion values inside their exact source span, does not stop it.

Require a completion cue in the span, matching the pattern used for cancellation.

🛡️ Require an explicit completion cue
+_COMPLETION_CUE = re.compile(
+    r"\b(?:complet(?:e|ed|es|ing)|finish(?:ed|es|ing)?|done|"
+    r"submitted|delivered|wrapped\s+up)\b",
+    re.IGNORECASE,
+)
     if kind == "commitment" and text in {"committed", "promised"}:
         return bool(re.search(r"\b(?:commit|promise|will)\b", context, re.IGNORECASE))
     if kind in {"action", "event", "status"} and text == "completed":
-        return True
+        return bool(_COMPLETION_CUE.search(source))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if kind in {"action", "event", "status"} and text == "completed":
return True
_COMPLETION_CUE = re.compile(
r"\b(?:complet(?:e|ed|es|ing)|finish(?:ed|es|ing)?|done|"
r"submitted|delivered|wrapped\s+up)\b",
re.IGNORECASE,
)
if kind in {"action", "event", "status"} and text == "completed":
return bool(_COMPLETION_CUE.search(source))
🤖 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 `@assertion_extraction.py` around lines 484 - 485, The completed branch in
_source_supports_assertion_value must validate the source span rather than
unconditionally returning true. Require an explicit completion cue in the span,
matching the existing cancellation validation pattern, while preserving the
current kind restrictions for action, event, and status.

Comment thread assertion_extraction.py
Comment on lines +538 to +554
source_span = snapshot.content[start:end]
kind = str(row["kind"] or "").strip().lower()
value_text = str(row["value_text"] or "")
if not _source_supports_assertion_value(
source_span, row["object_value"], kind=kind, context=snapshot.content
) or not _source_supports_assertion_value(
source_span, value_text, kind=kind, context=snapshot.content
):
raise ValueError(f"{label} assertion value is not in the exact source span")
return AssertionCandidate(
source_span_start=start,
source_span_end=end,
subject_key=subject,
predicate_key=_canonical_predicate(row["predicate_key"]),
object_value=row["object_value"],
value_text=str(row["value_text"] or ""),
kind=str(row["kind"] or "").strip().lower(),
value_text=value_text,
kind=kind,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the source versus context split.

Line 542 and line 544 pass the span as source and the whole message as context. Inside the helper, the branches disagree on which one they consult: the commitment branch at line 483 searches context, while the cancels branch at line 487 and the token match at line 489 search source.

Both choices are defensible. A commitment cue can sit outside the cited clause, while a cancellation must be inside it. The split is invisible at the call site and easy to invert by accident. Add a short comment recording the rule.

🤖 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 `@assertion_extraction.py` around lines 538 - 554, Document the intentional
source-versus-context distinction at the _source_supports_assertion_value call
site in the assertion candidate flow: commitment cues may be found in the full
message context, while cancellation cues and token matches must be found within
the cited source span. Add a short comment without changing the existing
arguments or validation behavior.

Comment thread assertion_state.py
Comment on lines 181 to 182
item["unresolved_conflict"] = assertion_id in unresolved_conflicts
item["attribution"] = _attribution(item)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Conflict fields stay definite while active state becomes unknown.

This change nulls active, lifecycle_status, and active_assertion_ids when relations are truncated, because those values are derived from an incomplete relation set. unresolved_conflict at line 181 and conflict_assertion_ids at lines 204-206 are derived from the same incomplete set but are still published as definite values.

Both inputs are affected by truncation:

  • explicit_conflicts is filled by the loop at lines 134-146 over the truncated relations, so a dropped contradicts relation hides a real conflict and the field reports no conflict.
  • unresolved_conflicts is intersected with active_ids at line 153 and extended by the group-variant loop at lines 166-169, both keyed on active_ids. A dropped cancels relation leaves a cancelled assertion inside active_ids, which can invent a conflict that does not exist.

The internal consumer is protected: reasoning.py line 936 sets group_truncated and execute_plan rejects truncated latest_fact state. The public surface is not. lcm_query_state in tools.py lines 459-464 emits "active_assertion_ids": null next to a confident "conflict_assertion_ids": [...] in the same response, so a caller cannot tell that the second list is equally unreliable.

Apply the same unknown contract to the conflict fields.

🛡️ Null the conflict fields under truncation
-        item["unresolved_conflict"] = assertion_id in unresolved_conflicts
+        item["unresolved_conflict"] = (
+            None if relations_truncated else assertion_id in unresolved_conflicts
+        )
-        conflict_assertion_ids=tuple(
-            assertion_id for assertion_id in ids if assertion_id in unresolved_conflicts
+        conflict_assertion_ids=(
+            None
+            if relations_truncated
+            else tuple(
+                assertion_id
+                for assertion_id in ids
+                if assertion_id in unresolved_conflicts
+            )
         ),

This requires the matching type change on conflict_assertion_ids at line 31, plus the same is not None guards in tools.py at lines 464 and 498-500 that active_assertion_ids already received.

Also applies to: 204-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 `@assertion_state.py` around lines 181 - 182, Apply the truncation-aware
unknown contract to conflict fields: update the type of conflict_assertion_ids,
set both unresolved_conflict and conflict_assertion_ids to null when relations
are truncated, and preserve their current derived values otherwise. Update the
corresponding lcm_query_state handling in tools.py, including the guards near
active_assertion_ids, so null conflict fields are emitted safely.

Comment thread engine.py
Comment on lines +322 to +344
while True:
try:
job()
except (Exception, asyncio.CancelledError):
logger.warning(
"LCM background temporal rollup maintenance failed for database=%s scope=%s",
key[0],
key[1],
exc_info=True,
)
with self._condition:
if self._follow_up_key == key and self._follow_up_job is not None:
job = self._follow_up_job
self._follow_up_key = None
self._follow_up_job = None
self._running_follow_up = True
self._condition.notify_all()
continue
self._active_keys.discard(key)
self._running_follow_up = False
self._release_key_owners_if_idle_locked(key)
self._condition.notify_all()
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Protect the active-key bookkeeping against a BaseException escape.

The except (Exception, asyncio.CancelledError) clause does not catch other BaseException subclasses, such as SystemExit raised inside a job. If one escapes, the worker thread dies while key stays in _active_keys and its owners stay in _owned_keys. Two consequences follow:

  • drain_owner() with timeout=None never returns for that owner.
  • Every later schedule() for the same key takes the key in self._active_keys follow-up branch, so that scope never runs maintenance again, even after schedule() starts a replacement worker.

Move the state cleanup into a finally block so the key is always released.

🛠️ Proposed fix to release the active key on any exit path
-            while True:
-                try:
-                    job()
-                except (Exception, asyncio.CancelledError):
-                    logger.warning(
-                        "LCM background temporal rollup maintenance failed for database=%s scope=%s",
-                        key[0],
-                        key[1],
-                        exc_info=True,
-                    )
-                with self._condition:
-                    if self._follow_up_key == key and self._follow_up_job is not None:
-                        job = self._follow_up_job
-                        self._follow_up_key = None
-                        self._follow_up_job = None
-                        self._running_follow_up = True
-                        self._condition.notify_all()
-                        continue
-                    self._active_keys.discard(key)
-                    self._running_follow_up = False
-                    self._release_key_owners_if_idle_locked(key)
-                    self._condition.notify_all()
-                    break
+            try:
+                while True:
+                    try:
+                        job()
+                    except (Exception, asyncio.CancelledError):
+                        logger.warning(
+                            "LCM background temporal rollup maintenance failed for database=%s scope=%s",
+                            key[0],
+                            key[1],
+                            exc_info=True,
+                        )
+                    with self._condition:
+                        if self._follow_up_key == key and self._follow_up_job is not None:
+                            job = self._follow_up_job
+                            self._follow_up_key = None
+                            self._follow_up_job = None
+                            self._running_follow_up = True
+                            self._condition.notify_all()
+                            continue
+                        break
+            finally:
+                with self._condition:
+                    self._active_keys.discard(key)
+                    self._running_follow_up = False
+                    self._release_key_owners_if_idle_locked(key)
+                    self._condition.notify_all()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while True:
try:
job()
except (Exception, asyncio.CancelledError):
logger.warning(
"LCM background temporal rollup maintenance failed for database=%s scope=%s",
key[0],
key[1],
exc_info=True,
)
with self._condition:
if self._follow_up_key == key and self._follow_up_job is not None:
job = self._follow_up_job
self._follow_up_key = None
self._follow_up_job = None
self._running_follow_up = True
self._condition.notify_all()
continue
self._active_keys.discard(key)
self._running_follow_up = False
self._release_key_owners_if_idle_locked(key)
self._condition.notify_all()
break
try:
while True:
try:
job()
except (Exception, asyncio.CancelledError):
logger.warning(
"LCM background temporal rollup maintenance failed for database=%s scope=%s",
key[0],
key[1],
exc_info=True,
)
with self._condition:
if self._follow_up_key == key and self._follow_up_job is not None:
job = self._follow_up_job
self._follow_up_key = None
self._follow_up_job = None
self._running_follow_up = True
self._condition.notify_all()
continue
break
finally:
with self._condition:
self._active_keys.discard(key)
self._running_follow_up = False
self._release_key_owners_if_idle_locked(key)
self._condition.notify_all()
🤖 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 `@engine.py` around lines 322 - 344, Restructure the worker loop around the job
execution and follow-up handling so the active-key cleanup always runs in a
finally block, including when job() raises a BaseException such as SystemExit.
Ensure _active_keys.discard(key), _running_follow_up reset,
_release_key_owners_if_idle_locked(key), and condition notification execute on
every exit path, while preserving follow-up job processing when available.

Comment on lines +502 to +539
def test_relation_truncation_marks_lifecycle_and_active_state_unknown():
target = "a" * 64
row = {
"assertion_id": target,
"subject_key": "user:self",
"predicate_key": "drink.preference",
"scope_key": "",
"kind": "preference",
"object_value": "tea",
"polarity": "positive",
"value_text": "tea",
"speaker_role": "user",
}
relations = [
{
"relation_type": "confirms",
"from_assertion_id": target,
"to_assertion_id": target,
}
for _ in range(500)
]

class FakeStore:
def query_assertions(self, **_kwargs):
return [row]

def query_relations(self, *, limit, **_kwargs):
return relations[:limit]

state = query_assertion_state(
FakeStore(), subject_key="user:self", predicate_key="drink.preference"
)

assert state.relations_truncated is True
assert state.active_assertion_ids is None
assert state.assertions[0]["active"] is None
assert state.assertions[0]["lifecycle_status"] is None
assert state.assertions[0]["semantic_state"] == "unknown"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good truncation test. Extend it to the conflict fields.

The construction is precise: the 500 relations are confirms, which is absent from _INACTIVE_RELATION_STATUS, so lifecycle stays empty and the assertion would read as active. The test therefore proves the truncation override rather than incidental inactivity.

unresolved_conflict and conflict_assertion_ids are not asserted. Those are the two fields that assertion_state.py still publishes as definite under truncation, despite deriving from the same incomplete relation set. Adding assertions here pins whichever contract you decide on.

💚 Suggested additional assertions
     assert state.relations_truncated is True
     assert state.active_assertion_ids is None
     assert state.assertions[0]["active"] is None
     assert state.assertions[0]["lifecycle_status"] is None
     assert state.assertions[0]["semantic_state"] == "unknown"
+    # Conflict state derives from the same truncated relation set.
+    assert state.assertions[0]["unresolved_conflict"] is None
+    assert state.conflict_assertion_ids is None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_relation_truncation_marks_lifecycle_and_active_state_unknown():
target = "a" * 64
row = {
"assertion_id": target,
"subject_key": "user:self",
"predicate_key": "drink.preference",
"scope_key": "",
"kind": "preference",
"object_value": "tea",
"polarity": "positive",
"value_text": "tea",
"speaker_role": "user",
}
relations = [
{
"relation_type": "confirms",
"from_assertion_id": target,
"to_assertion_id": target,
}
for _ in range(500)
]
class FakeStore:
def query_assertions(self, **_kwargs):
return [row]
def query_relations(self, *, limit, **_kwargs):
return relations[:limit]
state = query_assertion_state(
FakeStore(), subject_key="user:self", predicate_key="drink.preference"
)
assert state.relations_truncated is True
assert state.active_assertion_ids is None
assert state.assertions[0]["active"] is None
assert state.assertions[0]["lifecycle_status"] is None
assert state.assertions[0]["semantic_state"] == "unknown"
def test_relation_truncation_marks_lifecycle_and_active_state_unknown():
target = "a" * 64
row = {
"assertion_id": target,
"subject_key": "user:self",
"predicate_key": "drink.preference",
"scope_key": "",
"kind": "preference",
"object_value": "tea",
"polarity": "positive",
"value_text": "tea",
"speaker_role": "user",
}
relations = [
{
"relation_type": "confirms",
"from_assertion_id": target,
"to_assertion_id": target,
}
for _ in range(500)
]
class FakeStore:
def query_assertions(self, **_kwargs):
return [row]
def query_relations(self, *, limit, **_kwargs):
return relations[:limit]
state = query_assertion_state(
FakeStore(), subject_key="user:self", predicate_key="drink.preference"
)
assert state.relations_truncated is True
assert state.active_assertion_ids is None
assert state.assertions[0]["active"] is None
assert state.assertions[0]["lifecycle_status"] is None
assert state.assertions[0]["semantic_state"] == "unknown"
# Conflict state derives from the same truncated relation set.
assert state.assertions[0]["unresolved_conflict"] is None
assert state.conflict_assertion_ids is None
🤖 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_assertion_extraction_state.py` around lines 502 - 539, Extend
test_relation_truncation_marks_lifecycle_and_active_state_unknown to assert the
truncation behavior for conflict fields: verify unresolved_conflict is unknown
and conflict_assertion_ids is unset or otherwise marked indeterminate according
to the chosen contract. Update assertion_state.py so these fields are not
published as definite when relations_truncated is true, while preserving their
existing values for complete relation results.

Comment thread tests/test_reasoning.py
Comment on lines +913 to +933
def test_how_long_ago_uses_one_anchored_operand(evidence_db):
messages, assertions = evidence_db
content = "I visited Paris on 2023-03-18."
store_id = _message(messages, content, "2023-03-18")
operands = _ground(
messages,
assertions,
[_raw(store_id, content, content, date="2023-03-18")],
question_date="2023-03-20",
)

plan = compile_evidence_plan(
"How long ago did I visit Paris?",
"2023-03-20",
).plan

assert plan.operation == "date_interval"
assert plan.exact_operands == 1
assert plan.interval_unit == "day"
assert execute_plan(plan, operands).trace.result == "2 days"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test does not exercise the new coarsest-unit path.

compile_evidence_plan("How long ago did I visit Paris?") now sets interval_unit_fixed = False, and execute_plan reports the coarsest exact calendar unit for such a plan. The interval here is 2 days, so the coarsest-fit branch selects "day" anyway. The assertions pass identically whether interval_unit_fixed is True or False, so the new flag is untested.

Add coverage that separates the two behaviors:

  • assert plan.interval_unit_fixed is False for the bare question,
  • assert interval_unit_fixed is True and unit "year" for "How long ago ... in years?",
  • assert a multi-year gap reports in years, and a 3-week gap reports in weeks, for the bare question.
💚 Suggested additional assertions
     assert plan.operation == "date_interval"
     assert plan.exact_operands == 1
     assert plan.interval_unit == "day"
+    assert plan.interval_unit_fixed is False
     assert execute_plan(plan, operands).trace.result == "2 days"
+
+    fixed = compile_evidence_plan(
+        "How long ago, in years, did I visit Paris?",
+        "2023-03-20",
+    ).plan
+    assert fixed.interval_unit_fixed is True
+    assert fixed.interval_unit == "year"
🤖 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_reasoning.py` around lines 913 - 933, Expand
test_how_long_ago_uses_one_anchored_operand to verify the bare question sets
plan.interval_unit_fixed to False, then add fixed-unit coverage for “in years?”
asserting interval_unit_fixed is True and interval_unit is “year”. Also assert
that the bare question reports a multi-year interval in years and a three-week
interval in weeks, preserving the existing anchored-operand setup.

Comment on lines +463 to +467
started_at = time.monotonic()
engine.on_session_start(scope, conversation_id="slow-rollup-conversation")
bind_elapsed = time.monotonic() - started_at

assert bind_elapsed < 0.15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tight 0.15-second wall-clock thresholds risk CI flakes in tests/test_rollup_builder.py. Both tests separate asynchronous from synchronous behavior with a 150 ms budget, but each measured operation performs real SQLite work. The regression each test guards against would block for about 2 seconds, so a much wider threshold preserves the same discrimination.

  • tests/test_rollup_builder.py#L463-L467: raise assert bind_elapsed < 0.15 to assert bind_elapsed < 1.0; the bind also performs lifecycle writes and a possible empty-lifecycle GC pass.
  • tests/test_rollup_builder.py#L815-L819: raise assert shutdown_elapsed < 0.15 to assert shutdown_elapsed < 1.0; the shutdown closes five SQLite connections, each running PRAGMA wal_checkpoint(PASSIVE).
📍 Affects 1 file
  • tests/test_rollup_builder.py#L463-L467 (this comment)
  • tests/test_rollup_builder.py#L815-L819
🤖 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_rollup_builder.py` around lines 463 - 467, Widen the asynchronous
timing thresholds in tests/test_rollup_builder.py at lines 463-467 and 815-819
from 0.15 seconds to 1.0 second: update the bind_elapsed assertion after
engine.on_session_start and the shutdown_elapsed assertion in the corresponding
shutdown test, preserving the existing timing checks and regression coverage.

Comment on lines +266 to +279
def test_historical_cutoff_hydrates_trusted_store_timestamp(tmp_path):
engine = _engine(tmp_path)
past = _evidence(engine, "The first purchase cost $20.")
future = _evidence(engine, "The second purchase cost $30.")
engine._store._conn.executemany(
"UPDATE messages SET timestamp = ? WHERE store_id = ?",
[
(1_704_067_200, int(past["exact_ref"].split(":")[1])),
(1_767_225_600, int(future["exact_ref"].split(":")[1])),
],
)
engine._store._conn.commit()
past["date"] = "2024-01-01"
future["date"] = "2024-01-01"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

State why this test writes SQL instead of using the _evidence helper.

_evidence already accepts observed_at=, so the raw UPDATE looks redundant. It is not. Passing observed_at= sets msg["timestamp"], which MessageStore.append normalizes into the observed_at column, and _ref_observed_at reads observed_at first. This test patches the timestamp column while observed_at stays NULL, so it exercises the timestamp fallback branch at selective_compiler.py lines 122-123 specifically.

That intent is invisible to a reader and the test reaches through two private attributes (engine._store._conn). Add a one-line comment so a future cleanup does not "simplify" it into the helper and silently stop testing the fallback.

📝 Proposed comment
     engine = _engine(tmp_path)
     past = _evidence(engine, "The first purchase cost $20.")
     future = _evidence(engine, "The second purchase cost $30.")
+    # Patch the `timestamp` column directly and leave `observed_at` NULL, so
+    # this covers the timestamp FALLBACK in _ref_observed_at. Using
+    # _evidence(observed_at=...) would populate observed_at and test the
+    # primary branch instead.
     engine._store._conn.executemany(
         "UPDATE messages SET timestamp = ? WHERE store_id = ?",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_historical_cutoff_hydrates_trusted_store_timestamp(tmp_path):
engine = _engine(tmp_path)
past = _evidence(engine, "The first purchase cost $20.")
future = _evidence(engine, "The second purchase cost $30.")
engine._store._conn.executemany(
"UPDATE messages SET timestamp = ? WHERE store_id = ?",
[
(1_704_067_200, int(past["exact_ref"].split(":")[1])),
(1_767_225_600, int(future["exact_ref"].split(":")[1])),
],
)
engine._store._conn.commit()
past["date"] = "2024-01-01"
future["date"] = "2024-01-01"
def test_historical_cutoff_hydrates_trusted_store_timestamp(tmp_path):
engine = _engine(tmp_path)
past = _evidence(engine, "The first purchase cost $20.")
future = _evidence(engine, "The second purchase cost $30.")
# Patch the `timestamp` column directly and leave `observed_at` NULL, so
# this covers the timestamp FALLBACK in _ref_observed_at. Using
# _evidence(observed_at=...) would populate observed_at and test the
# primary branch instead.
engine._store._conn.executemany(
"UPDATE messages SET timestamp = ? WHERE store_id = ?",
[
(1_704_067_200, int(past["exact_ref"].split(":")[1])),
(1_767_225_600, int(future["exact_ref"].split(":")[1])),
],
)
engine._store._conn.commit()
past["date"] = "2024-01-01"
future["date"] = "2024-01-01"
🤖 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_selective_compiler.py` around lines 266 - 279, Add a one-line
comment immediately before the direct SQL update in
test_historical_cutoff_hydrates_trusted_store_timestamp explaining that it
intentionally leaves observed_at NULL to exercise the timestamp fallback in
_ref_observed_at, rather than using _evidence(observed_at=...). Preserve the
existing private connection access and test setup.

Comment thread trajectory_store.py
Comment on lines +641 to +665
except sqlite3.OperationalError:
# Two very different tables reach this branch, and only one of them
# should be tolerated:
#
# LEGACY -- a corpora table written before the ``schema_version``
# column existed. It still carries the core identity
# columns, and failing the open on it would brick a
# store that is merely old. Treat it as unversioned,
# the same way the table-absent branch above does.
# MALFORMED -- a table that is missing the core columns as well.
# Nothing can be recovered from it, and swallowing the
# error here lets the open continue until some later
# INSERT fails on an arbitrary column, reporting the
# wrong cause and doing so AFTER FTS repair has run.
#
# Distinguish them by a column the legacy table certainly has.
columns = {
str(row[1])
for row in self._conn.execute(
"PRAGMA table_info(lcm_trajectory_corpora)"
)
}
if "identity_digest" not in columns:
raise
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restrict the legacy fallback to a missing schema_version column.

Line 641 catches every sqlite3.OperationalError. SQLite uses this exception for lock and other database failures, not only missing columns.

If a lock error occurs and clears before PRAGMA table_info runs, Line 663 accepts a current table as legacy. Writable initialization can then continue into migration or FTS repair instead of reporting the original database failure.

Catch only the missing-schema_version error, or inspect table columns before the SELECT. Add a regression test for a non-schema OperationalError.

#!/bin/bash
python - <<'PY'
import sqlite3
import tempfile

with tempfile.NamedTemporaryFile(suffix=".db") as handle:
    setup = sqlite3.connect(handle.name)
    setup.execute(
        "CREATE TABLE lcm_trajectory_corpora "
        "(singleton INTEGER PRIMARY KEY, identity_digest TEXT, schema_version INTEGER)"
    )
    setup.commit()
    setup.close()

    locker = sqlite3.connect(handle.name, isolation_level=None)
    locker.execute("BEGIN EXCLUSIVE")
    reader = sqlite3.connect(handle.name, timeout=0.01)
    try:
        reader.execute(
            "SELECT schema_version FROM lcm_trajectory_corpora WHERE singleton = 1"
        )
    except sqlite3.OperationalError as exc:
        print(f"OperationalError from a lock: {exc}")
    finally:
        reader.close()
        locker.rollback()
        locker.close()
PY
🤖 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 `@trajectory_store.py` around lines 641 - 665, Restrict the fallback around the
schema-version lookup to genuine missing-column cases only; do not treat lock or
other sqlite3.OperationalError failures as legacy tables. Update the logic near
the lcm_trajectory_corpora schema check to distinguish the absent schema_version
column before returning, while propagating unrelated database errors unchanged,
and add a regression test covering a non-schema OperationalError such as a lock.

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.

4 participants