Skip to content

(Depends on #1611) Render session blocks and id markers (speedkick) - #1632

Closed
edwinyyyu wants to merge 73 commits into
MemMachine:speedkickfrom
edwinyyyu:feat/event-memory-render-ids-speedkick
Closed

edwinyyyu wants to merge 73 commits into
MemMachine:speedkickfrom
edwinyyyu:feat/event-memory-render-ids-speedkick

Conversation

@edwinyyyu

@edwinyyyu edwinyyyu commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1611 (context parts and block kinds), which is stacked on #1617 and #1597; this PR is the one commit on top of #1611's tip. EventMemory.render_segments gains session blocks and optional id markers, so an agent that searches and expands can name a session or a segment back to the memory from the text it was given.

What changes

EventMemory.render_segments(segments, *, datetime_format, parts=("author",), ids=()):

  • Takes any segments, in any order, and renders a segment given twice once (overlapping windows).
  • Lays them out as a block per session, the sessions in the order of their latest timestamps with the latest last, a blank line between blocks; each block in the store's order (timestamp, event_uuid, index, offset), one line per run of adjacent pieces of one event, as before. Nothing in the text says whether two lines are adjacent in the store: a filtered walk can omit a neighbor and no segment carries a position, so no rendering can promise contiguity and this one claims none.
  • ids names what is marked, each independently: "session" heads every block with [session:"<id>"], the id JSON-quoted since a session id is any string up to 255 bytes; "segment" starts every line with [segment:<hex>], or [segments:<first>..<last>] when the line holds more than one segment. A uuid is 32 hex digits, no hyphens. The plural and the range say which id opens the event and which closes it without prompting; a one-segment event carries one id. An unknown kind raises ValueError.
  • With no ids and one session the text is exactly what render_segments produced before, which rerank relies on (it renders one window per hit, ids off).

Rendered, with both kinds on:

[session:"planning"]
[segment:9531985d5d9d49f89818e811892f902b] [Thursday, January 15, 2026, 10:30 AM] Alice: "Can we move the launch to March?"
[segment:36f675cc81e74ef5a8e25d940ed90475] [Thursday, January 15, 2026, 10:31 AM] Bob: "March works if legal signs off by the 20th."

[session:"standup"]
[segment:6513270e269e4d37b2a74de452e6b438] [Thursday, January 15, 2026, 11:30 AM] Carol: "Legal signed off on the March launch."
[segments:6b0d549b6f03475a9600a35a099950d8..8d116ece173847d9bd9c172411e20b8f] [Thursday, January 15, 2026, 11:32 AM] Bob: "Roadmap draft is up; the launch row moves to March and pricing gets its own milestone."

The grammar a client can substitute short ids into is one marker per line: \[segments?:([0-9a-f]{32})(?:\.\.([0-9a-f]{32}))?\], and \[session:("(?:[^"\\]|\\.)*")\] at a block's head.

Decisions

  1. Flat input, not windows. A window boundary would have been the only thing a windows-shaped input carried, and it cannot be honored: a window from a filtered walk may have events missing between two lines that look adjacent. So the renderer claims adjacency nowhere and needs no boundaries.
  2. Ids per event line, at the line's start. Every event is addressable, not only a window's edges; the marker names what the line contains rather than where an id sits, so no suffix is needed (one line is one event) and a one-segment event is not marked twice.
  3. Short ids are the caller's. The server cannot resolve an id abbreviated per conversation, so the client translates before calling expand regardless; emitting the abbreviation is then substitution on the markers above, and a returned or mutable mapping would add a type to the API and a request payload that grows with everything the model has seen. Minimal-unique prefixes (what claude-memory does) are the stateless alternative and are out of scope here.
  4. Hex, no hyphens, per the review; ~4 tokens per id fewer than the canonical form, and the canonical form is still what every API surface speaks.

Verification

  • uv run pytest packages/server/server_tests/memmachine_server/episodic_memory packages/server/server_tests/memmachine_server/server: 642 passed. New in TestRender: block order by latest timestamp with the store's order inside a block, session headers with a quoted id, segment/segments markers, a segment given twice, an unknown kind rejected; the earlier render and rerank tests pass unchanged, which is the "same text as before" claim.
  • ruff check, ruff format --check, ty check: clean (ty's two pre-existing spacy unresolved imports aside).

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE

edwinyyyu and others added 30 commits September 14, 2026 16:35
Implements the session/source/expansion/eviction part of
design/event_memory_handoff.md from the tenant-lifecycle branch. The
context-part and block-kind model and the segmenter/deriver tables
follow in a second change; this one keeps the producer/null context
union and the `block_type` discriminator as they are.

Data models. `Event`, `Segment` and `Derivative` carry `session_id`
and `source_id` as nullable fields, copied verbatim down the
pipeline; null is encoded as a missing record key, `None` in a typed
id list selects it, and property values stay `None`-free. `SearchHit(score, seed, segments)` replaces
`ScoredSegmentContext` and `QueryResult`; `Neighborhood(before,
after)` and `EvictionOptions` are added.

Reserved keys. `common/property_keys.py` reserves the `memmachine_`
namespace; every system value a search filters on at the vector stage
sits in the record under a reserved key (`event_timestamp`,
`event_session`, `event_source`, `block_kind`). `utils.py` owns the
translation between the typed filters (`since`, `until`, `session_ids`,
`source_ids`, `block_kinds`) and filter trees; a caller key in the
namespace is rejected before any segment is written.

Segment store. `segment_store_sg` gains `session_id`, `source_id` and
`block_kind` columns, a session-led ordering index and a source index; the total order is
`(timestamp, event_uuid, index, offset)`, windows and neighborhoods
are confined to the seed's session, and a null session is one stream.
`get_segment_windows` takes `before`/`after` and the typed filters;
`get_segment_neighborhoods` returns the neighbors and never the seed, as two
lists; `delete_derivatives` unlinks without touching segments; PG
lateral reads run one statement pair per seed session. No migration:
`startup()` keeps `create_all`, and an existing speedkick database is
recreated; schema migration waits for the lifecycle/DDL changes.

EventMemory. `encode_events` forgets the batch first, so a repeat
leaves one copy, and runs eviction from the agentic_expansion branch,
cosine only: batch predecessors, one bounded neighbor query per
derivative, a cluster over `target_size` trimmed from its temporal
middle, displaced records and their links deleted, skipped ones never
written. `query` is the vector stage and returns hits with the seed's
index in its window; `rerank` is the second stage, static, for a
caller with a reranker; `expand` walks a neighborhoods from a segment
or event anchor; `render` replaces the string formatters. The reranker
and the per-call format options leave the constructor and the call,
respectively; the deriver's format is fixed per memory.

Server. `LongTermMemory` sets `Event.source_id` from the producer id,
leaves the session null and keeps the producer context; it reranks after `query` and reads
hits.

Tests: the branch's neighbor and eviction tests ported to the new
shapes on both dialects, plus session confinement, half-open bounds,
instant comparison of zoned bounds on SQLite, source and kind filters,
and link deletion. Each new store assertion was checked to fail
against a mutated store (no session predicate, an inclusive `until`, a
filtered neighborhoods seed, an unnormalized bound).

Rebased 2026-09-10 onto speedkick after MemMachine#1598 merged, adopting its
post-review names (`segment_by_derivative`, `seed_cosine_similarities`).
`common/property_keys.py` and its test, which MemMachine#1598 did not carry into
its merge, are included here.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MuAu353FiSmCJjLX1LWDQW
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
Every field of Event, Segment and Derivative carries a description;
the byte bound on ids is a validator on the model, so
EventMemory._validate_events checks only property keys; positions are
validated non-negative. SearchHit.seed is seed_index, checked to lie
inside segments, and the window around a seed is a segment window.
EvictionOptions.similarity_threshold is cosine_similarity_threshold,
the threshold at or above which eviction is considered; the other two
options say what is fetched and what is kept.

The store contract's class docstring only contrasts the two reads;
the details live on each method. Typed id lists hold ids only, and a
seed with no session walks every session: events in no session do not
belong together, so "no session" is not a value a list can name and
the only timeline to show around such a seed is everything. The
timestamp ordering index segment_store_sg__in_ts_ev_ix_of is restored
for that walk beside the session-led one.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The memory makes no use of either bound, so the caller discards hits
itself; the vector stage keeps its limit because the store uses it.
Every hit comes back rescored, in descending score.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
cosine_similarity_matrix in eviction, and cosine similarity in every
docstring, comment and test name that named a bare similarity.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
…the store

Properties are set at ingest and never edited; a changed event is
forgotten and encoded again under the same uuid, with new segment and
derivative uuids. Event, EventMemory and SegmentStorePartition each
say so in their own terms, and the store's add is pinned as an insert
that rejects a stored uuid, so the vector record's copy of the
declared properties stays exact by construction and a future update
operation has to argue with the contract.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
get_segment_windows and get_segment_neighborhoods differed only in
whether the seed was filtered, which is a lookup concern, not a
walk's. get_segments(uuids, filters) returns the segments the
partition holds that pass; get_segment_neighborhoods(segments, ...)
walks the order around segments the caller holds and looks nothing
up, so a segment can be walked from even after its row is gone. The
rule for a caller: filters select what a read returns, and only a
segment obtained first can be walked from. query fetches its seeds
with the filters and walks from those; expand fetches its anchor
unfiltered and walks; eviction reads timestamps with the lookup. No
walk is asked for when expand_context is zero.

The seed keys reach PostgreSQL as typed bound parameters in a row set
rather than a VALUES list, which SQLAlchemy would recompile on every
call, since the data would be part of the statement's cache key.
Measured against the previous head, alternating runs on one
PostgreSQL container with a fresh analyzed schema per run, the
search-shaped read is a wash within noise and the SQLite read pays
one more connection checkout per call; the numbers are in the pull
request.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
…orhood)

A hit carried a flat list and an index into it, which the lookup and
the walk had to be flattened into and the reader had to index back
out of. QueryHit holds the seed the query matched and the Neighborhood
around it, the shape expand returns, so walking further from a hit
composes with expand and there is no index invariant to keep. window()
gives the flat list where one is wanted: rendering and the server's
episode folding.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The contract is a store's, not a table's: segments are immutable, and
a walk starts from a segment, not from a lookup of it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The typed filters a search takes were all on the lookup and the walk
except session_ids. It is one of them now, selecting what a read
returns as source_ids does; the walk's confinement to the given
segment's session is a separate rule, so a seed with no session walks
every session and session_ids narrows what it shows. expand takes it
with the other filters a search takes, and query passes it through to
both reads.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The test helper gives every event a session, so the anchor had one
and its walk stayed in it. The anchor now has none, which is the case
the test is about.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
…fault

The session is the unit the store's order is partitioned into, and the
walk's confinement already treated it that way; the one case without
one needed an unconfined walk, a second ordering index and a session
filter on the walk to scope it, and the timeline it produced
interleaved unrelated conversations. Every event now belongs to one
stream: session_id is a required, non-empty, bounded string on Event,
Segment and Derivative, the column is NOT NULL, the reserved key is
always written, and a walk always pins its seed's session, so
get_segment_neighborhoods takes no session_ids. get_segments keeps it
as the visibility filter; expand passes it to the lookup, so an anchor
outside the named sessions is not found. The source stays nullable:
nothing is partitioned by it.

The legacy API carries no conversation id, so LongTermMemory writes
every event to memmachine_default, one stream per partition under a
reserved name a caller cannot use; the new API requires a session.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
A walk that took segments spared one point lookup and cost every
caller above the store a segment object: an API caller expanding by
tool call would have had to rebuild the segment exactly or keep a
cache of recent ones, or the server would have kept one for it.
get_segment_neighborhoods takes seed uuids again, locates them
unfiltered, and walks from what it finds; an unknown seed is absent.
query walks from the uuids of the seeds its lookup admitted, and
expand walks from the anchor's uuid, checking it against session_ids
with a lookup only when sessions are named.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
A naive datetime names no instant. Reading it as UTC is a guess a
caller has to know about, and a wrong guess silently shifts an event
by hours in the store's order and in every rendered date. The models
take AwareDatetime, as the episode model already does, and the typed
bounds since and until reject a naive value on both store reads.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The summaries of the two reads repeated their method docstrings; the
walk's own contract now names the order it follows.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
… column once

before and after must be nonnegative on the walk, and expand_context
on query; a negative value raised nothing before and produced an empty
or odd window. The store no longer converts a naive timestamp
anywhere: the column's type, UtcInstant, binds an aware value as its
UTC instant and rejects a naive one, and decodes the naive result
SQLite returns as the UTC instant the column holds, so the two
read-back conversions and the bound conversions go. The bounds are
still checked at the method, before a session is opened, so the error
names the parameter. Docstrings say "timestamps" of the segments or
neighbors, plural.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
Only the default stream's name is reserved; a caller may name a
session memmachine_anything.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
Eviction and what only it requires leave this change for one stacked
on it, so each can be reviewed alone: EvictionOptions and the
eviction parameter, the eviction stage of encode_events and the
batch's temporal sort it depends on, the predecessor, target and
stored-timestamp helpers, and delete_derivatives on the store
contract, the SQLAlchemy store and the fake, with their tests.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The parameters say what the filters do, and the total order says
where an event's other segments sit.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
A session is a session, a source a source, a context a context; the
timestamp is timezone-aware and the rejection of a naive value is the
type's, not the description's. The neighborhood's sides are nouns
like the other descriptions.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
@edwinyyyu
edwinyyyu force-pushed the feat/event-memory-render-ids-speedkick branch from ea9feec to 5919408 Compare September 15, 2026 21:37
Two `if`s say what a loop over a tuple of names said; `_in_values` is a
static method under `_row_conditions`, the one place that calls it; and
the comment on an empty lookup says what the registry read is for.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
@edwinyyyu
edwinyyyu marked this pull request as draft September 15, 2026 21:53
@edwinyyyu
edwinyyyu force-pushed the feat/event-memory-render-ids-speedkick branch from 5919408 to f513d79 Compare September 15, 2026 21:53
The order that put the incarnation second removed a competitor from the
planner's candidates for the link table's foreign-key check; it did not
make the planner choose well, statistics do. A fresh PostgreSQL table
misplans until its first ANALYZE whatever the indexes (the lookup by
uuid runs a sequential scan in that window too), and autovacuum's first
pass, or an ANALYZE after an initial import, ends it for the table's
lifetime. So the indexes read as the rest of the store does, scoped by
the incarnation first, and the PR body states the practice.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
@edwinyyyu
edwinyyyu force-pushed the feat/event-memory-render-ids-speedkick branch from f513d79 to 2910e87 Compare September 15, 2026 22:25
The source-pinned walk index served a walk filtered by one source, and
no measured workload runs one; the session-led index serves every walk,
and a filter on source or kind scans past the session's other rows, tens
of microseconds on a 200,000-row table. An index is paid for on every
insert for as long as it exists, and adding one later is a one-off
background build (2.5 s per million rows on PostgreSQL, concurrently),
so the store indexes the reads it has: the primary key, lookup by
event, and the walk. A walk filtered by source or by kind earns its
index when a workload shows it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
@edwinyyyu
edwinyyyu force-pushed the feat/event-memory-render-ids-speedkick branch from 2910e87 to ed9a13e Compare September 16, 2026 16:43
@edwinyyyu
edwinyyyu force-pushed the feat/event-memory-render-ids-speedkick branch from ed9a13e to a0ffd1b Compare September 16, 2026 17:02
edwinyyyu and others added 7 commits September 16, 2026 16:06
`encode_events` no longer forgets the batch's events before encoding
them: every call adds, and encoding an event a second time stores a
second copy, as speedkick's encode does. The forget-first step widened a
race that speedkick already has: a forget that runs between an encode's
segment commit and its vector upsert orphans the encode's records, and
with encode forgetting first, a concurrent re-encode of one event took
that path too. A follow-up moves the upsert inside the write transaction
and rejects a reused event uuid at the store; until then encode is a
plain insert.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
Ported from the agentic_expansion branch, cosine only. EventMemory
takes an optional EvictionOptions: a cosine similarity threshold at or
above which eviction is considered, the number of stored derivatives
at or above it fetched per new derivative, and how many of a new
derivative and those stored ones to keep when there are more. None
keeps every derivative and issues no query.

At encode, after embedding: the batch's predecessors of each
derivative are computed from the batch's own embeddings, earlier
indices only, so a batch evicts what serial ingestion would; one
batched neighbor query fetches the stored derivatives at or above the
threshold; the stored ones' timestamps come from their segments
through the store's lookup, since the vector store answers uuids and
scores only, and a neighbor whose segment is gone is not a member;
then, per derivative, the members over target_size are trimmed from
the temporal middle, the earliest target_size // 2 and the latest
remainder kept. Displaced stored records are deleted from the vector
store and unlinked from their segments with delete_derivatives, which
the store contract, the SQLAlchemy store and the fake gain; skipped
batch derivatives are never written. The batch is sorted by timestamp
first, so the predecessor rule matches serial order.

Tests: the branch's eviction tests on the new shapes, and
delete_derivatives on both dialects, including on a stale handle.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
…s (speedkick)

The second half of design/event_memory_handoff.md, on top of the
session/source/expansion/eviction change: the content model and the
processing steps become registered families keyed by kind.

Blocks. `Block` is an ABC with `kind` as the discriminator every
registered family uses and `render(options) -> str | None`; the union
stays closed over `TextBlock`; the segment row's `block_kind` reads
`block.kind`.

Context. `Context = Mapping[str, ContextPart]`, keyed by part kind,
never None; `Author` is the first registered kind, an unregistered
kind decodes to `UnknownPart` and round-trips unchanged; `with_part` composes one. `ProducerContext`, `NullContext` and the
discriminated union go; the codec encodes a context as `{kind:
fields}`. The server puts the producer id in an `Author` part beside
`source_id`, so rendering keeps its `producer: text` shape.

Segmenter and deriver. `Segmenter` and `Deriver` are tables from block
kind to handler, built from handlers in order with a later handler
replacing an earlier one for its kind. `BlockSegmenter[B]` and `BlockDeriver[B]` are the one-kind
handler contracts, typed by the block class: `split(event, block) ->
list[Piece]` and `derive(segment, block) -> list[str]`. The table
builds every envelope; a handler decides pieces or texts and nothing
else. A kind with no handler passes through as one segment and derives
nothing; nothing raises on a kind. `TextSegmenter`, `WholeTextDeriver`
and `SentenceTextDeriver` keep their names as `text` handlers;
`PassthroughSegmenter` and its configuration name go: one segment per
block is the identity segmentation, and a default gets no name, so an
omitted `segmenter` means it.

Composition. A derivative is text (`Derivative.text`, the text to
embed, plus the segment's `block_kind` for the record), so every
derivative gets the same context processing. `format_header` is the
one composition point for the embedded text and the rendered header:
the timestamp, then the context parts `parts` names in that order
(default `("author",)`), then the content. Parts carry no order; a part
not listed contributes nothing; a new kind is placed by listing it.
`parts` is a parameter of the composers, the handler and `render`,
not a field of `DatetimeFormat`, which writes a timestamp and nothing
else. A handler owns the composition of what it embeds, its
`datetime_format` and `parts`, so a kind or a handler can embed under
its own; the Deriver table composes each derivative's text with them;
rendering for display takes the caller's per call.

Tests: table dispatch, override order, envelope copying; text handlers returning bare
content; composition order, unlisted parts, kind-name validation, and
the anchor over a derivative; context and block round-trips including
an unregistered part kind.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
RegisteredBlock as the field type left three type errors where a
handler's or the codec's Block was assigned to the field. The fields
are typed Block, decoded through the registered union by a
before-validator and serialized as the concrete kind; the handler
tests keep a typed block in hand instead of narrowing the field.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
A context was a mapping the caller keyed by hand, and the rule that
the key equals the part's kind lived in prose. Context is a class now:
built from parts, read by kind, so there is no key to get wrong; two
parts of one kind are rejected at construction. It owns its wire form
through a Pydantic core schema, so a model field of the type accepts
an instance or `{kind: fields}` and serializes to `{kind: fields}`,
and the per-model validators, `with_part`, `encode_context` and
`decode_context` go.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
`render` takes any segments, drops one given twice, and lays them out as
a block per session, the sessions in the order of their latest
timestamps with a blank line between, each block in the store's order,
one line per run of adjacent pieces of one event. Nothing in the text
says whether two lines are adjacent in the store, since a filtered walk
can omit a neighbor and no segment carries a position.

`ids` marks what a reader can name back: `"session"` heads each block
with `[session:"<id>"]`, the id JSON-quoted since a session id is any
string; `"segment"` starts each line with `[segment:<hex>]`, or
`[segments:<first>..<last>]` when the line holds more than one, so the
marker says which id opens the event and which closes it without a word
of prompting, and a one-segment event carries one id. Uuids are 32 hex
digits. With no ids and one session the text is what `render` produced
before, which `rerank` relies on.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
@edwinyyyu

Copy link
Copy Markdown
Contributor Author

Closed in favor of its main port, #1688, which carries this PR's content at its final tip; the review history stays here. See the "Source and review" section of #1688 for what was reviewed and what was adapted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant