Skip to content

feat(memories): occurredAt, when a Memory's event happened - #163

Merged
spinsirr merged 4 commits into
mainfrom
feat/memory-occurred-at
Oct 23, 2026
Merged

spinsirr merged 4 commits into
mainfrom
feat/memory-occurred-at

Conversation

@corespeed-agent-ensemble

@corespeed-agent-ensemble corespeed-agent-ensemble Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What changes

A Memory may now carry occurredAt: when the event or statement it records happened, as its writer states it. This is not createdAt or updatedAt. Those only say when Lore stored or changed the row, and for imported or late-written history they equal the import or writing time.

  • The writer supplies it as an RFC 3339 date-time with an offset, for example 2026-10-20T15:30:00-07:00. Lore keeps the UTC instant to the millisecond. The instant must fall in the UTC years 0001 to 9999.
  • Lore never infers it from content. It is never chunked, embedded, or used for ranking.
  • Every Memory response returns it: list, detail, search hit, context packet, and Workspace archive.
  • An update compares it as an instant. The same time with another offset writes nothing.

Surfaces:

Surface Change
Core validateMemoryOccurredAt, MEMORY_OCCURRED_AT_LIMITS, the column in every write and read
Schema Revision 18: memories.occurred_at, the Proposal columns, NOT VALID CHECKs, the outbox field, the review guard, a 19-argument lore.submit_memory_proposal, the memoryOccurredAt capability. Revision 19: validate the CHECKs. compatible_from stays 9.
HTTP / OpenAPI / SDK occurredAt on Memory create, update, output, Proposals, and context packets
CLI --occurred-at TIME, and --no-occurred-at on update and propose update
MCP The write tools ask the agent for occurredAt; search results and context packets return it
Web UI A "Happened (UTC)" field in the editor, a "Happened" row on Memory detail, and the proposed time in Proposal review
Archive Export writes lore-workspace-v4, which carries occurredAt; import still accepts v1 to v3
Eval harness Writes each LongMemEval-S and LoCoMo session's own time as occurredAt and shows it to the reader as an occurred-at attribute on each evidence tag

The first commit is a separate harness change: the default eval judge is now gpt-5.6-luna. gpt-5.6-sol stays as a profile, because stored verdicts name it.

A review fix makes keyed retries safe across the upgrade. A create or update response stored by revision 17 has no occurredAt, so its replay now answers occurredAt: null (replayedMemory).

A dependency commit patches next from ^16.3.8 to ^16.4.2. bun audit began failing CI on new advisories: two critical remote code execution advisories and one high denial of service, covering 16.0.0 through 16.4.0. The installed @opennextjs/cloudflare 1.20.8 already accepts it, so Wrangler is unchanged. The Next build, the 401 smoke test, the edge middleware entry, the OpenNext build, and wrangler deploy --dry-run all pass.

Why

On LongMemEval-S, the harness writes a session's date once, at the top of its first chunk. On main, only about 22% of the passages the reader sees carry a date. The reader then cannot order events, or picks the only dated value, which is often the older one.

Before building this, I measured the effect with a harness-only stand-in that put each passage's session time in front of it. The test used all 500 questions, the same retrieval, and the reader gemini-3.5-flash.

Judge Base With times Fixed / broken p
gpt-5.6-sol 57.4% 61.0% 24 / 6 0.0014
gpt-5.6-luna 57.0% 60.4% 23 / 6 0.0023

Under both judges the verdict was improved with no category regression. temporal-reasoning went from 47.2% to 57.5% (14 fixed, 1 broken).

Eval gate (this PR, real product path, Luna judge)

bun run eval gate --base origin/main --dataset locomo,longmemeval-s --tier standard: no-evidence on both.

Dataset Base Candidate Fixed / broken p
LongMemEval-S (140) 62.1% 65.0% 6 / 2 0.29
LoCoMo (200, token F1) 46.8% 47.1% — —

LongMemEval-S by category: knowledge-update +15.0 (3 / 0), temporal-reasoning +10.0 (2 / 0), preference −5.0 (1 / 2). LoCoMo turns already start with DATE:, so no change was expected there. Retrieval is unchanged, and there were no isolation failures. Reader input grows by about 7% on LongMemEval-S and 29% on LoCoMo, because of the extra attribute.

The standard tier is too small for this effect; the stand-in also showed no evidence at standard (p = 0.23).

The full 500-question gate passes: improved. I ran bun run eval gate --base origin/main --dataset longmemeval-s --tier full with the Luna judge, on this branch's product path at 43bc345. The commits after it change only the next version and replay handling, not search or the reader.

Category Base Candidate Fixed / broken
All (500) 57.0% 60.6% 23 / 5
temporal-reasoning (127) 47.2% 57.5% 14 / 1
multi-session (121) 43.8% 46.3% 3 / 0
knowledge-update (72) 66.7% 68.1% 3 / 2
single-session-user (64) 68.8% 70.3% 1 / 0
single-session-preference (30) 26.7% 26.7% 2 / 2
single-session-assistant (56) 76.8% 76.8% 0 / 0
abstention (30) 96.7% 96.7% 0 / 0

The overall gain is +3.6 points, 95% interval [+1.6, +5.8], McNemar p = 0.00091, with no category regression. Retrieval is unchanged at 97.9% all-gold, and there were no isolation failures. Reader input grows from 2,538 to 2,854 tokens per question (+12%).

Decisions for the PM

  • Scope: event time only. This does not add a validity interval (valid_from / valid_until) or Memory revision history ("what did Lore believe on date X"). The experiment did not test those, and the second needs a decision on whether edited or deleted content is retained.
  • No time filters or recency ranking. Search still filters only on updated_at. Filters such as occurredAfter would need a new lore.lexical_candidates signature, so they are left for a follow-up.
  • Instant only, no stored offset. A writer's local offset is not kept, so a time near midnight can show a different calendar day in UTC. The UI labels the field "(UTC)".
  • Archive format v4. A release older than schema revision 18 refuses a v4 archive. Upgrade the target before moving a Workspace to it.
  • Agents are asked for more. The MCP write tools now ask for occurredAt whenever content records an event. This adds a small token cost per write, and search output grows by one field per result.
  • Default judge change. Luna-graded scores are not comparable with Sol-graded ones. The harness keeps them apart, because the judge is part of every cache key.

Verification

  • Local: design:check, typecheck, lint, test (1,537 tests), build, packages:smoke, smoke:next, Cloudflare build, and wrangler deploy --dry-run all pass. One exception: tests/modules/code/code-index.test.ts › "accounts for every Git tree entry" fails here and on main in this sandbox, because its global gitignore hides the fixture's dist/.
  • New tests:
    • core validation and update semantics
    • HTTP writes, replays, refusals, and visibility under RLS and scope change
    • outbox fields
    • Proposals, including a review-guard refusal when an older instance drops the time
    • archive v4 golden checksum, round-trip, and refusals
    • schema drift: the CHECK equals the validator at the range edges
    • a revision-17 → 19 upgrade test that runs a revision-17 application's statements
    • MCP, CLI, UI helpers, and harness rendering

🤖 Generated with Claude Code

Agent and others added 4 commits October 23, 2026 02:23
The harness's default judge is now GPT-5.6 Luna through the logged-in
Codex CLI, at the same low reasoning effort as Sol. The Sol profile
stays, because stored runs and verdicts name it; a profile's identity is
part of every cache key, so Luna re-grades stored answers without new
reader calls. Re-grading the 500-question LongMemEval-S runs of
2026-10-22 with Luna kept their comparison's verdict (improved, p =
0.0023, against p = 0.0014 with Sol).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Memory may now carry occurredAt: when the event or statement it records
happened, as its writer states it. It is not createdAt or updatedAt, which
for imported or late-written history only say when Lore stored the row.
The writer supplies it as an RFC 3339 date-time with an offset; Lore keeps
the UTC instant to the millisecond, never infers it from content, never
chunks, embeds, or ranks it, and returns it with every Memory: list,
detail, search hit, context packet, and archive.

- Core: validateMemoryOccurredAt (UTC years 0001-9999), the column in
  every write and read, and an update that compares the instant, so the
  same time with another offset writes nothing.
- Schema revision 18 adds memories.occurred_at, its Proposal columns, NOT
  VALID CHECKs, the outbox field, the review guard, a 19-argument
  lore.submit_memory_proposal, and the memoryOccurredAt capability;
  revision 19 validates the CHECKs. compatible_from stays 9.
- HTTP, OpenAPI, SDK, CLI (--occurred-at, --no-occurred-at), MCP (write
  tools ask for it; search results return it), Proposals, and the web
  editor, Memory detail, and Proposal review.
- Workspace archives move to lore-workspace-v4, which carries occurredAt;
  import still accepts v1 to v3. The per-Memory export allowance grows
  from 256 to 320 bytes.
- The eval harness writes each LongMemEval-S and LoCoMo session's own
  time as occurredAt and shows it to the reader on each evidence tag.

Why: on 500 LongMemEval-S questions, only about a fifth of reader
passages carried their session's date, because it is written once at the
top of a session. Showing each passage's time raised answers from 57.0%
to 60.4% (23 fixed, 6 broken, p = 0.0023), temporal-reasoning from 47.2%
to 57.5%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bun audit now flags next 16.0.0 through 16.4.0: two unauthenticated
remote code execution advisories (GHSA-2464-fhr4-w9xw in image
optimization with SVG files, GHSA-673q-mjm6-r5xr in `use cache`
functions returning typed arrays) and one denial of service in Server
Actions (GHSA-p3m5-59mp-jv5r). Next 16.4.2 fixes all three. The pinned
@opennextjs/cloudflare 1.20.8 accepts next >= 16.3.8, so Wrangler stays
as it is. typecheck, next build, smoke:next (401 for an
unauthenticated GET /), the edge middleware entry, the OpenNext build,
and wrangler deploy --dry-run pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vision 18

A keyed create or update replays the body its first attempt stored. A body
an instance of revision 17 stored has no occurredAt, so during the 24-hour
ledger window after the upgrade a retry would answer a Memory without the
field the contract now requires. The Memory service answers such a replay
with occurredAt null, which is what that Memory had; a body that states
the field is returned unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@corespeed-hopper corespeed-hopper 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.

Approved at 2690bb8 after three review rounds.

Reviewed the diff from merge base f7abe1a, including timestamp validation and mutation semantics, migration and Proposal guards, RLS visibility, archive v4, public clients, browser fields, and evaluation plumbing. The legacy retry-body finding is fixed for both create and update. I independently reproduced that issue with PGlite and ran the focused regression after the fix. No findings remain.

All required checks passed on this exact head: static, tests, database, packages, build, and the aggregate check. CI evidence: https://github.com/corespeed-io/lore/actions/runs/39890131828. The Next.js dependency fix clears the previously failing audit.

The PR records the product trade-offs and reports the standard evaluation gate as no-evidence. Its full evaluation is still pending as stated; this technical approval does not claim a measured quality improvement.

Reviewer session: f2ae8d47-9773-4c4a-a4ce-3a8f772bc656.

@spinsirr
spinsirr merged commit e8e1be9 into main Oct 23, 2026
7 checks passed
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