Skip to content

Take filtered context from a bounded window next to each seed (fixes #1712) - #1713

Merged
edwinyyyu merged 24 commits into
MemMachine:mainfrom
edwinyyyu:port/filtered-context-window-main
Sep 30, 2026
Merged

edwinyyyu merged 24 commits into
MemMachine:mainfrom
edwinyyyu:port/filtered-context-window-main

Conversation

@edwinyyyu

@edwinyyyu edwinyyyu commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Purpose of the change

Fixes #1712. On PostgreSQL, a context read with a property filter could read every row on the seed's side of its partition and sort it: the planner has no statistics for property predicates, estimates about one match per seed, and plans a bitmap over that side instead of an ordered walk that stops at the context size. Requests took seconds on large partitions.

Description

  • Filtered context comes from a bounded window. With a property filter, get_segment_contexts takes each seed's context on each side from the 1,000 segments nearest it on that side, read with no filter; the filter and the context size apply over those rows (_context_rows_query). The inner read has no filter for the planner to misestimate, so it stays an ordered index scan and stops once the context is found or the window ends. No segment further than 1,000 segments from its seed is context, filtered or not; the SegmentStore ABC now allows an implementation-defined bound on every context read. The bound is fixed in the store; callers do not choose it.
  • Unfiltered reads keep their statement, which already reads exactly the context size; the bound caps that size at 1,000, which needs no window and costs nothing.
  • SQLite takes the same window, so both dialects return the same context. Its per-seed reads now build each direction's statement once per request and bind each seed's position (first commit): building a statement costs more than SQLite takes to run it, so building one per seed and direction dominated the read. The SQLite table below shows the first commit on its own.
  • Both dialects build their context statements in _context_rows_query: the walk past the seed on one side, the partition's liveness check, and, with a property filter, the window over the walk. They differ only in where the seed's position comes from: the seeds subquery's columns on PostgreSQL, bound parameters on SQLite. On PostgreSQL the liveness check moves from the seeds subquery into the walk, where the plan evaluates it once per statement; SQLite's statements are byte-identical to before.
  • PostgreSQL loads context rows through the ORM, as SQLite does, instead of passing ten keyword arguments per row to SegmentRow's constructor, as main does.

Measurements

Measured on 2026-09-25 with this PR at 38d255252 against main (whose segment store is unchanged since 66d4a653d, #1661): 20 seeds per request, 2 segments back and 4 forward unless noted, the rows of 21 partitions interleaved on disk; property filters matching 20% and 1% of segments. p50 per request with one client, requests/s with concurrent clients. Arms alternated, on AC power, while another session's unit tests shared the CPU (1-minute load 2.3-6.8). This PR ran twice per table and the medians are shown; main ran twice on SQLite and once on PostgreSQL, where its filtered reads take seconds, and a morning run of main on a quiet host agreed within 10% on those rows.

PostgreSQL 16 (128 MB shared buffers), one 1,000,000-segment partition and twenty 20,000-segment ones:

request main, p50 this PR, p50 main, req/s at 16 this PR, req/s at 16
1M partition, 20% filter 6.5 s 5.9 ms 2.0 225.5
1M partition, 1% filter 2.8 s 9.8 ms 1.9 215.6
1M partition, 20% filter, 4 back and 12 forward 6.7 s 9.1 ms 1.1 126.5
1M partition, 1% filter, 4 back and 12 forward 6.1 s 18.0 ms 1.1 136.4
20k partition, 20% filter 106 ms 6.1 ms 63.4 216.9
20k partition, 1% filter 93 ms 10.1 ms 64.2 225.6
1M partition, no filter 6.7 ms 5.5 ms 199.0 259.8

On small PostgreSQL partitions, where main's plan is already cheap, this PR is as fast or faster. Medians of five runs of main and four of this PR; main first, this PR second:

partition 20% filter 1% filter no filter
200 segments 7.13 / 5.87 ms 3.63 / 3.61 ms 6.35 / 5.34 ms
2,000 segments 6.88 / 6.15 ms 8.66 / 8.41 ms 6.64 / 5.22 ms

The window adds a subquery to a filtered statement, but PostgreSQL now loads context rows through the ORM instead of building each by hand (6c7f41f6b), so building a request's two statements takes 0.95-0.98 ms with a filter against main's 0.88-0.96 ms, and 0.46-0.48 ms without one against 0.77-0.80 ms.

SQLite, one 200,000-segment partition and twenty 20,000-segment ones; p50 in ms with one client and with four, for main, this PR's first commit, and this PR:

request main, 1 first commit, 1 this PR, 1 main, 4 first commit, 4 this PR, 4
200k partition, 20% filter 17.0 12.8 13.6 62.1 41.3 45.1
200k partition, 1% filter 28.1 24.1 27.1 92.1 83.0 101.3
200k partition, 20% filter, 4/12 21.3 16.2 17.7 79.6 61.5 66.6
200k partition, 1% filter, 4/12 51.3 46.1 49.2 208.2 206.1 213.4
20k partition, 20% filter 17.6 13.6 14.3 60.0 42.0 46.7
20k partition, 1% filter 28.8 25.0 28.4 91.5 83.2 103.0
200k partition, no filter 15.3 11.0 11.6 54.8 38.6 40.8

SQLite's planner already walked the index in order, so there the window costs 1.0-1.25x over the first commit. With one client this PR is as fast as main or faster in every row; with four, a 1% filter is 3-13% slower than main. What the window buys on SQLite is the same context as PostgreSQL and a bounded worst case: with a property that only the 20 seeds carry, a request takes 37 ms instead of 613-617 ms on the 200,000-segment partition (and 36 ms instead of 70-78 ms on a 20,000-segment one), because the walk no longer runs toward the end of the partition.

For the same seeds, both dialects returned the same context as main in every scenario but one, where the bound applies: 12 forward with the 1% filter, which matches every 100th segment, returns the 10 matches within 1,000 segments.

Choosing the window's size

The bound is a cost limit that keeps what filters ask for, not a relevance cutoff. The window counts the segments the walk passes, so how much it keeps depends on whether the walk stays inside the seed's conversation.

The walk stays inside the seed's conversation. It does in this store when a partition holds one conversation, and in #1684's schema, which keeps many conversations in a partition with a session column the walk follows. On a real store of coding-agent sessions in #1684's schema (PostgreSQL, 20 seeds, 4 back and 12 forward), a 1,024-segment window returned what the current statement returns for a user-and-assistant-message filter (15.2 of 16 neighbors per seed) and 94% of it for filters on user messages alone, in less time than the current statement. A smaller window cuts sparse filters off sooner: 128 returned half as much for user messages alone.

The walk crosses conversations. It does in this store when a partition holds many conversations: with no session column, the conversation is only a filter, and the walk passes other conversations' segments. #1684 is meant to replace that layout. On a snapshot of such a store (SQLite, 917,610 segments from 2,039 conversations in one partition; 5,000 random message seeds), a 1,000-segment window kept this share of what an unbounded walk within the seed's conversation returns:

filter 4 back 12 forward 16 either way
messages 98.7% 96.0% 94.5%
assistant messages 97.9% 95.5% 93.9%
user messages 81.3% 55.4% 48.2%

On that store, the unbounded filtered walk (that store's own build of the segment store, the same walk as #1661's) ran to the partition's start or end for 7.5-9.2% of seeds: whenever the seed's conversation had fewer matches left on a side than requested, it read on through every other conversation's segments. The worst seeds took 1.0-1.5 s; typical ones 1.0-1.5 ms at p50. The window stops such a walk after 1,000 segments, about 1-1.6 ms at the 1.1-1.6 µs per segment measured there.

The window is 1,000, the round number next to the 1,024 measured; nothing depends on a power of two.

Commits

Branched from main at 66d4a653d, where #1661 merged; dd9851a4c merges main at d57f5cb36 (#1671 and #1541), which touches none of this PR's files. This PR is its twenty-three commits: 5a4addc3c, 97c708767, 9055c2ad8, ac96db241, ab58e5072, 9f5a5c8cf, 040f3fd55, e13a259a0, d47909804, b0ff12e49, 35414350a, f92524a9a, 74e91b678 and 4b6d25e39, then after the merge 155a5a523, 745c67921, 6c7f41f6b, 08420008f, 38d255252, 7189e656a, 9aac74f77, 862f15ae2 and dddaecf86.

Verification

At each of the first fourteen commits, on main at 66d4a653d: ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); the full server suite without integration tests passes, 1982 tests at the first commit, 1983 from the second (the window's test) and 1986 from d47909804 (the model test). The event memory and long-term memory integration tests pass against PostgreSQL at 35414350a and 74e91b678, 107 with 3 skipped.

The window's test sets the bound to 4 segments and checks that a match beyond it is not context and that an unfiltered read stops at it; it fails on both dialects with the window's LIMIT off by one or removed and with the unfiltered cap removed. d47909804 adds test_random_context_reads_agree_with_a_model: random context reads, several seeds at a time, with filters of every node type, many segments sharing a timestamp, and another partition's segments at the same times, compared with an in-memory model of the timeline; every partition is far smaller than the bound. The existing context tests pass with the lateral's walk left uncorrelated to its seed and with ties ordered by the wrong columns; the new test fails on both dialects with ties misordered, with the window ordered the same way on either side, and with the filter resolved against the table instead of the window, and on PostgreSQL with the walk uncorrelated. It passes on both dialects at #1661's last commit, so outputs there and here agree wherever every match is within the bound.

The refactors change no results. ab58e5072 builds each statement's walk in _context_rows_query: SQLite's context statements compile byte-identical to before it, on PostgreSQL only the liveness check moves (the plan evaluates it once per statement), and p50s stay within 0.2 ms of ac96db241's over five alternating rounds. Before and after 040f3fd55 (keyword-only flags and bounds) and b0ff12e49 (statements built from Core columns, SQLite rows loaded through select(SegmentRow).from_statement()), every context statement compiles identically on both dialects; b0ff12e49 cut the Python that builds a filtered PostgreSQL request's statements from 1.69-1.71 ms to 1.13-1.24 ms, measured on 2026-09-25 before 6c7f41f6b lowered it further. 35414350a annotates with the documented ColumnCollection instead of the undocumented ReadOnlyColumnCollection. f92524a9a caps unfiltered reads at the same bound. 9055c2ad8, 9f5a5c8cf, e13a259a0, 74e91b678 and 4b6d25e39 rename a helper or change only comments and docstrings; ac96db241 sets the bound to 1,000.

After the merge: 155a5a523 builds SQLite's statements only for the sides a read asks for; 745c67921 had each model-test read set the bound at random, down to one segment, and 7189e656a reverts it: the bound's size is not part of the contract and bounds of a few segments are not realistic use, while the window's test already catches the bound's failures and the model test, without a bound, the multi-seed one (a walk uncorrelated to its seed on PostgreSQL); 6c7f41f6b loads PostgreSQL's context rows through the ORM. 6c7f41f6b, against the commit before it in one quiet window: p50 at one client 14-22% lower with a filter on the 1M-segment partition and on the 20k one with the 20% filter, and requests/s at 16 clients 26-39% higher in every scenario, with identical contexts. 08420008f resolves filter fields against columns only, and 38d255252 reads SQLite's context one direction at a time, as PostgreSQL does; both leave the SQL unchanged on both dialects (SQLite runs the same statements, all backward before forward), and against 6c7f41f6b in the measurement window above, SQLite p50s are within 3% either way and PostgreSQL's equal or lower, with identical contexts. At 745c67921, 6c7f41f6b and 38d255252: ruff check, ruff format --check and ty check clean; the full server suite without integration tests passes, 1994 tests (the merge brought in #1541's); the event memory and long-term memory integration tests pass against PostgreSQL, 107 with 3 skipped. At 08420008f: ruff, ty and the event memory tests without integration (193) pass. At 7189e656a (test file only): ruff and ty clean, and the segment store tests pass, 101 on SQLite and 104 against PostgreSQL. 9aac74f77 drew the model test's identifiers from its seeded generator, and 862f15ae2 reverts it: the seeded generator already makes every run test the same cases, and identifiers stay random UUIDs; at 862f15ae2 the test file is as at 7189e656a, and the segment store tests pass, 101 on SQLite and 104 against PostgreSQL. dddaecf86 gives the model test's events random UUIDs in sorted order, so events sharing a timestamp come in the same order every run and every run tests the same cases; at it, ruff and ty are clean, the segment store tests pass (101 and 104), and the model test still fails on both dialects with ties misordered, with the window ordered the same way on either side and with the filter resolved against the table, and on PostgreSQL with the walk uncorrelated.

🤖 Generated with Claude Code

@edwinyyyu
edwinyyyu force-pushed the port/filtered-context-window-main branch 2 times, most recently from db5d3ca to 471cca8 Compare September 25, 2026 00:26
edwinyyyu and others added 11 commits September 25, 2026 10:59
…'s position

SQLite's context reads run two statements per seed, and each was built
per seed with the seed's walk position as literals. Building a
statement and computing its cache key costs several times what SQLite
takes to run it: 0.16 ms against 0.04 ms per statement on a
200,000-segment partition, for a filtered statement. Each direction's
statement is now built once per request and run per seed with the
position bound. With 20 seeds, 2 back and 4 forward, a request takes
11.7 ms instead of 15.3 ms unfiltered and 12.4 ms instead of 17.0 ms
with a property filter matching 20% (p50, one client).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…emMachine#1712)

A context read with a property filter selected, per seed and direction,
the first matching rows in walk order: ORDER BY the walk with the filter
in the WHERE clause and LIMIT the context size. The planner has no
statistics for property predicates; it estimates about one match per
seed, costs the ordered walk as reading the seed's whole side, and on
PostgreSQL plans a bitmap over that side and a sort instead. The read
then grew with the partition rather than with the context asked for.

With a filter, the context now comes from the seed's next 1,024
segments in walk order, read with no filter, and the filter and the
context size apply over those rows. The inner read has no filter to
misestimate, so it stays an ordered index scan, and it stops once the
context is found or the window ends. A match further than 1,024
segments from the seed is no longer context; the segment store ABC now
allows that bound. Unfiltered reads keep their statement. SQLite's
per-seed reads take the same window, so both dialects return the same
context.

Measured against MemMachine#1661's tip, 20 seeds per request with 2 segments back
and 4 forward, the rows of 21 partitions interleaved:

- PostgreSQL 16: a filtered request takes 8-12 ms instead of 4.5-7.1 s
  on a 1,000,000-segment partition, and 8-13 ms instead of 103-115 ms
  on 20,000-segment ones; unfiltered, 7.6 ms against 7.8 ms.
- SQLite, where the plan never read the whole side: filters matching
  20% or 1% take 1.1-1.2x as long as with the previous commit alone
  (14.1 vs 12.4 ms, 28.4 vs 24.2 ms), while a property that only the
  seeds carry takes 36 ms instead of 602 ms on a 200,000-segment
  partition, since the walk no longer runs toward the partition's end.
- For the same seeds, both dialects returned the same context as
  before, except where the bound applies: 12 forward with a filter
  matching every 100th segment returns the 10 matches within 1,024
  segments.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The helper orders segment rows by their chronological columns
(timestamp, event, index, offset), newest first when descending, the
order the store's context reads called chronological_order before
the window. _walk_order named no column or direction. The window
query's docstring now says its result comes nearest the seed first,
which holds on both sides.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Nothing depends on a power of two here: the bound is a LIMIT on the
segments a filtered context read takes per seed and side, and 1,000
is the round number.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Both context reads built the same walk before handing it to
_context_rows_query: the partition's segments past the seed on one
side, nearest first. _context_rows_query now takes the seed's ordering
values and the side, and builds the walk itself. Its callers pass only
what differs between them, bound parameters on SQLite and the seeds
subquery's columns on PostgreSQL, and no longer pair a range condition
with a direction flag that has to agree with it.

The walk carries the partition's liveness check, so every context
statement carries it from one place. PostgreSQL's statement had it in
its seeds subquery, which drops it; otherwise its SQL is unchanged, and
SQLite's is byte-identical. correlate_except(SegmentRow) replaces the
lateral's two correlate(seeds_subquery) calls: only the walk needed
one, when a property filter nests it in the window.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The docstring now says what the statement selects, what the seed's
ordering values may be, what the window bounds and why it is read
without the filter, and that a deleted partition yields nothing. The
account of how an unestimated filter turns the walk into a read of the
seed's whole side stays in the PR and MemMachine#1712.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Calls like _context_rows_query(values, True, max_backward_segments,
property_filter) did not say what True or the bound were. The side,
limit and filter of _context_rows_query and of the lateral's
per-direction helper, _chronological_order's descending, and
_resolve_segment_field's row are now keyword-only; each helper's
subject (the seed's ordering values, the row, the field) stays
positional.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The comment now names what is bound per seed (its timestamp, event
UUID, index and offset) and why: building a statement costs more CPU
than SQLite spends running it, so building one per seed would dominate
the read.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The existing context tests pass with the lateral's per-seed walk left
uncorrelated to its seed (only one filtered test, and it has one seed)
and with segments sharing a timestamp ordered by the wrong tie-breakers
(every test gives each segment its own timestamp). The new test reads
random contexts, several seeds at a time, with filters of every node
type, from a partition where many segments share a timestamp and
another partition holds segments at the same times, and compares each
result with an in-memory model that evaluates filters in SQL's logic.
Every partition is far smaller than the filtered read's window, so the
model has no bound.

It passes on both dialects here and at MemMachine#1661's tip, and fails on both
with the window ordered the same way on either side, with the filter
resolved against the table instead of the window, and with the
tie-breakers reordered, and on PostgreSQL with the walk uncorrelated.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Building the window's statement through an ORM alias adapted every
column the filter and the ordering read from it, and the walk went
through the mapped class. With a property filter that made building a
PostgreSQL request's two statements, with SQLAlchemy's cache key,
cost 1.6-1.8 ms of Python against 0.86 ms at MemMachine#1661's tip. The walk and
the window now use the table's and the window's own columns, and
SQLite's per-seed statements load segment rows through
select(SegmentRow).from_statement(), since building them from plain
rows cost more than ORM loading.

The SQL is byte-identical on both dialects. A filtered request now
takes 1.15-1.21 ms to build, of which about 0.3 ms is the window
itself: its subquery's columns and the larger statement to hash.
Filtered reads on PostgreSQL partitions of 200 and 2,000 segments,
20 seeds, went from 4-28% slower than MemMachine#1661's tip to 1-9%; SQLite
reads are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
ReadOnlyColumnCollection, what FromClause.c returns, is importable only
from sqlalchemy.sql.base and has no entry in SQLAlchemy's documentation.
ColumnCollection, its base class, is documented and exported from
sqlalchemy. Annotations only.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@edwinyyyu
edwinyyyu force-pushed the port/filtered-context-window-main branch from 1eb1b82 to 3541435 Compare September 25, 2026 18:06
edwinyyyu and others added 3 commits September 25, 2026 11:13
The SegmentStore ABC allowed a bound only on filtered context. It now
allows an implementation-defined bound on how many of the segments
nearest each seed context may come from, whatever the filter: a segment
that far from its seed is not a neighbor either way, and a hard bound
keeps every context read's cost bounded.

The store applies its window to unfiltered reads too by capping their
limit at it: an unfiltered read already reads exactly its context size,
so the cap needs no window subquery and costs nothing. The constant is
renamed _MAX_CONTEXT_DISTANCE, and the window's test now checks that an
unfiltered read stops at the bound; it fails with the cap removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The ABC now says what a caller can rely on, that each side's context
may be limited to an implementation-defined number of the segments
nearest the seed, rather than describing what an implementation does
beyond the bound.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The ABC now says only that an implementation may bound how far from
each seed it looks for context, so a side can come back with fewer
segments than requested even when more exist further away. It no longer
says how the bound is counted, so it neither reads as though context
might not match property_filter nor rules out an implementation with no
bound or with a bound on matching segments.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@edwinyyyu
edwinyyyu marked this pull request as ready for review September 25, 2026 18:25

@marvinyu-memverge marvinyu-memverge 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.

Approving at 4b6d25e. No asks from me.

What I checked:

  • Read the three files in full at head. On PostgreSQL the walk inside the window subquery correlates to the enclosing LATERAL's seed columns without re-selecting the seeds, so each seed gets its own bounded window. Moving the registry EXISTS out of the seeds subquery doesn't weaken liveness: the seed read still carries it, and the _ensure_partition_live read after the context statements still turns a mid-read delete into the stale-handle error.
  • Ran the module on SQLite at head: 101 pass.
  • The model test only runs on partitions well under the bound, so I also ran it with _MAX_CONTEXT_DISTANCE monkeypatched to 1, 2, 3 and 5, against a model that keeps the nearest N segments on each side and only then applies the filter and context size (1-5 seeds per read, shared timestamps, another partition at the same times, up to 6 per side so the unfiltered cap kicks in too). 32/32 agree, and all 32 fail with the model's bound off by one, so on SQLite the bound is exactly "the N nearest segments per side, then filter + limit". For PostgreSQL I'm relying on CI running the module.
  • Nothing merged to main since 66d4a65 (#1541, #1671) touches these files, and it merges cleanly.

@edwinyyyu
edwinyyyu marked this pull request as draft September 25, 2026 19:54
edwinyyyu and others added 6 commits September 25, 2026 12:59
The loop built both directions' statements on every read and ran each
only when its side asked for segments, so a read with no backward
context (expand_context of 1 or 2) built a statement it never used.
Each statement is now built only when its side asks for segments, as
the PostgreSQL path already does.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The bound test used one seed, and the model test's partitions never
reached the bound, so no test exercised the bound with several seeds,
filters and shared timestamps together. Each model-test read now sets
the bound at random, from one segment to the default, and the model
applies it before the filter: the nearest segments on each side, then
the filter and the requested count.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The lateral path built each context row by hand, passing ten keyword
arguments to SegmentRow's instrumented constructor, as main did. It now
selects each seed's UUID next to SegmentRow aliased to the lateral
subquery, so the ORM loads the rows, as the SQLite path does. The SQL
selects the same columns plus incarnation, and the contexts are
identical.

Measured against the previous commit in one quiet window, arms
alternated (20 seeds, 2 back and 4 forward unless noted): p50 at one
client 14-22% lower with a filter on the 1M-segment partition and the
20k one with a 20% filter (7.6 to 5.9 ms with 20%; 22.2 to 19.0 ms
with 1%, 4 back and 12 forward), and requests/s at 16 clients 26-39%
higher in every scenario. Building a request's two statements takes
0.95-0.98 ms with a filter instead of 1.18-1.22 ms, and 0.46-0.48 ms
without one instead of 0.74-0.78 ms, since the select list is one
entity instead of ten columns.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
_resolve_segment_field took either the mapped class or a column
collection, defaulting to the class, because the seeds query passed the
class and the window passed its columns. The seeds query now passes the
table's columns too, so the resolver takes one kind of argument, named
columns like _chronological_order's, with no default, and no longer
needs .expression for the timestamp column. The compiled SQL is
unchanged on both dialects.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The loop built and ran the backward and forward statements in two
near-identical blocks each. It now has the PostgreSQL path's shape: a
per-direction function that builds the direction's statement once and
runs it for every seed, called for a side only when that side asks for
segments. The statements are the same; all of a read's backward
statements now run before its forward ones.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This reverts commit 745c679. The bound's size is not part of the
contract, and bounds of a few segments are not realistic use. The
bound test already fails with the window's limit off by one or removed
and with the unfiltered cap removed, and the model test, without a
bound, already fails on PostgreSQL with a seed's walk uncorrelated to
its seed, the multi-seed risk the random bounds were meant to cover.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@edwinyyyu
edwinyyyu marked this pull request as ready for review September 25, 2026 22:41
edwinyyyu and others added 3 commits September 25, 2026 16:54
The model test's timestamps, properties, seeds and filters came from a
seeded generator, but segment, event and derivative identifiers came
from uuid4(), so which of two events sharing a timestamp came first
changed from run to run. They now come from the same generator, so
every run generates the same data. _seg takes an optional uuid for it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This reverts commit 9aac74f. The seeded generator already makes every
run test the same cases: the timestamps, properties, seeds, context
sizes and filters. Identifiers stay random UUIDs from uuid4(); they
only decide which of two events sharing a timestamp sorts first, which
the model orders the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Events sharing a timestamp are ordered by their UUIDs, which came from
uuid4() in generation order, so their order changed from run to run.
The test now draws its events' UUIDs from uuid4(), sorts them, and
hands them out in generation order, so every run tests the same cases,
including the order of tied events, while the UUIDs stay random.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@edwinyyyu
edwinyyyu merged commit 82b6c6f into MemMachine:main Sep 30, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Issues relating to MemMachine performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Filtered context expansion reads the seed's whole side of its partition on PostgreSQL

3 participants