oficina / memoria Public

Review 5: a late row got through the deadline; three guards saw nothing

Reviewed by Codex GPT-6-Astra (dispatched: codex/gpt-6-astra; self-
reported "GPT-6 (Codex)"), of a8287b5 alone. REQUEST CHANGES: one
runtime defect, two ineffective guards, and the Home gap 5b1ac4e had
already half-closed.

**A row could still come back after its deadline.** The deadline was
checked on entry to each proposal and nowhere after, so the last
proposal's recogniser pass could run past it and its row be returned: a
proposal arriving with 10 ms left against a 1.26 MB passage produced a
model row at 1.01 s. The deadline is now also checked after the
acceptance loop, in the labelling loop, and before returning. A single
recogniser pass is not interruptible, so latency can still overrun by
one passage's scan — the spec comment says so — but the rows are never
late. Guarded by the reviewer's shape with its presence half: the same
proposal with time to spare returns the row.

**Caching, dedupe and the per-proposal check were all invisible to the
suite** — the thousand-identical-proposals test passed with any of them
removed, because dedupe collapsed the thousand to one before the others
mattered. Rather than delete them, measured whether they CAN be seen:
one recogniser pass over 1.23 MB is ~200 ms, over 410 KB ~70 ms, and a
near-miss strings.Index over 1.23 MB ~1 ms. At those sizes each one's
absence lands past the 800 ms deadline, so each now has a guard sized
from the measurement: a thousand distinct present proposals (caching,
166 ms against 881 ms), a thousand identical near-misses (dedupe, 219 ms
against 1.02 s), 2,500 distinct near-misses (the per-proposal check,
1.02 s against 2.58 s). Each fixture asserts it is ONE chunk, because
the first was split small by the chunker. One threshold was set at
300 ms against a 219 ms baseline and widened to 600 ms before commit.

**The Home footer guard now names every route and ignores status.** It
accepted "at least one page made a claim" and checked only 200s, so the
confirm page — which renders its failure at 400 — was never looked at.
The memoriatest walk likewise checks a footer at any status now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 867612cee74a417d0d5bccb514b207c0c74892e2 parent 5b1ac4e
5 files changed, +173 −59
  • docs/superpowers/specs/2026-09-03-memoria-design.md +6 −1
  • internal/memoria/home_router_test.go +15 −11
  • internal/memoria/modelfacts_test.go +131 −45
  • internal/memoria/store.go +17 −1
  • internal/memoriatest/footer_model_test.go +4 −1

CI passed — run details

CI log
=== vet ===
go vet ./...
=== fmt ===
=== test ===
go test ./...
?   	amadan.net/oficina/memoria/cmd/memoria	[no test files]
ok  	amadan.net/oficina/memoria/internal/memoria	4.372s
ok  	amadan.net/oficina/memoria/internal/memoria/chunk	(cached)
ok  	amadan.net/oficina/memoria/internal/memoria/fact	(cached)
?   	amadan.net/oficina/memoria/internal/memoria/icons	[no test files]
ok  	amadan.net/oficina/memoria/internal/memoria/index	(cached)
ok  	amadan.net/oficina/memoria/internal/memoria/notion	(cached)
ok  	amadan.net/oficina/memoria/internal/memoria/provider	(cached)
ok  	amadan.net/oficina/memoria/internal/memoria/query	(cached)
ok  	amadan.net/oficina/memoria/internal/memoria/seal	(cached)
ok  	amadan.net/oficina/memoria/internal/memoria/sibling	(cached)
ok  	amadan.net/oficina/memoria/internal/memoriatest	1.575s
?   	amadan.net/oficina/memoria/tools/bench	[no test files]
?   	amadan.net/oficina/memoria/tools/drive	[no test files]
?   	amadan.net/oficina/memoria/tools/seed	[no test files]
?   	amadan.net/oficina/memoria/tools/vendorassets	[no test files]
=== migration-check ===
go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check
rastrillo migration check: models and migrations agree
diff --git a/docs/superpowers/specs/2026-09-03-memoria-design.md b/docs/superpowers/specs/2026-09-03-memoria-design.md
index b57e8b5..03c68ec 100644
--- a/docs/superpowers/specs/2026-09-03-memoria-design.md
+++ b/docs/superpowers/specs/2026-09-03-memoria-design.md
@@ -3659,7 +3659,12 @@ claim could be false **outside** the path the section above describes:
instance" while a search had just sent passages. It now states the
switch: with AI on it names TensorX and the trigger; with AI off it
makes the strong claim, which is then true. A gate walks every declared
- route, so a page cannot skip it.
+ GET route and checks each footer at whatever status it renders; a route
+ that renders no footer must be on a list with its reason, or the walk
+ fails. The pages that exist only when Home is configured are checked by
+ name, through the Home fixture. (The first version of that gate skipped
+ non-200 routes silently and so missed all four Home pages — found by
+ review, then fixed.)
- **Processing time.** The deadline covered the call and not the work on
its response; a legal-sized response of repeated proposals took 6.8 s to
validate. One deadline now covers both, identical proposals are
diff --git a/internal/memoria/home_router_test.go b/internal/memoria/home_router_test.go
index 1a326ad..59fc973 100644
--- a/internal/memoria/home_router_test.go
+++ b/internal/memoria/home_router_test.go
@@ -304,37 +304,41 @@ func TestHomeSigninClearsOnlyDefinitivelyStaleSession(t *testing.T) {
// The footer's egress claim on the pages that exist only when Home is
// configured. TestTheFooterStatesTheModelSwitch (memoriatest) walks every
// declared route, but its harness does not configure Home, so these 404
-// there and cannot be checked — this is where they are. They render
-// through renderHome, the Render callback the Home library is given.
+// there — this is where they are checked. They render through renderHome,
+// the Render callback both the Home routes and its Bridge are given.
+//
+// **Every route explicitly, at whatever status it renders.** The first
+// version accepted "at least one page made a claim" and checked only
+// 200s, so /account/home/confirm — which renders its failure page at 400 —
+// was never looked at, and removing ModelOn from renderHome left the
+// suite green until the next commit added this. A review found both.
func TestHomePagesStateTheModelSwitch(t *testing.T) {
+ routes := []string{"/signed-out", "/account/home", "/account/home/confirm", "/signin/local"}
for _, on := range []bool{true, false} {
f := newHomeRouterFixture(t)
if on {
f.app.store.extractor = &recorder{}
}
f.local(t)
- checked := 0
- for _, path := range []string{"/signed-out", "/account/home", "/signin/local"} {
+ for _, path := range routes {
w := f.do("GET", path, "")
b := w.Body.String()
i := strings.Index(b, "<footer")
- if w.Code != 200 || i < 0 {
+ if i < 0 {
+ t.Errorf("AI on=%v, %s (%d): no footer, so this route is unchecked", on, path, w.Code)
continue
}
foot := strings.Join(strings.Fields(b[i:]), " ")
if !strings.Contains(foot, "leaves this instance") {
+ t.Errorf("AI on=%v, %s (%d): the footer makes no egress claim", on, path, w.Code)
continue
}
- checked++
if on && (strings.Contains(foot, "no document ever") || !strings.Contains(foot, "TensorX")) {
- t.Errorf("AI on, %s: footer does not disclose TensorX: %q", path, foot)
+ t.Errorf("AI on, %s (%d): footer does not disclose TensorX", path, w.Code)
}
if !on && (!strings.Contains(foot, "no document ever") || strings.Contains(foot, "TensorX")) {
- t.Errorf("AI off, %s: footer does not make the true claim: %q", path, foot)
+ t.Errorf("AI off, %s (%d): footer does not make the true claim", path, w.Code)
}
}
- if checked == 0 {
- t.Fatalf("AI on=%v: no Home page rendered a footer with a claim; this proves nothing", on)
- }
}
}
diff --git a/internal/memoria/modelfacts_test.go b/internal/memoria/modelfacts_test.go
index 6b51bd9..f3bb9f3 100644
--- a/internal/memoria/modelfacts_test.go
+++ b/internal/memoria/modelfacts_test.go
@@ -840,60 +840,128 @@ func TestAModelRowNamesTheProviderAndTheModel(t *testing.T) {
}
// **A model response is bounded in bytes, and its processing must be
-// bounded in time too.** A review sent 1,000 identical proposals — a
-// valid 54 KB response, under the cap — against a 64 KB passage, and
-// validation took 6.8 seconds, after the provider's deadline had already
-// expired and with nothing checking for cancellation. The recognisers ran
-// once per proposal; they now run once per passage, identical proposals
-// are validated once, and the cap stops the work as soon as it is certain.
+// bounded in time too.** Each subtest guards one mechanism, and each was
+// found unguarded by review: a thousand-identical-proposals test passed
+// with caching removed, with dedupe removed, and with the per-proposal
+// deadline check removed. The fixtures are sized from measurement so the
+// version WITHOUT the mechanism lands past the 800 ms deadline and the
+// version with it lands far inside: one recogniser pass over this 1.23 MB
+// passage is ~200 ms, one over the 410 KB one ~70 ms, and a near-miss
+// strings.Index over 1.23 MB ~1 ms (measured 2026-09-27).
+//
+// Every passage is ONE chunk — chunk.Split bounds by words, and the dense
+// part has no spaces — asserted, because the first fixture here was split
+// small by the chunker and passed against the unfixed code.
func TestAModelResponseIsCheapToProcess(t *testing.T) {
- // ONE large chunk, dense with recognisable facts. chunk.Split bounds a
- // chunk by WORDS, so text without spaces stays whole: the first version
- // of this fixture used ordinary sentences, the chunker cut it into
- // small passages, and the old per-proposal behaviour ran fast enough to
- // pass. This is the reviewer's shape — a single 60 KB passage.
- big := "## Contact\n\nOffice address: 12 Main Street, Dublin 2.\n" +
- strings.Repeat("D02X285,info@tito.io,", 3000)
-
- t.Run("a thousand identical proposals", func(t *testing.T) {
+ const unit = "D02X285,info@tito.io," // two recognised facts, no spaces
+ build := func(t *testing.T, prefix string, reps int) *Store {
+ t.Helper()
s := storeForTest(t)
- upload(t, s, "Tito office", big)
- s.extractor = &recorder{reply: func([]string) ([]provider.Pointer, error) {
- out := make([]provider.Pointer, 1000)
- for i := range out {
- out[i] = provider.Pointer{Passage: 0, Text: "12 Main Street, Dublin 2"}
+ upload(t, s, "Tito office", "Office address: "+prefix+strings.Repeat(unit, reps))
+ return s
+ }
+ onePassage := func(t *testing.T, calls [][]string) {
+ t.Helper()
+ if len(calls) != 1 || len(calls[0]) != 1 {
+ t.Fatalf("sent %v passages; the fixture must be ONE chunk or it measures nothing", lens(calls))
+ }
+ }
+ timed := func(t *testing.T, s *Store, q string) (time.Duration, []Fact) {
+ t.Helper()
+ st := time.Now()
+ p := searchFacts(t, s, q)
+ took := time.Since(st)
+ t.Logf("search took %v", took)
+ return took, modelRows(p)
+ }
+
+ // Mutation: the only deadline check is on entry to each proposal. The
+ // proposal arrives with 10 ms left, and its recogniser pass takes ~200
+ // ms: the row must not come back. The presence half, with no wait, is
+ // what shows the row exists to be dropped.
+ t.Run("a row whose processing crosses the deadline is dropped", func(t *testing.T) {
+ for _, late := range []bool{false, true} {
+ s := build(t, "12 Main Street, Dublin 2,", 60000)
+ rec := &recorder{}
+ s.extractor = ctxAwareRecorder(rec, func(ctx context.Context) {
+ if late {
+ if dl, ok := ctx.Deadline(); ok {
+ time.Sleep(time.Until(dl) - 10*time.Millisecond)
+ }
+ }
+ }, point(0, "12 Main Street, Dublin 2"))
+ _, rows := timed(t, s, "office address")
+ onePassage(t, rec.calls)
+ if !late && len(rows) != 1 {
+ t.Fatalf("no row with time to spare (%d); the late case proves nothing", len(rows))
+ }
+ if late && len(rows) != 0 {
+ t.Errorf("a row came back after its deadline had passed: %+v", rows)
}
- return out, nil
- }}
- start := time.Now()
- rows := modelRows(searchFacts(t, s, "office address"))
- // Against the 6.8 s measured, with an order of magnitude to spare
- // for a slow machine; the fixed code takes a few milliseconds.
- if took := time.Since(start); took > 700*time.Millisecond {
- t.Errorf("took %v to process one repeated proposal", took)
}
- if len(rows) != 1 {
- t.Errorf("rows = %d, want 1: a thousand copies are one proposal", len(rows))
+ })
+
+ // Mutation: recompute the recognisers per proposal. A thousand DISTINCT
+ // proposals that are all present, against a dense passage: cached, one
+ // ~70 ms pass; uncached, a thousand, cut off at the deadline.
+ t.Run("the recognisers run once per passage", func(t *testing.T) {
+ var prefix strings.Builder
+ var props []provider.Pointer
+ for i := 0; i < 1000; i++ {
+ a := fmt.Sprintf("Unit%d-Harbour-Cork", i)
+ prefix.WriteString(a + ",")
+ props = append(props, provider.Pointer{Passage: 0, Text: a})
+ }
+ s := build(t, prefix.String(), 20000)
+ rec := &recorder{reply: func([]string) ([]provider.Pointer, error) { return props, nil }}
+ s.extractor = rec
+ took, _ := timed(t, s, "office address")
+ onePassage(t, rec.calls)
+ if took > 400*time.Millisecond {
+ t.Errorf("took %v: the recognisers are running per proposal", took)
}
})
- t.Run("hundreds of distinct proposals are refused by the cap, quickly", func(t *testing.T) {
- s := storeForTest(t)
- body := "## Our addresses\n\n"
- var want []provider.Pointer
- for i := 0; i < 400; i++ {
- a := fmt.Sprintf("%d Harbour Street, Cork", i+1)
- body += "Address: " + a + ".\n"
- want = append(want, provider.Pointer{Passage: 0, Text: a})
+ // Mutation: drop the dedupe. A thousand IDENTICAL near-miss proposals:
+ // each would be a full ~1 ms scan of 1.23 MB; deduped, one.
+ t.Run("identical proposals are examined once", func(t *testing.T) {
+ s := build(t, "", 60000)
+ props := make([]provider.Pointer, 1000)
+ for i := range props {
+ props[i] = provider.Pointer{Passage: 0, Text: "D02X285,info@tito.iq"}
}
- upload(t, s, "Tito office", body)
- s.extractor = &recorder{reply: func([]string) ([]provider.Pointer, error) { return want, nil }}
- start := time.Now()
- if rows := modelRows(searchFacts(t, s, "address")); len(rows) != 0 {
- t.Errorf("rows = %d; four hundred addresses are past the cap", len(rows))
+ rec := &recorder{reply: func([]string) ([]provider.Pointer, error) { return props, nil }}
+ s.extractor = rec
+ took, _ := timed(t, s, "office address")
+ onePassage(t, rec.calls)
+ // 600 ms: the search alone — opening and snippeting a 1.23 MB chunk —
+ // is ~220 ms before the model path starts; without dedupe it is
+ // ~1.02 s, deadline-bound (measured 2026-09-27). 300 ms, the first
+ // threshold, left 80 ms of margin.
+ if took > 600*time.Millisecond {
+ t.Errorf("took %v: identical proposals are each being examined", took)
+ }
+ })
+
+ // Mutation: drop the per-proposal deadline check. 2,500 DISTINCT
+ // near-miss proposals — dedupe cannot help — at ~1 ms each is ~2.5 s of
+ // scanning; the check stops it at the 800 ms deadline. The threshold
+ // sits between, and only the deadline can get under it.
+ t.Run("the deadline stops the scan, not the end of the list", func(t *testing.T) {
+ s := build(t, "", 60000)
+ props := make([]provider.Pointer, 2500)
+ for i := range props {
+ props[i] = provider.Pointer{Passage: 0, Text: fmt.Sprintf("D02X285,info@tito.%dq", i)}
+ }
+ rec := &recorder{reply: func([]string) ([]provider.Pointer, error) { return props, nil }}
+ s.extractor = rec
+ took, rows := timed(t, s, "office address")
+ onePassage(t, rec.calls)
+ if took > 1400*time.Millisecond {
+ t.Errorf("took %v: the scan ran past the deadline to the end of the list", took)
}
- if took := time.Since(start); took > 700*time.Millisecond {
- t.Errorf("took %v", took)
+ if len(rows) != 0 {
+ t.Errorf("rows = %+v", rows)
}
})
@@ -916,3 +984,21 @@ func TestAModelResponseIsCheapToProcess(t *testing.T) {
}
})
}
+
+// ctxAwareRecorder records like recorder, runs wait with the context it
+// was given (to spend the budget), then replies.
+func ctxAwareRecorder(rec *recorder, wait func(context.Context), reply func([]string) ([]provider.Pointer, error)) Extractor {
+ return ctxThen{rec: rec, wait: wait, reply: reply}
+}
+
+type ctxThen struct {
+ rec *recorder
+ wait func(context.Context)
+ reply func([]string) ([]provider.Pointer, error)
+}
+
+func (c ctxThen) ExtractAddresses(ctx context.Context, passages []string) ([]provider.Pointer, error) {
+ c.rec.calls = append(c.rec.calls, append([]string(nil), passages...))
+ c.wait(ctx)
+ return c.reply(passages)
+}
diff --git a/internal/memoria/store.go b/internal/memoria/store.go
index 01df9e4..7a85f3d 100644
--- a/internal/memoria/store.go
+++ b/internal/memoria/store.go
@@ -1283,6 +1283,19 @@ func (s *Store) modelFacts(ctx context.Context, results []Result, texts map[int6
}
accepted = append(accepted, coord{ptr.Passage, start, end})
}
+ // **Checked AFTER the work, not only before each proposal.** The loop
+ // above checks on entry, so the last proposal's recogniser pass could
+ // run past the deadline and its row still be returned: a review had
+ // one proposal arrive with 10 ms left against a 1.26 MB passage and got
+ // a model row back at 1.01 s. A row the deadline has passed is dropped
+ // here, again in the labelling loop, and once more before returning.
+ //
+ // What this does NOT bound: a single recogniser pass is not
+ // interruptible, so the path can overrun by one passage's scan. The
+ // rows are never late; the latency can be, by that much.
+ if ctx.Err() != nil {
+ return nil
+ }
// **Sorted by (rank, offset) before anything is chosen**, so the
// provider's return order never decides which occurrence is cited.
@@ -1298,6 +1311,9 @@ func (s *Store) modelFacts(ctx context.Context, results []Result, texts map[int6
var out []Fact
seen := map[string]bool{}
for _, c := range accepted {
+ if ctx.Err() != nil {
+ return nil
+ }
text := passages[c.passage]
// 6. The span is sliced from the document. The model's own string
// is not used again.
@@ -1330,7 +1346,7 @@ func (s *Store) modelFacts(ctx context.Context, results []Result, texts map[int6
// is all this layer can apply them to. §8.9 says what that costs: an
// address the model did not return cannot trip the cap, and nothing
// here can know it was there.
- if len(out) > MaxFacts || collides(out) {
+ if len(out) > MaxFacts || collides(out) || ctx.Err() != nil {
return nil
}
return out
diff --git a/internal/memoriatest/footer_model_test.go b/internal/memoriatest/footer_model_test.go
index f4f1242..a096263 100644
--- a/internal/memoriatest/footer_model_test.go
+++ b/internal/memoriatest/footer_model_test.go
@@ -98,7 +98,10 @@ func footers(t *testing.T, h http.Handler, docID int64) map[string]string {
rec := get(t, h, path)
body := rec.Body.String()
i := strings.Index(body, "<footer")
- if rec.Code != http.StatusOK || i < 0 {
+ // A footer is checked at WHATEVER status it renders with: an error
+ // page carries the same footer and the same claim. The first
+ // version checked 200s only.
+ if i < 0 {
// **A skip has to be explained**, or the walk proves less than
// it says. Every route below renders no layout under this
// harness, for the reason given; anything else that fails to