Skip to content

Phase 8: episodes behind facts - #24

Merged
rohan-hotdata merged 5 commits into
mainfrom
phase-8-episodes
Oct 15, 2026
Merged

rohan-hotdata merged 5 commits into
mainfrom
phase-8-episodes

Conversation

@rohan-hotdata

Copy link
Copy Markdown
Collaborator

This PR adds episodes behind facts. capture can store the text it reads as an episode, and recall can add episodes to the block after the facts. On the benchmark, the memory arm goes from 29 to 45 of 54 on sample A, and from 31 to 45 on sample B. Closes #22.

How

  • Memory.capture takes an optional episode key. remember writes the facts, each with the episode id in sources, and then one put writes the episode. A capture of an unchanged episode calls no extractor and writes nothing.
  • Memory.recall takes episodes: "cited" (the episodes that the facts of the block cite) or "search" (a second search, over the episodes). The facts take the budget first. Whole episodes then fill what is left, under a line episodes:, each with a header ## <key>@<revision> [observed: <date>].
  • The block shows a source that is the id of a record in the same namespace as <key>@<revision>. The stored source keeps the full id. So the run id of the benchmark never reaches a prompt, and runs after the first replay capture from the extractor cache.
  • bench/e2e.py gains --session-episodes (episode key session-<hash>, no session:<hash> source on facts) and --episodes. Each result row lists the episode keys of its block. bench/diagnose.py records whether the block of each miss held an evidence session.
  • No new Store method and no new record field.

One change from the agreed proposal: it said one writer for the facts and the episode. A flush of HotdataStore loads its tables at the same time, so the episode could land without the facts, and a retry would then skip the extractor. Two flushes in a fixed order, as in load, avoid that. docs/internal/plan.md records it.

Why

After phase 7, the largest groups of misses were "no stored fact held the answer" (16 on A, 14 on B) and "the block held some needed facts" (6 and 7). The extractor drops details, and nothing kept the session text. The retrieval arm showed that whole sessions answer 45 of 54.

Results

All on the local stack, GPT-4o reader and judge, --extractor detail. Full tables are in docs/benchmarks.md.

Run Sample A Sample B Reader tokens, median (A)
none (facts only), 16,000 29 31 1,715
cited, 16,000 / 32,000 / 64,000 31 / 38 / 44 1,728 / 6,300 / 12,446
search, 16,000 / 32,000 / 64,000 34 / 43 / 45 45 (64,000) 1,738 / 5,715 / 12,849
Retrieval arm (phase 5 and 6) 45 48 29,878

The diagnosis of search at 64,000: misses with no stored fact went from 17 to 5 on each sample. In 8 of the 9 misses on each sample, the block held an evidence session. The runs and diagnoses cost at least $8.10.

Verification

  • make verify: 236 passed. make integration: 117 passed on HotdataStore, run at c501fc9. The later commits touch only bench/ and docs.
  • New conformance tests on both drivers: capture with an episode, one flush per table, a retried capture, a failing extractor, refused keys, the source display, cited and search blocks against a pinned string, the budget rule, and as_of on episodes.
  • bench/tests: the new options, the episode key, and a check that two runs with different run ids send the same extractor prompts.
  • The README examples run and print what their comments say. The skill check runs the two new commands.
  • Not verified: the cache replay on the benchmark beyond the cost logs, which show runs after the first spending cents; latency of recall with episodes in the cloud.

Notes

  • The budgets were 16,000, 32,000, and 64,000 characters, not 8,000. The reason was an estimate that the facts leave no room at 8,000. The runs showed blocks of facts of at most 5,626 characters, so that estimate was too high.
  • The reader prompt still calls the block "facts extracted from history chats", also when it holds episodes.
  • The local engine refused one fused search once, with a stale BM25 index error. A rerun did not show it.
  • A run that loses its connection to the model API stops as a whole, and keeps nothing until it ends. Both affect only the benchmark runner.
  • Follow-up proposed: build the memory once and reuse it across recall settings, and add an 18-question smoke run on MemoryStore.

capture takes an optional episode key, and recall takes episodes: cited
or search. The block shows a same-namespace source as key@revision. The
plan records the owner's decisions of task 1 and the budgets of task 3.
capture takes an episode key: remember writes the facts, each citing the
episode, and then one put writes the episode. A capture of an unchanged
episode calls no extractor. recall takes episodes, cited or search, and
adds whole episodes after the facts inside the budget. The block shows a
same-namespace source as key@revision. Conformance tests run on both
drivers, and the ledger, skill commands, and changelog follow.
The runner gains --session-episodes, which captures each session under
the key session-<hash> with no session:<hash> source on the facts, and
--episodes, the recall value. Each result row lists the episode keys of
its block. The diagnosis replays capture the same way and records, for
each miss, whether the block held an evidence session as an episode.
docs/benchmarks.md reports each episodes value and budget on sample A,
the best value on sample B, the cost of each run, and the diagnosis.
search at 64,000 characters scored 45 of 54 on both samples, against 29
and 31 with facts alone, at about 12,800 reader tokens. The README gets
the row, and the contract keeps the budget note beside its default.
@rohan-hotdata
rohan-hotdata requested a review from a team as a code owner October 15, 2026 18:04
@rohan-hotdata
rohan-hotdata requested review from zfarrell and removed request for a team October 15, 2026 18:04
Comment thread src/hotmemory/contract.py Outdated
continue
revision = int(source.rpartition("@")[2])
found = self._store.get(namespace, key, revision)
if found is not None and found.kind == "episode":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking: _cited can put an episode that is past its forget_after into the block. Skip such a record with is_forgotten from hotmemory._rules:

Suggested change
if found is not None and found.kind == "episode":
if (
found is not None
and found.kind == "episode"
and not is_forgotten(found, self._clock())
):

Store.get does not hide a record that is past its forget_after. list and search do hide it (docs/guarantees.md line 49).

Failure scenario: a consumer stores an episode with forget_after set to a past time, and sweep has not run yet. A fact cites that episode. recall(..., episodes="search") leaves the episode out, but recall(..., episodes="cited") shows its full content. Retention data then reaches the prompt of an agent.

Do not use is_listed here. A cited revision can be superseded on purpose, and is_listed drops a superseded revision. Add a conformance test for the forgotten case. State the rule in the "cited" bullet of docs/contracts.md.

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

Review

Blocking Issues

  • src/hotmemory/contract.py:244: Memory._cited reads each cited episode with Store.get. Store.get does not hide a record that is past its forget_after. Thus recall(..., episodes="cited") shows an episode that list and search hide. episodes="search" leaves the same episode out.

Action Required

  • In _cited, skip a record when is_forgotten(found, self._clock()) is true. Keep superseded revisions.
  • Add a conformance test for a cited episode that is past its forget_after.
  • State the rule in the "cited" bullet of docs/contracts.md and in docs/guarantees.md.

recall with episodes="cited" read each episode with get, which does not
hide a record past its forget_after. The block could then show an
episode that search hides. _cited now skips such an episode, and keeps a
superseded revision, which the fact cites on purpose.
@rohan-hotdata
rohan-hotdata merged commit b5443ac into main Oct 15, 2026
3 checks passed
rohan-hotdata added a commit that referenced this pull request Oct 16, 2026
* Close phase 8 and plan phases 9 to 11

The roadmap closes phase 8 with PR #24 and adds three phases: faster
evaluation, the ranking with episode chunks, and a large memory store.
The later phases move to 12 to 15. plan.md holds the tasks of phases 9
and 10, and the parts of phase 11 discussed so far.

* Correct the phase 8 run times in the plan

The seven phase 8 runs on sample A took 1,133 to 2,085 seconds, not 1,145
to 1,507. Both copies now name duration_s in the result files as the source.
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.

Phase 8: episodes behind facts

1 participant