Skip to content

feat(memory): preserve corroborating episode evidence - #340

Open
coleleavitt wants to merge 7 commits into
cortexkit:masterfrom
coleleavitt:feat/memory-episode-provenance
Open

feat(memory): preserve corroborating episode evidence#340
coleleavitt wants to merge 7 commits into
cortexkit:masterfrom
coleleavitt:feat/memory-episode-provenance

Conversation

@coleleavitt

@coleleavitt coleleavitt commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add content-versioned per-session evidence rows for TypeScript and Rust memory stores
  • make exact repeats idempotent within one session while distinct sessions increase corroboration
  • preserve evidence through update, merge, move/copy collisions, v22 backfill, identity merge, Pi merge, deletion, and TS↔Rust authority transfer
  • keep seen_count monotonic and based on distinct corroborating sessions without reducing legacy aggregate counts
  • document the cross-runtime source contract and migration v79/v51

Fixes #335
Related to #334

Attribution boundary

OpenCode supplies the assistant message that owns the tool call, not a verified originating user message. Primary writes therefore remain source_type=agent; the dormant user variant is not falsely populated. source_message_id is the host-native assistant tool owner on OpenCode and null on Pi/Rust. A future verified host user-message link can activate user safely.

Semantic boundary

Evidence records observations of exact content versions. It does not claim paraphrased memories are the same assertion; explicit merge retains the original hashes.

Verification

  • plugin provenance/migration/authority/identity/backfill/tool suites: 121 pass
  • complete Pi ctx_memory suite, including restored pre-existing coverage: 25 pass
  • plugin and Pi typecheck/build pass
  • plugin lint passes; Pi has one pre-existing non-null warning
  • Rust mc-store compiled against public cortexkit\/commons; 136/136 store tests pass. Full mc-module workspace tests remain blocked by unavailable subconscious path dependencies
  • three Oracle reviews found and verified fixes for false user attribution, lifecycle evidence loss, non-atomic writes, content-version drift, distinct-session counting, legacy count monotonicity, and TS/Rust merge parity

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Preserves content-bound, per-session episode evidence for memories and makes duplicate writes idempotent within a session. Old: duplicate writes bumped seen_count without provenance and evidence-only updates sometimes didn’t sync; new: evidence keyed by content hash and session recomputes seen_count from distinct sessions, and evidence-only changes now sync and seed mirrors in all runtimes.

  • Review and migration
    • Schema v79: create memory_evidence (PK: memory_id, content_hash, source_session_id) with a session index; backfill from memories; bump LATEST_SUPPORTED_VERSION to 79; replay-safe for armed stores.
    • Behavior: use insertMemoryIdempotent and recordMemoryEvidence instead of ad‑hoc seen_count bumps; same-session repeats are no-ops; distinct sessions add corroboration. Attribution requires a verified owner for source_message_id; tool writes default source_type=agent and never infer user.
    • Lifecycle and counts: updates, rekeys/collision merges, identity merges, v22 backfill, copies, and deletes union or cascade evidence; when evidence exists, seen_count derives from distinct-session evidence, otherwise the legacy max is preserved.
    • Cross‑runtime: state‑sync payloads and authority seeds carry evidence arrays; mirror sync reinstalls evidence snapshots; evidence‑only updates rewind watermarks to force resend; TypeScript and Rust stores persist/union evidence and maintain distinct‑session counts.

Written for commit 1975b8a. Summary will update on new commits.

Review in cubic

Greptile Summary

The PR adds content-versioned, per-session evidence for durable memories across the TypeScript, Pi, and Rust stores.

  • Adds evidence schemas and migrations for both storage implementations.
  • Preserves and unions evidence across memory lifecycle operations and authority transfers.
  • Makes same-session repeats idempotent while distinct sessions increase corroboration.
  • Updates state synchronization to include evidence and resend evidence-only changes to existing memory rows.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/plugin/src/tools/ctx-memory/tools.ts Duplicate writes now request Rust synchronization before returning, covering both existing-row and unique-constraint-race outcomes.
packages/plugin/src/features/magic-context/memory/storage-memory.ts Adds idempotent per-session evidence recording and monotonic distinct-session corroboration accounting.
packages/plugin/src/hooks/magic-context/module-state-sync.ts Includes memory evidence in cross-runtime state payloads so synchronized rows preserve provenance.
packages/plugin/src/hooks/magic-context/rust-mode-transform.ts Rewinds memory-specific watermarks when a tool mutation requests synchronization, ensuring evidence-only changes to existing IDs are resent.
crates/mc-store/src/lib.rs Adds the Rust evidence schema and lifecycle handling needed for TypeScript-to-Rust parity.
packages/pi-plugin/src/tools/ctx-memory.ts Adopts the shared idempotent memory insertion and evidence semantics for Pi writes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  W[Memory write or repeat] --> TS[(TypeScript memory store)]
  TS --> E[(Per-session evidence)]
  E --> C[Recompute monotonic seen_count]
  E --> Q[Request Rust memory sync]
  Q --> R[Rewind memory watermarks]
  R --> P[Build state-sync payload]
  P --> RS[(Rust memory store)]
  RS --> M[Mirror or authority transfer]
  M --> TS
Loading

Reviews (7): Last reviewed commit: "style(pi): format memory evidence regres..." | Re-trigger Greptile

Context used (3)

Comment thread packages/plugin/src/tools/ctx-memory/tools.ts

@cubic-dev-ai cubic-dev-ai 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.

7 issues found across 33 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/plugin/src/features/magic-context/storage-db.ts">

<violation number="1" location="packages/plugin/src/features/magic-context/storage-db.ts:1070">
P3: The v79 table and index definition is duplicated in `migrations.ts`. Future schema changes can leave fresh databases initialized here and upgraded databases migrated by v79 with different constraints or indexes; keep one canonical definition, preferably the migration, and remove this duplicate.</violation>
</file>

<file name="packages/plugin/src/hooks/magic-context/rust-mode-transform.ts">

<violation number="1" location="packages/plugin/src/hooks/magic-context/rust-mode-transform.ts:908">
P2: During authority preparation, this code runs a separate evidence query for every memory. Load evidence in one query and group it by `memory_id` to avoid making large-project handoffs unnecessarily slow.</violation>
</file>

<file name="packages/plugin/src/hooks/magic-context/module-state-sync.ts">

<violation number="1" location="packages/plugin/src/hooks/magic-context/module-state-sync.ts:1489">
P2: This map calls getMemoryEvidence(args.pass.db, memory.id) once per memory row, each issuing its own prepared-statement SQL query against memory_evidence. For a full force sync this is an N+1 query pattern on the module-state-sync hot path: with hundreds or thousands of memories it runs that many separate SELECTs while serializing the payload. Batch the evidence lookup into a single query (WHERE memory_id IN (...)) keyed by memory id, then assign per row.</violation>
</file>

<file name="packages/plugin/src/features/magic-context/memory/promotion.ts">

<violation number="1" location="packages/plugin/src/features/magic-context/memory/promotion.ts:65">
P2: When a legacy memory has no provenance row, a later historian session records evidence but leaves `seen_count` unchanged. Update the idempotent evidence path to preserve the legacy aggregate and count this newly observed session, so corroboration does not silently undercount migrated memories.</violation>
</file>

<file name="packages/plugin/src/features/magic-context/migrations-v79.test.ts">

<violation number="1" location="packages/plugin/src/features/magic-context/migrations-v79.test.ts:10">
P3: The v79 backfill test only covers the happy path (a memory with non-null source_session_id). The migration's two other contract branches are unverified: rows with NULL source_session_id must be excluded from backfill, and COALESCE(source_type, 'historian') must produce 'historian' when source_type is absent. Add assertions for those cases so a regression (e.g. dropping the WHERE clause and backfilling dangling evidence) is caught.</violation>
</file>

<file name="packages/pi-plugin/src/tools/ctx-memory.ts">

<violation number="1" location="packages/pi-plugin/src/tools/ctx-memory.ts:475">
P1: When an exact hash already exists in a Rust-module-managed project, this call records evidence through the unguarded existing-row branch. The old path checked TypeScript authority via `updateMemorySeenCount`, so add the same authority guard before dedup updates or route this write through the Rust facade.</violation>
</file>

<file name="packages/pi-plugin/src/tools/ctx-memory.test.ts">

<violation number="1" location="packages/pi-plugin/src/tools/ctx-memory.test.ts:1181">
P2: The test's title promises to verify that content-bound evidence is preserved through a merge, but it only checks the shape of the three rows: every `content_hash` is asserted with `expect.any(String)` and no row validates which hash it holds. As written, the assertion passes even if the canonical memory's own hash was written onto all copied rows, if all three hashes are identical (evidence dedupe/versioning broken), or if the copied hashes point at unrelated content. To actually verify the feature, assert each `content_hash` equals the expected normalized hash of its source content (and that the three hashes are distinct).</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread crates/mc-store/src/lib.rs Outdated
Comment thread crates/mc-store/src/lib.rs Outdated
Comment thread packages/plugin/src/tools/ctx-memory/tools.ts
Comment thread packages/plugin/src/features/magic-context/memory/storage-memory.ts
}

const memory = insertMemory(deps.db, {
const insertResult = insertMemoryIdempotent(deps.db, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When an exact hash already exists in a Rust-module-managed project, this call records evidence through the unguarded existing-row branch. The old path checked TypeScript authority via updateMemorySeenCount, so add the same authority guard before dedup updates or route this write through the Rust facade.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi-plugin/src/tools/ctx-memory.ts, line 475:

<comment>When an exact hash already exists in a Rust-module-managed project, this call records evidence through the unguarded existing-row branch. The old path checked TypeScript authority via `updateMemorySeenCount`, so add the same authority guard before dedup updates or route this write through the Rust facade.</comment>

<file context>
@@ -471,26 +472,19 @@ export function createCtxMemoryTool(
-				}
-
-				const memory = insertMemory(deps.db, {
+				const insertResult = insertMemoryIdempotent(deps.db, {
 					projectPath: projectIdentity,
 					category: rawCategory,
</file context>

.all(canonical?.id),
).toEqual([
{
content_hash: expect.any(String),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The test's title promises to verify that content-bound evidence is preserved through a merge, but it only checks the shape of the three rows: every content_hash is asserted with expect.any(String) and no row validates which hash it holds. As written, the assertion passes even if the canonical memory's own hash was written onto all copied rows, if all three hashes are identical (evidence dedupe/versioning broken), or if the copied hashes point at unrelated content. To actually verify the feature, assert each content_hash equals the expected normalized hash of its source content (and that the three hashes are distinct).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi-plugin/src/tools/ctx-memory.test.ts, line 1181:

<comment>The test's title promises to verify that content-bound evidence is preserved through a merge, but it only checks the shape of the three rows: every `content_hash` is asserted with `expect.any(String)` and no row validates which hash it holds. As written, the assertion passes even if the canonical memory's own hash was written onto all copied rows, if all three hashes are identical (evidence dedupe/versioning broken), or if the copied hashes point at unrelated content. To actually verify the feature, assert each `content_hash` equals the expected normalized hash of its source content (and that the three hashes are distinct).</comment>

<file context>
@@ -1112,3 +1112,89 @@ describe("createCtxMemoryTool", () => {
+					.all(canonical?.id),
+			).toEqual([
+				{
+					content_hash: expect.any(String),
+					source_message_id: null,
+					source_type: "agent",
</file context>

Comment thread crates/mc-store/src/lib.rs Outdated
Comment thread packages/plugin/src/features/magic-context/storage-identity-merge.ts Outdated
UNIQUE(project_path, category, normalized_hash)
);

CREATE TABLE IF NOT EXISTS memory_evidence (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The v79 table and index definition is duplicated in migrations.ts. Future schema changes can leave fresh databases initialized here and upgraded databases migrated by v79 with different constraints or indexes; keep one canonical definition, preferably the migration, and remove this duplicate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/src/features/magic-context/storage-db.ts, line 1070:

<comment>The v79 table and index definition is duplicated in `migrations.ts`. Future schema changes can leave fresh databases initialized here and upgraded databases migrated by v79 with different constraints or indexes; keep one canonical definition, preferably the migration, and remove this duplicate.</comment>

<file context>
@@ -1067,6 +1067,18 @@ export function initializeDatabase(db: Database): void {
       UNIQUE(project_path, category, normalized_hash)
     );
 
+    CREATE TABLE IF NOT EXISTS memory_evidence (
+      memory_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
+      content_hash TEXT NOT NULL,
</file context>

import { initializeDatabase, LATEST_SUPPORTED_VERSION } from "./storage-db";

describe("migration v79: memory evidence", () => {
it("creates the evidence identity and backfills known source sessions", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The v79 backfill test only covers the happy path (a memory with non-null source_session_id). The migration's two other contract branches are unverified: rows with NULL source_session_id must be excluded from backfill, and COALESCE(source_type, 'historian') must produce 'historian' when source_type is absent. Add assertions for those cases so a regression (e.g. dropping the WHERE clause and backfilling dangling evidence) is caught.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/src/features/magic-context/migrations-v79.test.ts, line 10:

<comment>The v79 backfill test only covers the happy path (a memory with non-null source_session_id). The migration's two other contract branches are unverified: rows with NULL source_session_id must be excluded from backfill, and COALESCE(source_type, 'historian') must produce 'historian' when source_type is absent. Add assertions for those cases so a regression (e.g. dropping the WHERE clause and backfilling dangling evidence) is caught.</comment>

<file context>
@@ -0,0 +1,49 @@
+import { initializeDatabase, LATEST_SUPPORTED_VERSION } from "./storage-db";
+
+describe("migration v79: memory evidence", () => {
+    it("creates the evidence identity and backfills known source sessions", () => {
+        const db = new Database(":memory:");
+        try {
</file context>

@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/plugin/src/features/magic-context/storage-identity-merge.ts">

<violation number="1" location="packages/plugin/src/features/magic-context/storage-identity-merge.ts:206">
P1: When an identity merge is rerun after a source row has a legacy baseline, this call counts that baseline again and inflates `seen_count`. Make the transferred legacy baseline idempotent by skipping already-superseded source rows or recording and consuming the baseline on the source.</violation>
</file>

<file name="crates/mc-module/src/lib.rs">

<violation number="1" location="crates/mc-module/src/lib.rs:1702">
P2: When a state-sync memory contains `evidence: null`, this field becomes `None`, so `into_row` treats it as omitted and preserves stale evidence instead of rejecting the malformed snapshot. Reject explicit `null` while accepting only an omitted field or an array, matching the cross-runtime contract.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

const targetId = collision.id;
const mergedSeen = Math.max(Number(collision.seen_count ?? 1), Number(row.seen_count ?? 1));
const mergedSeen =
mergeMemoryEpisodeEvidence(db, targetId, [sourceId]) ??

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When an identity merge is rerun after a source row has a legacy baseline, this call counts that baseline again and inflates seen_count. Make the transferred legacy baseline idempotent by skipping already-superseded source rows or recording and consuming the baseline on the source.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/src/features/magic-context/storage-identity-merge.ts, line 206:

<comment>When an identity merge is rerun after a source row has a legacy baseline, this call counts that baseline again and inflates `seen_count`. Make the transferred legacy baseline idempotent by skipping already-superseded source rows or recording and consuming the baseline on the source.</comment>

<file context>
@@ -201,23 +202,9 @@ function mergeMemoryRow(
-            evidenceRow?.count ?? 0,
-        );
+        const mergedSeen =
+            mergeMemoryEpisodeEvidence(db, targetId, [sourceId]) ??
+            Math.max(Number(collision.seen_count ?? 1), Number(row.seen_count ?? 1));
         const sourceClassifiedAt = Number(row.classified_at ?? 0);
</file context>

#[serde(default)]
mural_cue_rejection_count: i64,
#[serde(default)]
evidence: Option<Vec<ModuleMemoryEvidenceWire>>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When a state-sync memory contains evidence: null, this field becomes None, so into_row treats it as omitted and preserves stale evidence instead of rejecting the malformed snapshot. Reject explicit null while accepting only an omitted field or an array, matching the cross-runtime contract.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/mc-module/src/lib.rs, line 1702:

<comment>When a state-sync memory contains `evidence: null`, this field becomes `None`, so `into_row` treats it as omitted and preserves stale evidence instead of rejecting the malformed snapshot. Reject explicit `null` while accepting only an omitted field or an array, matching the cross-runtime contract.</comment>

<file context>
@@ -1699,7 +1699,7 @@ struct ModuleMemoryWire {
     mural_cue_rejection_count: i64,
     #[serde(default)]
-    evidence: Vec<ModuleMemoryEvidenceWire>,
+    evidence: Option<Vec<ModuleMemoryEvidenceWire>>,
 }
 
</file context>

@coleleavitt

Copy link
Copy Markdown
Contributor Author

Retriggering the immutable head because the only failed check was a pre-code OpenCode version-download failure in the non-required 0.0.0.0 smoke probe.

@coleleavitt coleleavitt reopened this Aug 19, 2026
@iceteaSA

Copy link
Copy Markdown
Contributor

Ran two independent cross-family reviews of this PR against the design in #335, then verified every finding below myself against the PR branch (feat/memory-episode-provenance @ 1975b8a8). Reporting because we filed the issue — take or leave any of it.

First: the invariant we cared about holds, and our prescription was wrong.

#335 argued for PRIMARY KEY (memory_id, session_id) specifically so one session couldn't inflate a corroboration count by restating a fact N ways. You used a 3-part key with content_hash and enforced the invariant in the counting instead:

COUNT(DISTINCT source_session_id)   × 3 sites in storage-memory.ts
COUNT(*) over memory_evidence       × 0

That's strictly better than what we asked for — it preserves content-version provenance that our 2-part key would have destroyed, and storage-memory-evidence.test.ts pins the exact case (one session, two content versions → 2 evidence rows, seenCount === 1). Our issue was wrong on the mechanism; the reviewers both landed on "content_hash earns its place." Worth saying plainly.

Also confirmed clean: LATEST_SUPPORTED_VERSION = 79 in lockstep, memory_evidence created in both the v79 migration and the fresh-DB path, no new mustMaterialize trigger, additive memories still ride m[1].

Two findings both reviewers reached independently

Both reduce to the same root: the evidence table has no session-lifecycle story. Flagging now rather than later because nothing gates promotion on evidence yet (grep evidenceCount|corroborat → 0 hits in storage-memory.ts; permanence still uses retrieval_count), so today this is bookkeeping drift — it becomes correctness the moment a threshold reads these rows.

1. clearSession() doesn't reap evidence. The PR has exactly one DELETE FROM memory_evidence, and it's WHERE memory_id = ? — memory-scoped, not session-scoped. storage-meta-session.ts isn't touched by the diff at all. A reviewer's in-memory probe after clearSession(db, "ses-leak") returned {"evidence":1,"memories":1}.

ARCHITECTURE.md (Storage & migrations) states the rule: "New session-scoped tables must be added to clearSession()." memory_evidence carries source_session_id, so it qualifies.

Genuinely two-sided though, and the second reviewer argued the other side well: deleting evidence means a memory silently loses corroboration it legitimately earned. Its proposed third option — roll a terminal session's distinct contribution into a compact per-memory count, then drop the detail rows — preserves the count without keeping dead session references forever. Your call which semantics you want; the current state looks like neither choice was made explicitly.

2. No harness discriminator, which makes safe cleanup impossible. memory_evidence is (memory_id, content_hash, source_session_id, source_message_id, source_type, observed_at). No harness column — while session_meta has one.

This one I can speak to from operational experience rather than reading. Yesterday I found 1,992 of 6,443 session_meta rows orphaned on my store (31%) — sessions deleted out-of-band without emitting session.deleted, so the event-driven cleanup never fired. Reaping them safely was only possible because session_meta has harness: Pi sessions are legitimately absent from opencode.db by design, so the obvious "delete rows with no matching opencode session" query destroys Pi metadata. I had exactly one such row and nearly took it.

memory_evidence can't be swept that way at all. There's an index on (source_session_id, memory_id) so the lookup is cheap — it's purely the missing discriminator. The reviewer also notes host-local session ids could collide across harnesses under the current PK.

Three Shoulds, lower confidence — evidence attached, judge for yourself

  • Publish-transaction cost. Each new corroboration adds two COUNT(DISTINCT …) scans plus insert/update/reload inside promoteSessionFactsDurable's transaction; SQLite reports USE TEMP B-TREE FOR count(DISTINCT). Given Plugin tests share one context.db, and PRAGMA busy_timeout equals bun's default test timeout — lock contention surfaces as an unattributable 5000ms timeout #312 (shared DB + busy_timeout=5000), lock-hold duration has bitten here before. A (memory_id, source_session_id) index would drop the temp b-tree.
  • N+1 in forced module sync. module-state-sync.ts issued 1,443 queries for 1,443 memories on a production copy — measured 9.344 ms warmed vs 0.824 ms bulk-loaded.
  • Pi divergence undocumented. OpenCode records sourceMessageId; Pi leaves it null. MEMORY-DESIGN.md:170-177 explains why, but PARITY.md is where deliberate divergences are registered.

Growth, for sizing

No retention, pruning, cap, or sweep. Rows grow as Σ_memory distinct(content_hash, source_session_id). On my corpus (1,432 memories, 6,443 sessions) the v79 migration created 2,272 rows at ~232 bytes/row including indexes. The reachable bound at 3 content versions is 1,432 × 6,443 × 3 ≈ 27.7M rows ≈ 6.4 GB — worst-case, not expected-case, but there's currently nothing that makes it unreachable.

What we could not check

Rust evidence tests didn't run — the cloned workspace was missing commons/crates/cortexkit-cache-core, so that leg is unverified, not passed.

What I did verify by reading the PR branch directly: promote_facts_tx in crates/mc-store/src/lib.rs contains 0 calls to record_memory_evidence_tx (the recorder is defined in the same file), and to_store_fact at crates/mc-module/src/historian.rs:74-80 sets source_session_id: None in production. The TS counterpart passes sourceSessionId: sessionId (memory/promotion.ts:60). If that's deliberate sequencing for a later PR, ignore this — but as it stands the two runtimes disagree about whether historian promotion produces evidence at all.

Happy to send a PR for the clearSession reap and/or the harness column if either is wanted, or to leave it entirely to you.

ualtinok added a commit that referenced this pull request Aug 19, 2026
… historian episode gap + armed-store fence gap; doctor probe egress + response-echo redaction)

Co-Authored-By: Alfonso <alfonso@cortexkit.io>

@alfonso-magic-context alfonso-magic-context left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed in depth — and worth saying first: you and @iceteaSA (#335) independently converged on the same missing primitive, which is strong signal the gap is real. The design direction is right: content-bound (memory, content hash, session) episodes make independent-session corroboration computable without adding a category, changing the uniqueness key, or touching rendered memory bytes.

Two blockers before this can merge:

  1. Rust-mode historian promotion records no episodes. to_store_fact() leaves source_session_id as None, publish_historian_chunk() calls promote_facts_tx() without the publishing session, and the exact-duplicate skip path never records evidence. In rust transform mode the historian is the main autonomous memory producer, so as delivered the corroboration signal would silently not accumulate exactly where it matters most. The new Rust tests cover direct insert/facade writes but not historian publication — that path needs both the wiring and a test.
  2. memory_evidence needs to join the authority guard fence. It mirrors from the module authority store, but the TypeScript side has no guard triggers for it, and recordMemoryEvidence() doesn't assert write permission itself — today it's protected only incidentally because episode writes also touch the guarded parent row. An evidence-only write path added later could mutate a MODULE-authority project's read model unguarded. It should get its own triggers plus coverage in the armed-replay test alongside the six existing guarded tables.

Change requests are narrow and the core design stands. If you'd rather we take the fence work (it's deep in our authority machinery), say so and we'll layer it as a follow-up commit on your PR.

@alfonso-magic-context

Copy link
Copy Markdown
Collaborator

Design update that affects this PR: we've taken the corroboration question to a public design discussion in #335 (full reasoning there, including why session-keyed episodes conflict with the operating model we're converging on and what we're proposing instead — deterministic decay + a two-step historian observation-reconciliation). Parking this PR pending that thread's outcome rather than asking for the rework from the earlier review — your input on the #335 direction is very welcome, and the review's rust-wiring and authority-fence findings will apply to whatever variant emerges.

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.

Memory promotion is single-episode: seen_count counts phrasing repeats, not corroboration — proposal for an episode table + optional tiering

3 participants