Skip to content

OpenClaw backfill reads message fields one level too high, projecting 0 rows - #552

Merged
philcunliffe merged 3 commits into
masterfrom
fix/issue-543
Aug 2, 2026
Merged

OpenClaw backfill reads message fields one level too high, projecting 0 rows#552
philcunliffe merged 3 commits into
masterfrom
fix/issue-543

Conversation

@philcunliffe

@philcunliffe philcunliffe commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Root cause

A real OpenClaw v3 type: "message" record is two levels deep. The record line
states only ['id', 'message', 'parentId', 'timestamp', 'type']; the message
itself (role, content, and on an assistant turn api, model, provider,
stopReason, usage) is nested under message.

parseOpenclawSessionMessage (session_file.js) read model, provider,
api, stopReason, and usage off the record line, so all of them came back
undefined. effectiveProviders then had no stated provider to propagate,
every record resolved to unknown, and PROJECTABLE_PROVIDERS excluded all of
them fail-closed. That is why a real ~/.openclaw scanned files_seen: 7, sessions_projected: 0, records_excluded: 18 and hyp status reported
backfill @hypaware/openclaw [done] (0 rows): a parse miss and an intended
exclusion are indistinguishable at that seam.

The same defect sat one stage later at both consumers, so fixing provider
alone would only have moved the drop:

  • backfill.js projectedMessageFromRecord read record.role / record.content
  • settle.js read message.record.role (rawRole) and message.record.content
    for the LLP 0159 content match key, so a real session settled nothing either

The fix

One envelope-shape change in the LLP 0158 reader, not a per-field patch and not
a loosened exclusion filter:

  • openclawMessageEnvelope(row) states the address once: the nested message
    object is the message envelope, with the record line as the fallback for a
    record that nests none. A field the envelope states is never overridden by a
    same-named field on the line.
  • role and content become normalized fields on OpenclawSessionMessage.
    Their address is the thing that was easy to get wrong, so they belong to the
    one reader rather than to each consumer's own reach into record. record
    stays exposed for genuinely caller-specific fields (parentId, toolCallId).
  • Both consumers now read the normalized fields, so neither can drift a level
    again.

usage needed no change: usageAttributes already accepts OpenClaw's
input / output / cacheRead / cacheWrite spelling.

Fixtures

All three OpenClaw suites now write the real two-level record shape through one
helper each (messageLine in the backfill suite, sessionFileLine in the
settlement suite, literal nested records in the reader suite). The old fixtures
asserted a flat envelope OpenClaw never writes, which is exactly why CI stayed
green through this bug. A new test pins the on-disk bytes against the shape
verified on a live install, so a fixture cannot quietly re-invent the flat one.

LLP 0158 is updated in the same commit: the verified record shape, read rule 5
(the envelope address and why it fails quietly), the "a field two callers must
locate identically is the reader's" consequence, and the path-faithful-fixture
consequence.

Test evidence

The added regression test is
test/plugins/openclaw-backfill.test.js ->
a mixed real-shape session partially projects: anthropic turns land, claude-cli turns stay excluded,
the mixed-session case from the issue: an anthropic turn and a claude-cli
turn in one real-shape file must partially project.

Before (on the fixed fixtures, unfixed source):

TAP version 13
# Subtest: a mixed real-shape session partially projects: anthropic turns land, claude-cli turns stay excluded
not ok 1 - a mixed real-shape session partially projects: anthropic turns land, claude-cli turns stay excluded
  ---
  duration_ms: 7.13412
  type: 'test'
  location: '.../test/plugins/openclaw-backfill.test.js:544:1'
  failureType: 'testCodeFailure'
  error: |-
    the anthropic half of the session must project

    0 !== 1

  code: 'ERR_ASSERTION'
  name: 'AssertionError'
  expected: 1
  actual: 0
  operator: 'strictEqual'
  ...
1..1
# tests 1
# pass 0
# fail 1

After:

TAP version 13
# Subtest: a mixed real-shape session partially projects: anthropic turns land, claude-cli turns stay excluded
ok 1 - a mixed real-shape session partially projects: anthropic turns land, claude-cli turns stay excluded
  ---
  duration_ms: 8.39832
  type: 'test'
  ...
1..1
# tests 1
# pass 1
# fail 0

Whole-suite reproduction, on the path-faithful fixtures against the unfixed
reader (this is the bug's blast radius, not one test's):

test/plugins/openclaw-backfill.test.js    # tests 21  # pass 6   # fail 15
test/plugins/openclaw-session-file.test.js # tests 20 # pass 16  # fail 4
test/plugins/openclaw-settlement.test.js  # tests 16  # pass 7   # fail 9

After the fix, all three are green (21/21, 20/20, 16/16), as are the other six
OpenClaw suites.

Repo gates:

npm test       -> # tests 3276  # pass 3275  # fail 0  # skipped 1
npm run typecheck -> clean

No pre-existing failures to disclose: the suite is fully green before and after.

Deliberately not in this PR

The issue's two adjacent decisions are design questions, not this defect, and
neither costs rows today (both parse to nothing rather than to something wrong):

  • whether *.trajectory.jsonl files should be skipped explicitly by the scanner
    rather than silently parsed-and-empty
  • whether probe-anthropic-* / probe-claude-cli-* attach-probe sessions should
    be imported or skipped by session-id prefix

Both want their own short decision LLP rather than a silent filter here.

Fixes #543

test and others added 2 commits July 31, 2026 22:00
A real OpenClaw v3 `type: "message"` record states only `id`, `parentId`,
`timestamp`, and `type` on the record line: `role`, `content`, `model`,
`provider`, `api`, `stopReason`, and `usage` are all nested under `message`.
The LLP 0158 reader read them off the record line, so every field came back
absent, every record resolved to `provider: unknown`, the backfill allowlist
excluded all of them, and `hyp status` reported `backfill @hypaware/openclaw
[done] (0 rows)` for a session it had failed to read. The settlement enricher
was broken the same way one seam later (`record.role`/`record.content`), so a
real session settled nothing either.

The reader now owns the envelope address: fields are read from the nested
`message` object, falling back to the record line for a record that nests
none, and `role`/`content` are normalized fields rather than something each
consumer picks out of the raw record. Both consumers read them off the
normalized message, so neither can drift a level again.

Fixtures across the three OpenClaw suites now write the real two-level shape
through one helper each; the old flat fixtures asserted an envelope OpenClaw
never writes, which is why the suite stayed green through the bug.

LLP 0158 records the verified record shape, the envelope read rule, and the
path-faithful-fixture consequence.

Co-Authored-By: Claude <noreply@anthropic.com>
… is the line's

Follow-up to the #543 envelope fix, from review of PR #552.

- `messageField` fell back on key *absence*, not value *usability*, so a
  present-but-unusable nested value permanently masked a good record-line
  one. A nested `provider: "  "` beside a line-level `provider: "anthropic"`
  resolved the record to `unknown` and the allowlist excluded it fail-closed;
  a nested `timestamp` that did not parse dropped `message_created_at`, which
  re-dates the row to session start, defeats the `--since` window (a
  timestamp-less item is kept unconditionally) and puts the settlement
  ordinal match outside every window so the turn never dedupes. Rule 3's
  present-value test now runs at both levels before the fallback decides.
- `id` is now read line-first, envelope-fallback. LLP 0158 verified message
  identity on the record line; envelope-first meant a future OpenClaw that
  copied the provider's own id into the nested message would silently repoint
  every `message_id` and `part_id`, so committed rows would stop deduping
  against new ones and the history would double with nothing raised.
- The OPENCLAW_HOME relocation fixture still wrote the invented flat shape,
  bypassing `messageLine`, so it passed with the envelope read reverted. It
  now goes through the helper, and the shape pin carries `idempotencyKey` so
  it matches the live key list the same file documents.
- Say which LEVEL `record` is: `parentId` is on the line, `idempotencyKey`
  and `toolCallId` are at `record.message`. The old wording invited the very
  read #543 was.
- LLP 0158 gains rules 6 and 7 for the two behaviors above, and the stale
  "no live OpenClaw install was reachable" note on `usageAttributes` is
  reconciled with the spelling this work verified.

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

Copy link
Copy Markdown
Contributor Author

Neutral review round: PR #552 @ 44c7080

Verdict: findings (5 actionable, all fixed and pushed as df7f119).

The core change is right and I could not break it. The two-level address is correct, both consumers now read the normalized fields, and there is no remaining raw read of role/content/provider/model anywhere in the repo (readOpenclawSessionMessages has exactly two callers, backfill.js and settle.js, both converted). The allowlist was not weakened: PROJECTABLE_PROVIDERS, effectiveProviders and partitionByBackend are byte-identical to master, claude-cli is still excluded with covered_by: claude_transcript, and unknown still fails closed. The LLP 0159 match key's semantics are unchanged: the stored openclaw.match_key still comes from wireMatchKey at capture, and only the session-file side of the comparison moved, so settlement of already-stored rows is strictly repaired rather than altered.

What the fallback rule does under stress is where the findings are.


1. High: messageField fell back on key absence, not value usability, so a garbage nested value permanently masked a good record-line one

session_file.js:335-338 (pre-fix)

function messageField(envelope, row, key) {
  const value = envelope[key]
  return value === undefined ? row[key] : value
}

Every other read in the file obeys the reader's own rule 3 ("unconfirmable is unresolvable"): nonBlankString and parseTimestampMs reject a present-but-unusable value. messageField ran before those, so an unusable envelope value consumed the read and the line was never consulted. Verified against the PR head through readOpenclawSessionMessages:

record result
line provider:"anthropic", nested provider:" " provider: undefined
line timestamp:"2026-07-30T00:00:00.000Z", nested timestamp:"not-a-date" timestampMs: undefined
line usage:{input:10,output:20}, nested usage:null usage: undefined

That value is absent (rule 3) and load-bearing at the same time. Downstream, each of those is a silent drop of the same family as #543:

  • lost provider resolves the record to unknown, and the allowlist excludes it fail-closed
  • lost timestampMs omits message_created_at (backfill.js:518-520), so the materializer substitutes conversation_started_at and the row lands in the wrong date partition; filterByWindow (src/core/backfill/scan_util.js:68) keeps timestamp-less items unconditionally, so --since imports records the window should exclude; and settle.js:381 sets Infinity, which is outside every window, so the ordinal fallback can never match, part_id dedupe never collapses the pair, and the turn double-counts, which is exactly what R11 exists to prevent

Fixed (session_file.js:363-366): the field's own present-value test now runs at both levels before the fallback decides. content and usage got named present-value tests (statedValue, plainObject) so every field is guarded the same way.

2. Medium: id was read envelope-first, contradicting LLP 0158's own statement of where identity lives

session_file.js:275 (pre-fix) read id through the same envelope-first path as content fields, while the PR's own LLP text says the record line states "what identifies and positions the message, ['id','message','parentId','timestamp','type']". Verified: a record with id at both levels resolved to the nested one.

No upside (the verified shape never nests id) and an unrecoverable downside. The nested envelope is OpenClaw's normalization of a provider response and already carries idempotencyKey; a version that also copied the provider's msg_01... id there would repoint every backfilled message_id (backfill.js:517) and every settled one (settle.js:471), and therefore every part_id. Committed rows would stop deduping against new ones and the history would double, with nothing raised anywhere.

Fixed (session_file.js:287): id is line-first, envelope-fallback, so a record that states identity only in the envelope still resolves one. LLP 0158 gains rule 7 stating the exception and why.

3. Medium: the PR's own fixture-fidelity guarantee was already false

The LLP addition claims "Every test that writes a session file writes the real two-level record shape, through one helper per suite, so no fixture can quietly re-invent a flat envelope." test/plugins/openclaw-backfill.test.js:682 bypassed messageLine() and wrote JSON.stringify({ type: 'message', ...ASSISTANT_RECORD }) by hand. Measured: with the envelope read reverted, that test still passed while 15 others failed, so the sole test of the relocated-OPENCLAW_HOME path was not a regression guard at all.

Separately, the new shape pin at :236 asserted nested keys without idempotencyKey, contradicting the live key list stated 190 lines above it and in LLP 0158 Context. The one test whose job is "this is the shape OpenClaw appends" pinned a shape OpenClaw does not append.

Fixed: the relocation fixture routes through messageLine, and ASSISTANT_RECORD carries idempotencyKey so the pin matches the documented key list. Re-verified by mutation: with openclawMessageEnvelope regressed to return row, the relocation test now fails (it did not before).

4. Medium: record was documented at the wrong level, inviting the #543 read

session_file.js:253-254 and types.d.ts:26 told the next caller that parentId, idempotencyKey and toolCallId are "reachable through record, the untouched record line". Per LLP 0158's verified key list idempotencyKey is nested, and match_key.js:175 states toolCallId/toolName "live on the message". So record.idempotencyKey is undefined, and the doc written to prevent one-level-too-high reads was itself instructing one.

Fixed: both sites now name the level. parentId is on the line; a message-level field this reader does not normalize is at record.message.

5. Low: timestamp precedence flipped versus master, unpinned

Master always used row.timestamp; the PR prefers the nested one, and a real record states both. I could not construct a failure from the precedence itself (the two differ only by append latency, inside every tolerance), but no test pinned it: both fixture helpers write the identical timestamp to both levels, so the precedence is unobservable in the suite, and only the envelope-absent direction was covered.

Fixed by pinning rather than changing: a new reader test states different values at the two levels and asserts envelope-wins for timestamp and line-wins for id in one place. Four more reader tests cover the blank/wrong-typed nested field, the blank-nested-with-no-line-value case, and a message key that is not an object.


Not changed, worth a human's eye

  • No hermetic smoke writes an OpenClaw session file, in any shape. There is no backfill_openclaw_fixture analog to the Codex and Claude flows, so tier 2 has never touched a real-shape OpenClaw session file and the only gate that would have caught OpenClaw backfill projects 0 rows from real session files: message fields are read at the top level but OpenClaw nests them under 'message' #543 is the manual docs/ACCEPTANCE.md:437. Given CLAUDE.md's three-tier model, that gap is the reason this bug shipped green, and it outlives this PR.
  • A projectable record that yields no message is counted nowhere (backfill.js:441, if (!message) continue), so scan_complete can under-report against messages_projected + records_excluded. Pre-existing, not introduced here.
  • LLP 0158 Decision still says "The invariant is tested once in core", which contradicts the same document's decision to keep the reader plugin-local. Pre-existing; this PR added a bullet beneath it without touching it.

Gates

npm test 3281 tests / 3280 pass / 0 fail / 1 skipped, npm run typecheck clean, at df7f119. Each fix was positively verified by mutation, not by a green suite: reverting the guard-order fix fails only a blank or wrong-typed nested field reads as absent..., reverting the id direction fails only the envelope wins for timestamp, the record line wins for id, and reverting openclawMessageEnvelope now fails the relocation test it previously passed.

House style checked: no em dash (U+2014) anywhere in the change, no statement-terminating semicolons, no @typedef and no inline import('...') types, and the three added @refs resolve to LLP 0158's ## Decision with correct attachment.

…xception

Three follow-ups to the round-1 fixes, all in the same family the round-1
findings were: a new behavior that no test pins, and docs that state a rule
the code does not follow.

- `statedValue`, the `content` present-value test the guard-order fix
  introduced, was entirely unpinned: replacing it with the identity function
  left the whole suite green while a nulled-out nested `content` suppressed a
  usable record-line value and landed `content: null` on the message. Pinned
  in both directions (line supplies it; absent when neither level does).
- `types.d.ts` still stated the blanket envelope-first rule over `id`, which
  the same commit made line-first (LLP 0158 rule 7), and over `content`,
  whose test refuses only `null`. The published declaration is what a package
  consumer reads, so it now names both exceptions.
- LLP 0158 rule 6 claimed a blank or wrong-typed nested value can never
  suppress the line. True of the string fields, false of `content`: a nested
  `content: "   "` or `content: 42` does suppress it. The rule now says what
  "reads as absent" is per field.

Also extends the non-object-`message` test with the `null` case, the one
input where the plain-object guard is the difference between reading the
record line and throwing out of the whole file read.

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

Copy link
Copy Markdown
Contributor Author

Neutral review round 2: PR #552 @ df7f119

Verdict: findings (3 actionable, all Low, all fixed and pushed as b60ca6a).

This round targeted the round-1 delta 44c7080..df7f119. All five round-1 fixes are real and I verified each by mutation rather than by a green suite. The three findings below are the same family the round-1 findings were, one level further in: a behavior the fix introduced that no test pins, and docs that state a rule the code does not follow.


Verification of the round-1 fixes

1. The reordered guard (session_file.js:363-366) is correct in both directions. Probed at runtime through readOpenclawSessionMessages, not reasoned about:

record result
line provider:"anthropic", nested provider:" " provider: "anthropic"
line timestamp:"2026-07-30T00:00:00.000Z", nested timestamp:"nope" line's value
line usage:{input:1}, nested usage:null {input:1}
line content:"from-line", nested content:null "from-line"
nested-only {id:" ", timestamp:"", model:42, stopReason:null} all four absent, nothing substituted

The resurrection direction holds too: on the verified real shape the record line carries only ['id','message','parentId','timestamp','type'], so a user turn whose envelope legitimately omits model/provider/usage has nothing to resurrect them from. I confirmed the per-field fallback does pick up a same-named line field when one exists (a synthetic line-level provider:'openai' reaches a user turn), but that is rule 6 working as documented, it is unchanged from before the round-1 fix, and it is unreachable on the shape OpenClaw writes.

Mutation: reverting messageField to the absence-based version fails exactly one test, a blank or wrong-typed nested field reads as absent... (61/62 pass).

2. The id direction flip is safe downstream. Line-first restores master's behavior exactly (master read nonBlankString(row.id)), so nothing regressed. Both part_id producers consume the same reader field: backfill.js:517 (projected.message_id = message.id, the gateway derives part_id from it) and settle.js:471-476 (settled.message_id = match.id, settled.part_id = match.id + '#' + partIndex). The settlement content match key does not depend on id at all: buildOpenclawSessionIndex keys on sessionMatchKey(rawRole, message.content) and uses id only as the value to upgrade to. match_key.js is byte-identical to master. Mutation: reverting to envelope-first fails exactly one test, the envelope wins for timestamp, the record line wins for id.

3. The fixture fix is real and was load-bearing. With openclawMessageEnvelope regressed to return row, 31 of 62 OpenClaw tests fail including a relocated install is found through OPENCLAW_HOME (#17). I then reverted only the fixture line back to JSON.stringify({ type: 'message', ...ASSISTANT_RECORD }) and re-applied the same regression: test #17 goes green again. That is direct proof the round-1 fixture change converted a test that passed for the wrong reason into a real guard. Dropping idempotencyKey from ASSISTANT_RECORD fails the shape pin, so the pin is honest against LLP 0158's live key list. Every settlement fixture routes through sessionFileLine (writeSessionFile, openclaw-settlement.test.js:171); no raw type:'message' literal bypasses a nesting helper in any OpenClaw suite.

4. Whole-PR sanity re-confirmed at df7f119. PROJECTABLE_PROVIDERS, effectiveProviders and partitionByBackend are byte-identical to master (only line numbers shift); claude-cli still excludes with covered_by: claude_transcript and unknown still fails closed. readOpenclawSessionMessages still has exactly two non-test callers, backfill.js:266 and settle.js:325, and no message.record.<field> read survives anywhere. match_key.js unchanged, so LLP 0159 match-key semantics are untouched. The PR's real diff against its merge-base (57e2ec9) is 8 files; master has since moved to be07015 (#554) but touched nothing under openclaw/, so there is no semantic conflict.

House style clean: no U+2014 anywhere in the diff, no statement-terminating semicolons, no @typedef, no inline import('...') types, and test/core/llp-ref-hygiene.test.js passes on the added @refs.


Findings

1. Low: statedValue, the new content present-value test, was pinned by nothing

session_file.js:378 (at df7f119)

function statedValue(value) {
  return value === null ? undefined : value
}

This function is new in the round-1 guard-order fix and is the only thing making a nulled-out nested content fall through to the record line. I replaced its body with return value (the pre-fix behavior) and ran the whole repo suite: 3281/3281 green. Nothing observed it. That is precisely the "a test that passes under both the fixed and the buggy reader is not pinning anything" case, and the consequence is live: with the identity version, a nested content: null both suppresses a usable line value and lands content: null on the message, which projectedMessageFromRecord drops (backfill.js:502-506) and which buildOpenclawSessionIndex hashes into a content key no live row can match (settle.js:388).

Fixed by adding a nulled-out nested content is unstated, so the record line still supplies it, which pins both halves (the line supplies it; with neither level stating it the key is absent, not null). Re-verified: the identity mutant now fails that test and only that test, 61/62.

2. Low: types.d.ts still stated the blanket envelope-first rule the same commit had carved two exceptions out of

types.d.ts:20-23 (at df7f119): "each is read off the nested message envelope, falling back to the record line, and is absent when the field is missing, non-string (for the string fields), or blank."

Both clauses are now false for a listed field. id is line-first as of the same commit (session_file.js:287, LLP 0158 rule 7), and content is neither string-tested nor blank-tested. This is the round-1 finding-4 defect in the one file it matters most in: types.d.ts is what npm run build:types publishes, so it is the description a package consumer reads, and it currently points them the way #543 was read.

Fixed: the interface doc now names both exceptions.

3. Low: LLP 0158 rule 6 overclaimed, and the code is the honest one

llp/0158-one-reader-for-openclaw-session-jsonl.decision.md:74-76 (at df7f119): "a nested value that reads as absent (blank, wrong-typed, null) cannot also be the value that suppresses the line."

True of every string field and of usage. False of content, whose test refuses only null. Verified at runtime: a nested content: " " and a nested content: 42 each suppress a line-level content: "from-line".

I deliberately did not "fix" this by tightening statedValue. The narrow test is the right call and is already argued at its own JSDoc: content has no single shape, and refusing a blank string would change message.content from '' to undefined, which changes the settlement content key for a turn whose stored match_key was computed at wire time. The overclaim is in the doc, so the doc moved: rule 6 now says "reads as absent" is each field's own answer, and states content's.

Also folded in (not a separate finding): the a message key that is not an object test (openclaw-session-file.test.js:374) asserted only outcomes that are identical with and without the isPlainObject guard, so it pinned nothing. It now covers message: null, the one input where the guard is the difference between reading the record line and throwing out of the whole file read.


Not fixed, and why

  • No hermetic smoke writes an OpenClaw session file in any shape. Reported in round 1, out of scope here by instruction, and it outlives this PR.
  • The per-field fallback can read a same-named field off the record line for a turn whose envelope deliberately omits it. Confirmed at runtime, but it is documented rule 6, it predates the round-1 fixes, and it is unreachable on the verified record shape. Not a defect; recorded so the next reader does not re-find it.
  • timestamp remains envelope-first while id is line-first, though LLP 0158 names both as line-level positioning fields. Round 1 pinned this rather than changing it; I could not construct a failure from the precedence either, both consumers read the same reader so they cannot disagree, and flipping it in a final review round would be a behavior change with no evidence behind it. Left as pinned.

Gates

At b60ca6a: npm test -> # tests 3282 # pass 3281 # fail 0 # skipped 1; npm run typecheck clean. (df7f119 baseline was 3281 / 3280 / 0 / 1.) Each fix positively verified against the committed blob: the new test name and the changed types.d.ts / LLP 0158 sentences are present in b60ca6a and absent in df7f119.

No open findings.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral triage: PR #552 @ b60ca6a

The review fix-loop hit its 2-round cap (44c7080 → 5 findings fixed as
df7f119, df7f119 → 3 findings fixed as b60ca6a), which routed this PR to
triage rather than a third review round. Both rounds reported every finding
they raised was fixed, not left open, so triage's job was to verify that claim
against the tree rather than assume a backlog.

Verification performed (in a scratch worktree at b60ca6a, npm install

  • npm test = 3282/3281/0/1, matching round 2's reported gates exactly):
  • Grepped every finding's cited symbol/location (messageField,
    statedValue, openclawMessageEnvelope, the id line-first read, the
    messageLine/sessionFileLine fixture helpers, types.d.ts, LLP 0158
    rules 6-7) and confirmed each fix is present in the committed tree exactly
    as both rounds describe.
  • Independently mutation-tested the three highest-risk claims rather than
    trusting the review prose: reverting statedValue to identity fails
    exactly 1 test (a nulled-out nested content is unstated...); reverting
    id to envelope-first fails exactly 1 test (the envelope wins for timestamp, the record line wins for id); reverting
    openclawMessageEnvelope to return row fails 31/62 OpenClaw tests
    including the OPENCLAW_HOME relocation test. All three match the rounds'
    own mutation claims. No raw record.role/record.content reads remain in
    backfill.js or settle.js.

Residual set — two items, both explicitly named by round 2 as deferred by
design, not unresolved defects:

  1. Hermetic smoke gap: no backfill_openclaw_fixture analog to
    backfill_codex_fixture.js / backfill_claude_fixture.js exists under
    hypaware-core/smoke/flows/. This is the actual reason OpenClaw backfill projects 0 rows from real session files: message fields are read at the top level but OpenClaw nests them under 'message' #543 shipped
    green. Classified non-blocking: it is a coverage gap, not a
    behavioral defect — nothing misbehaves today because of it.
  2. timestamp/id precedence asymmetry: timestamp resolves
    envelope-first, id resolves line-first (deliberate, LLP 0158 rule 7).
    Round 2 pinned it with a dedicated test and could construct no failure;
    both consumers share the one reader so they cannot disagree. Classified
    non-blocking: pinned, argued, and unreachable as a defect under the
    current one-reader design.

Both residuals are non-blocking, so the PR is clear to merge. Filed
#555 to carry them forward with
enough detail to act on later (including what a backfill_openclaw_fixture
flow would need to stage and assert).

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

Labels

neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenClaw backfill projects 0 rows from real session files: message fields are read at the top level but OpenClaw nests them under 'message'

1 participant