Conversation
Adds an optional `occurred_at` field to `AnalyzeRequest` and threads
it through to the extractor as a `<context occurred_at="..."/>` tag
in the LLM prompt. extract.v6 resolves in-turn relative-time references
("last Friday", "yesterday") to absolute dates *inside the extracted
fact text* — so the resolved date ends up in (a) the SEAL-encrypted
blob on Walrus and (b) the embedding vector. Targets the dominant
LOCOMO/LME temporal failure mode (78% of failing temporal answers
hinge on a date; 96.6% of those dates were absent from retrieved
memories on the v5 baseline).
Architecture A: the resolved date lives ONLY inside the encrypted fact
text + embedding. There is NO new metadata column on `vector_entries`.
The server can never filter or rank by event time — that's the
privacy-floor-preserving trade. Full design context on WALM-55.
Bench result (baseline preset, vs 2026-05-21 extract.v5 archive):
- LOCOMO temporal: 45.2 → 62.3 (+17.1) — past noise floor, mechanism confirmed
- LME temporal: 60.1 → 65.3 (+5.2) — past noise floor
- LOCOMO overall: 67.4 → 69.95 (+2.55)
- LME overall: 77.9 → 78.42 (+0.5)
Other categories within ±3 J judge-noise floor.
This commit also contains three transient-failure fixes diagnosed during
the bench investigation. Previously these dropped turns silently because
they returned HTTP 500 (not in the SDK retry set):
1. New `AppError::UpstreamUnavailable` variant → HTTP 503. Maps three
upstream failure shapes from `AppError::Internal`:
(a) Transport-level: `resp.text().await` fails — connection drop or
HTTP/2 stream reset mid-body.
(b) OpenRouter error envelope wrapped in HTTP 200 OK
(`{"error":{"message":"...","code":NNN}}` with no `choices`
field). Detected by new `parse_openrouter_error_envelope` helper
with a `choices.is_none()` guard to avoid false-positives on
bodies containing both fields.
(c) `gpt-4o-mini` HTTP 200 with `content: null` and
`completion_tokens: 0`. Fixed at the type level
(`ChatMessageResp.content: Option<String>`) — degrades to empty
string, which `parse_extracted_facts` correctly returns as zero
facts (same as the prompt's explicit NONE response).
Net effect: LME bench completion went from 91.5% (pre-fix) to 99.99%
(with fix), with the harness's existing retry policy now recovering
transient upstream failures.
2. Body-read switched from `resp.json()` to `resp.text() →
from_str(&body)` at both call sites (extractor + embedder) so the
envelope detection can short-circuit before deserialise.
3. The `Option<String>` content ripples to two other consumers of
`ChatMessageResp` outside the extractor: the `ask` endpoint
(`routes/admin.rs`) preserves its 'No response from LLM' fallback;
the long-text summariser (`routes/remember.rs`) keeps its empty
guard.
Trait change: `Extractor::extract_with_context` signature extended
with `occurred_at: Option<chrono::DateTime<chrono::Utc>>`. Default
impl ignores it (backwards-compatible for any future Extractor impls).
`LlmExtractor` builds 1–3 user messages depending on which contexts
are present, short-circuits to plain `extract` when both are absent.
10 new Rust tests pinning: envelope detection across 5 body shapes
(including the choices+error edge case), occurred_at block rendering
in two timezones, default-impl pass-through for occurred_at, the v6
prompt anti-leak rules, the 503 mapping, and the upstream_unavailable
observability label.
Files:
- src/types.rs — AnalyzeRequest.occurred_at, AppError::UpstreamUnavailable
- src/services/llm_chat.rs — ChatMessageResp.content: Option<String>
- src/services/extractor.rs — trait sig + impl + helpers + tests
- src/services/embedder.rs — parity envelope detection + test
- src/services/prompts/extract.txt — extract.v6 prompt
- src/routes/analyze.rs — pass occurred_at through
- src/routes/admin.rs — Option<String> ripple
- src/routes/remember.rs — Option<String> ripple
Wires the LOCOMO and LongMemEval harnesses to send each turn's session timestamp as the new `occurred_at` field on `/api/analyze`. Without this, the previous commit's server-side capability would be dormant on bench runs (the source datasets carry per-session dates that were being silently dropped before reaching the extractor). Both adapters normalise their source-specific date formats to RFC 3339 UTC at parse time so `Turn.timestamp` is a uniform shape by the time `build_ingest_text_per_turn` packages turns for ingestion: - LOCOMO: `"1:56 pm on 8 May, 2023"` → `"2023-05-08T13:56:00+00:00"` via new `_normalize_locomo_timestamp` helper. Unparseable strings return None with a warning log — that turn just goes through without a temporal anchor (graceful degradation, never fabricate now()). - LongMemEval: `"2023/04/10 (Mon) 17:50"` → `"2023-04-10T17:50:00+00:00"` inline at the `Turn` construction site (LME's existing `_parse_haystack_date` already produced a datetime; the change is just to .replace(tzinfo=UTC).isoformat() at assignment). Naive-UTC convention: source datasets don't carry timezone info. The harness treats naive timestamps as UTC for benchmark-mode consistency across runs. Documented in the adapter comments. Not acceptable for production callers — but the production SDKs don't expose `occurred_at` yet (deferred follow-up). API ripples through the harness: - `MemWalClient.analyze(text, namespace, occurred_at=None)` — optional param, sent on the JSON body only when not None. - `build_ingest_text_per_turn` return type widened from `list[tuple[str, str]]` to `list[tuple[str, str, str | None]]` — the new third element is the per-turn RFC 3339 timestamp. - `build_ingest_text_naive_concat` returns `(label, text, None)` — the concat helper collapses many turns into one blob, so picking any one turn's timestamp would be arbitrary. Documented in the docstring. - `run.py::stage_ingest` unpacks the new 3-tuple and threads `occurred_at` into `client.analyze()`. Type annotations updated. 4 new tests pin the normalisation behaviour: - LOCOMO canonical-format → exact RFC 3339 UTC output - LOCOMO unparseable input → None + warning - LOCOMO None input → None - LME fixture session dates → RFC 3339 UTC on the Turn The two existing per-turn-chunking tests in both adapters are updated to unpack the new 3-tuple and additionally assert the occurred_at shape (RFC 3339 with T separator + +00:00 or Z suffix). Files: - benchmarks/core/client.py — analyze() accepts occurred_at - benchmarks/benchmarks/base.py — build_ingest_text_* return triples - benchmarks/benchmarks/locomo.py — _normalize_locomo_timestamp - benchmarks/benchmarks/longmemeval.py — inline RFC 3339 normalisation - benchmarks/run.py — unpack 3-tuple, pass occurred_at - benchmarks/tests/test_adapters.py — coverage for normalisation
Removes the WALM-55: / MEM-57 prefixes from doc comments across the WALM-55 implementation. Explanatory text is preserved; only the ticket-prefix noise is stripped. Per code-review feedback — ticket IDs belong in commit messages / PR descriptions, not in source comments. No behaviour change. Symmetric diff (33 deletions / 33 insertions). Rust 267/267 + Python 42/42 tests pass.
Adds AnalyzeOptions object as an alternative to the (text, namespace) overload on analyze() and analyzeAndWait(). The new `occurredAt` field carries a Date or RFC-3339 string from the SDK caller to the server's AnalyzeRequest.occurred_at, where the extractor uses it as a temporal anchor (extract.v6 prompt resolves "last Friday" / "yesterday" to absolute dates inside the fact text before embedding/encryption). - New AnalyzeOptions type in types.ts; re-exported from index.ts. - normalizeAnalyzeOptions helper preserves backwards compatibility: passing a string still works as namespace. - occurredAtToWire normalises Date → RFC-3339 UTC with trailing 'Z' (matches server-side wire format). - analyzeAndWait threads the options object through analyze() so a single call captures both timestamp + wait semantics. No server change; field is already accepted on AnalyzeRequest. Backbone for the WALM-55 effect to reach JS/TS callers — the auto- capture hook and openclaw tools follow in subsequent commits.
Adds an optional `occurred_at` kwarg to MemWal.analyze(), MemWal.analyze_and_wait(), and their sync wrappers on MemWalSync. The field carries a datetime or RFC-3339 string to the server's AnalyzeRequest.occurred_at, where the extractor uses it as a temporal anchor (extract.v6 prompt resolves 'last Friday' / 'yesterday' to absolute dates inside the fact text before embedding/encryption). - _occurred_at_to_wire helper normalises datetime → RFC-3339 UTC with trailing 'Z'. Naïve datetimes are assumed UTC; aware datetimes are converted to UTC. String inputs pass through verbatim. - Field is omitted from the request body when None, so existing callers see no wire-format change. - 3 new tests pin the wire format across datetime-aware, datetime- naïve, and string inputs. Full suite still passes (34/34).
The agent_end auto-capture hook now passes `occurredAt: new Date()`
to analyze(), so every agent-captured conversation gets the WALM-55
temporal-anchor treatment: the server extractor resolves in-turn
relative references ('yesterday', 'last Friday') into absolute dates
inside the fact text before embedding/encryption.
Wall-clock time is the honest answer at this site because the hook
fires on agent_end — milliseconds after the conversation. This is
the highest-leverage WALM-55 win on the agent side: every Claude
Code / Claude Desktop / openclaw user benefits with no API surface
change.
The 'server must not silently default to now()' rule is preserved:
this is the caller passing now() with real knowledge of when the
event occurred, not the server inventing a fallback for an absent
field.
The memory_store agent tool now exposes an optional `occurredAt` arg
that flows through to the server's analyze() AnalyzeRequest.occurred_at.
Agents can now anchor recounted past events to specific dates ('user
mentioned they moved last March') by populating occurredAt with the
date they reason the event actually happened.
Distinguishes from the auto-capture hook (prior commit): there,
new Date() is the best honest approximation because capture fires
right after the conversation. Here, the agent has to reason about
when the event happened — which is why occurredAt is optional with
no default ('Omit when unknown; do not guess' is in the description).
Bumps the openclaw-memory-memwal SDK dependency from published ^0.0.2
to workspace:* so the AnalyzeOptions signature from the SDK commit is
visible. The published 0.0.2 wasn't being actively consumed anyway
(workspace apps already use workspace:* for the SDK); this just
aligns openclaw with that pattern.
The matching MCP analyze tool (services/server/scripts/mcp/tools/
analyze.ts) needs the same treatment but is deferred — services/
server/scripts/ is not currently a pnpm-workspace member, so wiring
it to the workspace SDK is a separate restructuring task.
Build verified clean: pnpm build passes on openclaw-memory-memwal
with the new AnalyzeOptions signature linked from the workspace SDK.
Replaces 162 instances of "MemWal" with "Walrus Memory" across 53 .md/.mdx files. Only prose references are updated — code identifiers (MemWal, MemWalManual, withMemWal, MemWalConfig, etc.), package names (@mysten-incubation/memwal), environment variables (MEMWAL_*), URLs (memwal.ai), inline code, and code blocks are all preserved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CHANGELOG files should preserve original branding at time of release. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Updates prose across docs to reflect the new messaging framework: - Category: "Portable agent memory" - Tagline: "Take your agent's memory anywhere" - Pillar 1: Portable by Design — memory across agents, apps, workflows - Pillar 2: Fully Under Your Control — programmable permissions, explicit ownership - Pillar 3: Built for Agent Coordination — shared memory spaces, verifiable integrity Replaces "privacy-first", "persistent, encrypted", and "long-term memory" framing with portability, owner control, and coordination language. Reverts changelog files from prior commit — release notes should preserve original branding at time of release. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds release metadata for the WALM-55 SDK changes shipping in this PR: - Changeset: @mysten-incubation/memwal patch (0.0.6 → 0.0.7) for the new AnalyzeOptions overload exposing optional occurredAt. - Changeset: @mysten-incubation/oc-memwal patch (0.0.3 → 0.0.4) for the auto-capture hook timestamping new captures with new Date(), the optional occurredAt arg on memory_store, and the workspace:* dep bump. - Python SDK manual bump: 0.1.3 → 0.1.4 across pyproject.toml + __init__.py + CHANGELOG.md entry, covering the new occurred_at kwarg + the SDK-boundary input validation rules. Patch bumps are sufficient because every new field is optional and omitted from the wire body when unset — existing callers see byte-identical request payloads. Verified with @changesets/cli: `pnpm changeset status` shows both TS packages slated for patch.
…urning wallet retries A wallet job that fails because a Sui owned object/version is locked to a competing transaction was falling through classify_sidecar_error to the generic Transient arm. Apalis then retried across every wallet and fired a misleading "wallet retries exhausted" alert, even though the object is locked until the lock clears (typically the next epoch boundary) and every immediate retry re-fails against the same object. - Add WalletJobError::ObjectLockedUntilEpoch — a third disposition that is neither Transient (don't retry into the same lock) nor Permanent (the input can succeed in a later epoch). aborts_retries() gates both the Apalis Abort and the exhausted-retries alert so a single locked object no longer consumes the whole wallet budget. - Classifier branch matches "already locked by a different transaction", ">1/3 of validators by stake", "non-retriable", "equivocated/equivocation", and "reserved for another transaction", ahead of the MoveAbort->Permanent catch and distinct from the recoverable "locked at version" case. - Parse locked object id / version / locking digest from the error string to enrich the alert and the persisted row. - New WalrusObjectLockedAlert with copy that names the real cause and surfaces the object id, version, and locking digest; deduped per (network, object_id) via a reusable AlertDedup so an equivocation burst doesn't spam. Window env-overridable (WALRUS_OBJECT_LOCK_ALERT_DEDUP_SECS). - Aborting errors now persist status='failed' instead of leaving the row stuck on 'running' forever (no further retries will touch it). Tests pin the exact production error string and cover abort disposition, the no-exhaust gate, metadata parsing, phrase variants, and no regression of balance::split / EWrongVersion recoverable classes.
…assifier Mirror the Rust classifier so sidecar-side wallet metrics categorize the non-recoverable owned-object lock separately from the recoverable "locked at version" class. Adds isWalrusObjectLockEquivocation (kept in sync with the Rust classify_sidecar_error patterns) and a dedicated walletObjectLockEquivocationTotal counter surfaced via /metrics/wallet. Also corrects the adjacent metric comments: the earlier "Sui no longer permanently locks coin objects on equivocation" claim is contradicted by the production incident, so the equivocation counter is now the canary for whether concurrent uploads are still equivocating owned objects. Tests cover the exact production error string, each phrase variant, case-insensitivity, and that the recoverable lock-at-version class does NOT match.
Closes two silent-data-loss paths flagged in code review: (1) extractor.rs: `ChatMessageResp.content: null` previously degraded to empty string and returned a successful 202 with facts=[]. The SDK/harness can't distinguish that from 'no memorable content' and will not retry, so the turn was silently dropped. Now maps to AppError::UpstreamUnavailable → HTTP 503 so the retry policy (_RETRY_STATUS = 429, 502, 503, 504) recovers. The explicit string "NONE" from the LLM remains the valid no-facts path (handled by parse_extracted_facts, unchanged). (2) extractor.rs + embedder.rs: non-success upstream HTTP statuses (429 + 5xx) previously routed to AppError::Internal → HTTP 500, which the harness does not retry by design. Now classified via new is_upstream_status_transient helper: 429 + 5xx → UpstreamUnavailable (retryable); other 4xx → Internal (real bug, retrying won't fix). Mirrors the 200-wrapped envelope handling for the case where the upstream surfaces the same condition as a raw HTTP status. The harness retry tuple stays (429, 502, 503, 504) — no 500 retry — so unknown internal errors still surface as genuine bugs. New unit test pins is_upstream_status_transient across the 429 + 5xx transient set and the 4xx + 2xx non-transient set. Rust suite: 268/268.
Three doc/description fixes from code review: (a) memory_store tool description (openclaw): previously instructed the agent to 'pass the date the event occurred' for recounted past events — exactly inverted from the server's actual semantics where occurredAt is the timestamp of the source text/conversation, not the historical event date. Failure mode: user says 'remember I called Alex last Friday' on Monday, agent reads our description and passes last Friday's date as occurredAt, extractor then resolves 'last Friday' relative to that — fact stamped a week early. New description matches the server prompt's contract: pass the conversation time for real-time stores; OMIT for recounted past events. (b) Python SDK docstring: said 'naïve datetimes are assumed UTC' but the implementation rejects them with ValueError (deliberately, to avoid silent timezone-off-by-N anchors for non-UTC callers). Docstring now matches behaviour: aware datetimes only; naïve raises; string inputs must carry Z or UTC offset. (c) Dropped the 'same approach as Mem0's memory_store' comment from store.ts header — kept the architectural rationale (analyze() vs remember() for fact extraction) without the third-party vendor attribution.
Feat: WALM-55 — extract.v6 timestamp injection for temporal recall
Address review: a bare "non-retriable" / ">1/3 of validators by stake"
preamble is not lock-specific — a generic invalid transaction or a
non-lock MoveAbort is also non-retriable — so matching on it alone
misclassified those as ObjectLockedUntilEpoch.
Now the object-lock branch requires either a lock/equivocation-specific
anchor ("already locked by a different transaction", "reserved for
another transaction", "equivocated"/"equivocation") OR the non-retriable
preamble corroborated by object-lock evidence in the same message
("object (" + "locked"). Mirrored in the TS detector.
Also fix the upload/set-metadata log lines: retryable was derived from
!is_permanent(), which printed retryable=true for ObjectLockedUntilEpoch
even though it aborts. Use !aborts_retries() so the log matches the
actual Apalis disposition.
Adds negative tests: a bare non-retriable invalid tx stays Transient and
a bare non-retriable MoveAbort stays Permanent — neither becomes an
object lock.
…ck-classifier fix(server): WALM-77 detect Sui object-lock / equivocation, stop burning wallet retries
Codex/docs memwal root redirect
hungtranphamminh
approved these changes
Jun 2, 2026
jasong-03
approved these changes
Jun 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.