A hermetic smoke writes a real-shape OpenClaw session file (#555 item 1) - #558
A hermetic smoke writes a real-shape OpenClaw session file (#555 item 1)#558philcunliffe wants to merge 6 commits into
Conversation
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>
…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>
#543 shipped green because tier 2 never touched an OpenClaw session file in any shape. Codex and Claude each have a hermetic flow that stages a real-shape transcript and drives `hyp backfill` against it; OpenClaw had no analog, so the only gate that could catch the two-level nesting bug was the manual acceptance smoke. Adds `backfill_openclaw_fixture`, mirroring `backfill_codex_fixture.js` and `backfill_claude_fixture.js`: boots `@hypaware/ai-gateway` + `@hypaware/openclaw` against a tmp HYP_HOME, stages a v3 session file under the fake HOME's `.openclaw/agents/<agentId>/sessions/<id>.jsonl`, drives `hyp backfill openclaw`, and asserts non-zero `ai_gateway_messages` rows with native record-line ids, envelope content, `provider=anthropic`, `conversation_source=openclaw`, the `backfill.provider_finish` / `backfill.write` spans and the `backfill.finish` log under the run's `dev_run_id`, and a zero-new-rows idempotent rerun. The fixture writer lives in `smoke/lib/openclaw_session_fixture.js` so no flow can invent a flatter, friendlier record shape, and takes a caller-chosen `mtimeMs` so the scheduled-sweep quiesce work (LLP 0173 T12) can extend it rather than replace it. Co-Authored-By: Claude <noreply@anthropic.com>
openclawMessageLine's JSDoc claimed writing `timestamp` at both levels "makes the envelope-first read of it observable," but the implementation copied the same destructured binding to both levels, so they were byte-identical and no assertion could tell envelope-first from line-first precedence. Add an optional `messageTimestamp` field that overrides the nested `message.timestamp` only, keeping the no-arg default (same value at both levels) unchanged since a real session file does write it that way. backfill_openclaw_fixture.js now stages the assistant record with a distinct envelope timestamp and asserts the projected row's `message_created_at` carries the envelope value, genuinely exercising the LLP 0158 / #555 envelope-first precedence. Also strengthen the run-2 idempotency assertion, which checked only `status === 'ok' && rows_written === 0` under a comment claiming "all part_ids already present" but never pinned that the session was actually re-read. A whole-file skip (e.g. the mtime-based quiesce-window skip LLP 0173 T12 will add, using the `mtimeMs` hook this PR introduced) would report the same zero rows_written and pass identically. Require `items_seen >= 1 && rows_skipped >= 1` too, so the predicate pins the dedupe mechanism the comment describes instead of being satisfied vacuously by a skip. Co-Authored-By: Claude <noreply@anthropic.com>
Review round 1 (head
|
Round-2 review nits on the tier-2 gate added for #543. Both are in the `session_projected` telemetry assertion, and this file exists purely as a regression gate, so assertion precision is its whole job. The filter matched on the log body alone, the only one of the four telemetry assertions not constrained to the run. Two records exist: the idempotency rerun dispatches with a fresh `DEV_RUN_ID`, and the plugin logger stamps it, so run 2 emits its own `session_projected` under `<dev_run_id>-rerun`. Reading `projected[0]` was correct only because emission order happens to put run 1 first, which is ordering rather than identity. Constrain it to `harness.devRunId` and this session's native `session_id`, matching what the three sibling filters already do. The `identity_source === 'native'` conjunct could never be false: the plugin hardcodes the string at `session_projected`'s single emission site, so the conjunct held whenever the log existed at all, while the comment read as though it separated a native-identity path from a fallback one. Drop it, and state what the assertion does pin: that the projection was reached, for this session, with both messages. The identity claim now sits where it can genuinely vary - `session_id` on the log, alongside the record lines' `message_id`s already asserted on the rows. No behavior change. Both round-2 mutations still fail the smoke: a flat `openclawMessageEnvelope` reproduces #543's `status: ok` / `rows_written: 0` on the first behavioral assertion, and swapping `messageField`'s envelope/line order for `timestamp` fails the envelope-precedence assertion with the record-line 10:00:02 value. Co-Authored-By: Claude <noreply@anthropic.com>
Review round 2 (head
|
|
The review fix-loop reached its 2-round cap, so this landed on the triage rung (LLP 0017). I independently re-verified all four findings raised across both review rounds against the
I also re-ran Zero unresolved findings remain. Per the task's guidance, an issue enumerating nothing would be Judgement: this PR can ship safely. |
|
| PR | Why it adds the file | What its version does |
|---|---|---|
| #558 | Fixes issue #555 (no hermetic smoke ever wrote a real-shape OpenClaw session file, which is why #543 shipped green) | Plain fixture smoke: real two-level envelope shape, non-zero rows, telemetry assertions, idempotency. Mutation-tested: reverting openclawMessageEnvelope to a flat read reproduces #543's signature and fails the smoke. Also adds hypaware-core/smoke/lib/openclaw_session_fixture.js. |
| #570 | Task T12 of LLP 0173, the openclaw-two-lane-capture change set |
Adds the Lane B dimensions the change set needs: quiesce-window behaviour (a file inside the window is skipped, outside it is captured) and a zero-duplicate assertion across both capture lanes. |
They were written to different specs and neither is a superset of the other, so I have not
picked a winner or merged them. That is a design call about what the smoke tier should
assert, and it is yours.
How this happened
When #558 was written I asked its author to factor the fixture writer out and make mtimeMs
caller-controllable specifically so T12 could extend it rather than replace it. T12 was then
implemented from LLP 0173's own task spec, independently, and produced its own version. The
two efforts never saw each other because they ran on branch-disjoint workers, which is the
same isolation that lets them run in parallel.
Options
- Merge OpenClaw two-lane capture (LLP 0172/0173): Lane A attach, Lane B sweep, json_path revival #570 first, then reconcile A hermetic smoke writes a real-shape OpenClaw session file (#555 item 1) #558 onto it — keeps the richer two-lane version, and
A hermetic smoke writes a real-shape OpenClaw session file (#555 item 1) #558's distinctive value (the mutation-tested regression gate for OpenClaw backfill projects 0 rows from real session files: message fields are read at the top level but OpenClaw nests them under 'message' #543) gets folded in. - Merge A hermetic smoke writes a real-shape OpenClaw session file (#555 item 1) #558 first, then reconcile OpenClaw two-lane capture (LLP 0172/0173): Lane A attach, Lane B sweep, json_path revival #570 onto it — keeps the mutation-tested gate as the
base, and T12's quiesce/dedupe cases get added on top. - Tell me which to keep and I will reconcile the other onto it and re-verify from CI.
Say which on either thread and I will carry it out on the next tick. Until then both PRs stay
held.
Related: #552 was squash-merged, so #558's branch still carries the original pre-squash
commits. GitHub still reports it MERGEABLE against master, so this is informational only,
but it is why #558's diff looks wider than its own changes.
Follow-up to PR #552. Actions item 1 only of issue #555; item 2 is deliberately not actioned (see below).
What landed
hypaware-core/smoke/flows/backfill_openclaw_fixture.js, the missing sibling ofbackfill_codex_fixture.js/backfill_claude_fixture.js. It:@hypaware/ai-gateway+@hypaware/openclawagainst a tmpHYP_HOME.openclaw/agents/<agentId>/sessions/<sessionId>.jsonlwith a
type: "session"v3 header and twotype: "message"records in the verified two-levelshape: the record line states only
id/message/parentId/timestamp/type, androle,content,model,provider,api,stopReason,usageare nested undermessagecannot quietly flatten the fixture back into something the pre-OpenClaw backfill reads message fields one level too high, projecting 0 rows #552 reader would also pass
hyp backfill openclawthroughdispatch/createCommandRegistry, mirroring the Codex flowai_gateway_messages(the literal OpenClaw backfill projects 0 rows from real session files: message fields are read at the top level but OpenClaw nests them under 'message' #543regression, which reported
status: okwithrows_written: 0), carrying the record lines'native
message_ids, the envelope'scontent_textandmodel, andprovider = anthropic/conversation_source = openclaw/client_name = openclawbackfill.provider_finishspanand a
backfill.finishlog under the run'sdev_run_idwith matching row counts, abackfill.writespan forai_gateway_messages, plus the provider's ownopenclaw.backfill.session_projectedlog withidentity_source = nativestill returns exactly two
hypaware-core/smoke/lib/openclaw_session_fixture.jsholds the fixture writer(
openclawMessageLine/writeOpenclawSessionFixture), mirroringtest/plugins/openclaw-backfill.test.js'smessageLine()andtest/plugins/openclaw-settlement.test.js'ssessionFileLine()rather than inventing a shape.Flows are resolved by filename by
hypaware-core/smoke/lib/harness.js, so registering the flow isthe file itself; there is no separate index list to edit. No LLP changed: this is a test-coverage
gap, not a design change (no LLP in the 0157-0162 set states a smoke inventory).
Mutation-test evidence
The fix here is the test, so it has to be shown to have teeth. The two-level handling in
hypaware-core/plugins-workspace/openclaw/src/session_file.jswas temporarily reverted to thepre-#552 behavior (read every message field off the record line):
function openclawMessageEnvelope(row) { - return isPlainObject(row.message) ? row.message : row + return row }Under mutation, the new smoke FAILS with #543's exact signature (
status: ok,rows_written: 0):Restored (
git checkout -- hypaware-core/plugins-workspace/openclaw/src/session_file.js;git statusclean of the mutation), and it passes again:So the smoke would have caught #543.
Other checks in the worktree:
npm test3281 pass / 0 fail / 1 skipped;npm run typecheckclean;npm run build:typesclean.Item 2 was deliberately NOT actioned
Issue #555 item 2 (the
timestamp/idprecedence asymmetry insession_file.js, wheretimestampresolves envelope-first viamessageFieldwhileidresolves line-first) is leftexactly as merged. Quoting the issue's own reasoning:
The issue asks to revisit it only "if a future change ever gives
backfill.jsandsettle.js(or athird consumer) independent access to the raw
recordfortimestamporidrather than goingthrough the shared reader." This PR adds no such consumer. The issue's "Non-issues (do not action)"
section was likewise left untouched.
Base branch note
This PR is stacked on
fix/issue-543(PR #552), not onmaster. PR #552 is still open andunmerged, so on
masterthe OpenClaw reader still takes message fields off the record line and thissmoke cannot pass there by construction. LLP 0173 records the same finding for its own T12: the
fixture "genuinely cannot be built correctly against the current tree." GitHub will retarget this
PR's base to
masterautomatically when #552 merges. Merge #552 first.Coordination with
integration/openclaw-two-lane-capture(LLP 0173, task T12)T12 on that branch also plans a
backfill_openclaw_fixture, extended with a quiesce-windowdimension (mtime-based skipping of recently-touched session files) and a two-lane dedupe
assertion. T12 is currently blocked and explicitly held until #552 merges, which is the same
blocker this PR has.
This flow is written so T12 extends rather than replaces it:
smoke/lib/openclaw_session_fixture.js, importable by asecond flow rather than private to this one
writeOpenclawSessionFixturetakes an optional caller-chosenmtimeMs(applied viafs.utimes),which is precisely the "controllable-mtime
agents/<id>/sessions/tree" T12 calls for. Nothing inthis flow uses it; it is there so T12 does not have to fork the helper
header,records,agentId, andsessionIdare all parameterizable, so a quiesce case canstage a second session file under the same agents root
sweep_openclaw_quiesce) rather than claiming thegeneric name for this one case
Backlink for whoever merges: LLP 0173 task T12 ("the hermetic smoke gap", Section 9),
llp/0173-openclaw-two-lane-capture.plan.mdonintegration/openclaw-two-lane-capture.Fixes #555