Uh oh!
There was an error while loading. Please reload this page.
OpenClaw Lane A live capture: client-header attribution, settlement match keys, Responses-API decoder (LLP 0175/0176) - #586
Conversation
…drop the wire timestamp prefix (LLP 0175)
Live OpenClaw captures were claimed by the Claude projector and never
settled: attach wrote only x-hypaware-upstream while the openclaw
projector's match() keys on x-hypaware-client, so every gateway exchange
misattributed as claude under a fallback session and the sweep re-imported
the same turns natively - unbounded duplication (LLP 0175).
- attach.js writes x-hypaware-client: openclaw alongside the upstream
marker; ownership still keys on the marker alone, so pre-fix entries
detach and a re-attach upgrades them in place.
- match_key.js normalizes the wire-only user-message timestamp prefix
('[Mon 2026-08-03 15:33 PDT] ...') out of text identity, symmetrically
on both builders; without it every user turn content-missed while its
exchange's assistant turns settled (the one residual observed after the
header fix).
Both verified against live traffic: exchanges now attribute openclaw,
settle onto the native session file identity for user and assistant
turns, and the sweep dedupes against them instead of duplicating. LLP
0175 records the evidence, remediation, and round-1 review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>…s name their path (LLP 0176) The gateway proxied /v1/responses traffic faithfully and recorded nothing: the openclaw projector's OpenAI branch spoke only Chat Completions, and an exchange no projector decodes vanished with a warn that named neither path nor status - hours of live OpenClaw OpenAI traffic read as 'client never attached' (LLP 0176). - projector.js grows a third parse branch, dispatched per exchange on path/body shape: request input items (message / function_call / function_call_output; reasoning replays skipped), instructions plus the leading system run folded into system_text, response output arrays (text, tool_use, reasoning-as-thinking), streamed exchanges rebuilt from the terminal response.completed payload with a finished-items fallback, and Responses usage re-keyed through the existing Chat Completions netting path (gross input minus cached read, LLP 0035). Decoded turns emit the shared block vocabulary, so they produce the same match keys the session file yields and settle instead of duplicating (LLP 0175's sequencing constraint, satisfied first). - message_projector.js's no_projector_match warn now carries path, method, status_code, and is_sse. Verified live: the first OpenAI turn ever captured from OpenClaw landed with openclaw attribution, gpt model and provider intact, settled onto the native session. LLP 0176 records the evidence and round-1 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… any-cast helper The projector's project() return union (projection | drop | promise) needs the same awaiting helper the sibling shape suite uses; property access on the bare union fails tsc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hat residual is documented The #root-cause section records the missing x-hypaware-client header only; the wire-only user-message timestamp prefix and its normalization are fix direction item 2. Adds the anchor and retargets the three refs. Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe
commented
Aug 3, 2026
Review: findings (no blockers)The three claimed areas are real and correctly aimed. Verified beyond the test suite: a Pushed (mechanical only)
Handed back1. major -
Confirmed: 2. minor - Reproduced: with { "role": "assistant", "content": [], "stop_reason": "error",
"attributes": { "openclaw": { "match_key": "5d008246..." } } }The docstring says the stream "degrades to the 3. minor - The chain is That fragments Lane A session grouping for exactly the traffic this PR just made recordable. In practice OpenClaw always sends 4. minor - LLP 0176 fix 2 is now more consequential than when it was written. With the client header landing, an error-status exchange in a matched shape no longer falls out at the unmatched-drop layer: it projects the request-side turns and no assistant row, with nothing marking it as failed. Confirmed a 400 Responses exchange writes the user turn and stops. That is the state fix 2 already describes as open, so it is not a regression from this PR, but the PR changes it from "records nothing" to "records a prompt with no reply and no error signal", and a reader of 5. minor - the header-absent fallback is still wrong-but-confident, and only the issue doc says so. With no 6. minor - test coverage gaps for the failure modes. The Responses tests are strong on the happy path and cover the cut-stream, incomplete-response, near-miss-bracket and mid-text-stamp cases well. Missing: (a) a 7. nit - issue docs are still 8. nit - Verification
Note on LLP numberingThis PR's |
philcunliffe
commented
Aug 3, 2026
Review round 2: findingsThe only delta since the round 1 record ( Every finding in the round 1 record still stands unaddressed at this head. Verdict is unchanged: changes requested, on finding 1. Blocking ask - Findings 2 through 8 are non-blocking and yours to take or leave. Findings 2 and 3 in particular are cheap guards worth considering before this lands. Reply on this thread or push to the branch, and neutral will re-engage on its next tick. |
philcunliffe
left a comment
There was a problem hiding this comment.
Requesting changes on one finding; the full review records are in the thread (rounds 1 and 2).
openclaw/src/match_key.js:97 - WIRE_TIMESTAMP_PREFIX's zone group [A-Z]{1,6} matches PDT/EDT/UTC but not the GMT+N short names JS emits for most non-US timezones. So the settlement content-miss this PR fixes stays open for those users, while the tests (which only cover PDT) report success.
Either widen the group to accept an offset tail, e.g. [A-Z]{1,5}(?:[+-]\d{1,2}(?::\d{2})?)?, and add GMT+2 / GMT+5:30 to the near-miss test table; or, if OpenClaw's stamp is provably always an alpha abbreviation, say so in the WIRE_TIMESTAMP_PREFIX doc comment so the narrow pattern is deliberate rather than incidental.
Everything else in the review is non-blocking. The three areas this PR targets all check out otherwise, and e44bb7a (an @ref anchor retarget) was pushed on your branch under neutral:adopt.
…es session-id fallback (PR #586) Finding 1 (blocking): WIRE_TIMESTAMP_PREFIX now accepts an offset tail (GMT+2, GMT+5:30) alongside alpha abbreviations, since JS short zone names are offset-style for most non-US zones; positive and near-miss cases added to the test tables. Finding 2: a Responses stream cut with zero finished items emits no assistant row instead of an empty-content row carrying a match key. Finding 3: openclawSessionId falls back through the Responses input array, so an instruction-less Responses request groups by conversation. Finding 8: the projection-skipped warn logs the pathname, not the full request URL with query string. Finding 4: LLP 0176 fix 2 notes the gap's changed shape now that matched error exchanges record a prompt with no reply and no error signal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bgmcmullen
commented
Aug 4, 2026
Pushed
Findings 5, 6 (beyond the tests the fixes above added), and 7 are left as-is for follow-up.
|
philcunliffe
commented
Aug 4, 2026
Review round 3: clean
Finding 1 (blocking): resolvedBuilt the stamp the way OpenClaw's observed shape implies (
16 of 16. The two-digit-hour-plus-45-minute case ( On the locale question: the observed No over-strip. Diffed the old pattern against the new across 20 strings. The only behavior changes are the six real GMT-offset stamps; every near-miss still returns Finding 2: resolved
It removes the harmful half (a content-free row acquiring a native Finding 3: resolved (array form)Finding 4: resolved, and the doc is accurateVerified rather than trusted. A 400-status Responses exchange in a matched shape projects one Finding 8: resolved
Revert tests
All three load-bearing. None passes both ways. Regressions: none
Residuals (minor, not asks, neither introduced here)A. The empty-assistant-row fix is Responses-only; both siblings still emit it. Reproduced on Chat Completions (cut stream, role-only chunk) and Anthropic (cut stream, B. The ConventionsClean across all seven touched files. No em dashes, no semicolons, no inline Verification
You took the harder route on finding 1, widening the pattern rather than asserting the narrow form was safe, and it was the right one. All four findings are genuinely closed, and the tests hold them closed. |
philcunliffe
left a comment
There was a problem hiding this comment.
Approving. All four findings from the earlier rounds are resolved, and I verified each empirically rather than by reading the diff: 16 of 16 real timezone stamps now strip (including the 45-minute and negative half-hour offsets), and all three new tests were confirmed to fail when their fix is reverted, so they are load-bearing.
Widening the zone pattern rather than asserting the narrow form was safe was the right call.
Two residuals are noted in the review record as observations, not asks: the empty-assistant-row guard is Responses-only while Chat Completions and Anthropic still emit the same row shape (pre-existing, tracked separately), and openclawSessionId's new branch covers the array input form but not the string form.
Merging is the maintainer's call; neutral holds here.
philcunliffe
commented
Aug 4, 2026
Merge note: one line to change if this lands after #592Not a review finding and nothing to fix here. Flagging a cross-PR interaction discovered while reviewing #592 ("a cut stream with no content emits no assistant row"). #592 adds a shared floor at the projector's row-assembly point that drops an assistant row when a stream was cut and captured no content. Its first implementation keyed cut-ness on The interaction: this PR's Concretely: on the merged tree, a cut Responses stream whose one finished item is of a type The fix, at merge time: replace Verified: the two branches merge cleanly (ort, no conflicts) and 194/194 openclaw tests pass on the merged tree. A terminal Merge order does not matter; whichever lands second wants this one-line edit. This PR's own approval stands unchanged. |
Round-2 review follow-up. The floor's safety rests on an unenforced convention: `markCutStream` is the only thing allowed to write `CUT_STREAM_STOP_REASON`, because the recorded value and the WeakSet membership that actually decides the drop are halves of one fact. The two wire shapes that exist are covered by behavior tests (verified by mutation: introducing a copy between mark and check, or stamping the literal directly, fails them). A wire shape added later is not, and PR #586's Responses reconstruction is exactly that case waiting to happen: it writes `partial.stop_reason = 'error'` as a bare literal and has to become `markCutStream(partial)` at merge time. Adds a source lint, the same shape as `house-style-em-dash.test.js`, that fails when the marker is stamped anywhere but inside `markCutStream`, and points `markCutStream`'s docstring at it. No behavior change. Co-Authored-By: Claude <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
#586 landed its Responses reconstruction stamping `partial.stop_reason = 'error'` as a bare literal, which is the merge obligation recorded on that PR's thread. Merging master into this branch reproduced it: the lint added in cd6c8b3 fails with not ok 18 - the cut-stream marker is only ever stamped through markCutStream GitHub reported the merge CLEAN, because the conflict is semantic rather than textual: no rung sees it and nothing but the lint would have caught it before it reached master. Routing the Responses path through markCutStream also closes the residual round 3 identified: a cut Responses stream whose finished items produce no mapped content emitted a content-free row with a live message_index, which #586's own doneItems.length === 0 guard does not cover. Fixes#591
…#592) * A cut stream with no content emits no assistant row on any wire shape (#591) The Anthropic and Chat Completions stream reconstructions both mark a stream that ended without its terminal event `stop_reason = 'error'` and return whatever arrived. When nothing arrived, that was still a row: `content: []` with a live `message_index` and no native `message_id`, so it stayed eligible for settlement's ordinal/time fallback and could acquire a `message_id` belonging to another turn. Every content-free assistant row also hashes to one canonical match key (5d008246) regardless of which decoder built it, so empty rows could collide across wire shapes too. The floor now sits at the one place a row is assembled, the projectedMessages loop in `project()`, so both shapes (and any added later) inherit it. The condition is "cut AND empty", keyed on the synthetic cut marker now named `CUT_STREAM_STOP_REASON`: a response that reached its terminal event and genuinely produced nothing keeps its row, and Chat Completions' delta stitching is untouched, so a cut stream that did carry partial text still records it. Co-Authored-By: Claude <noreply@anthropic.com> * Cut-stream floor keys on object identity, not the emitted stop reason (#591) `isEmptyCutRow` decided cut-ness by comparing the row's `stop_reason` to the literal `'error'`, on the premise that neither API sends `error` as a real stop reason. That premise does not cover the traffic this projector actually reads. An unrecognized upstream is parsed as the Anthropic wire by design, and an OpenAI-compatible endpoint reached through a config upstream can send `finish_reason: "error"`; a terminal response that said `error` and produced nothing is a real answer, and the row was the only record it happened. Reproduced on all three shapes: a terminal Chat Completions chunk with `finish_reason: 'error'`, a non-streamed body with the same, and an Anthropic `message_delta` + `message_stop` carrying `stop_reason: 'error'` all lost their assistant row. The same premise covered request history: an Anthropic history turn that replays `stop_reason: 'error'` on an empty assistant message was dropped too, which the role test alone does not prevent. Cut-ness is now carried out of band, in a module-level `WeakSet` the two reconstructions add to on the branch that stamps the marker. A wire body is parsed JSON, so it can forge any field value but never membership in that set, which makes "cut AND empty" true by construction rather than by an assumption about upstream stop-reason vocabularies. The emitted `stop_reason` is unchanged, and every cut-and-empty case the fix already dropped still drops. Co-Authored-By: Claude <noreply@anthropic.com> * Gate the cut-stream marker on a single stamping point (#591) Round-2 review follow-up. The floor's safety rests on an unenforced convention: `markCutStream` is the only thing allowed to write `CUT_STREAM_STOP_REASON`, because the recorded value and the WeakSet membership that actually decides the drop are halves of one fact. The two wire shapes that exist are covered by behavior tests (verified by mutation: introducing a copy between mark and check, or stamping the literal directly, fails them). A wire shape added later is not, and PR #586's Responses reconstruction is exactly that case waiting to happen: it writes `partial.stop_reason = 'error'` as a bare literal and has to become `markCutStream(partial)` at merge time. Adds a source lint, the same shape as `house-style-em-dash.test.js`, that fails when the marker is stamped anywhere but inside `markCutStream`, and points `markCutStream`'s docstring at it. No behavior change. Co-Authored-By: Claude <noreply@anthropic.com> * isEmptyCutRow's docstring names the wrong reason history is safe (#591) The comment said request-history messages are "excluded twice over, by the set and by the assistant-role test". Only the set excludes them. A replayed history turn can carry role: 'assistant' with an empty content array, which is precisely the claim round 1 of this PR made and round 2 falsified by execution, so leaving the sentence standing invites the same mistake back. Comment only. Co-Authored-By: Claude <noreply@anthropic.com> * Merge master and settle the Responses cut marker through markCutStream #586 landed its Responses reconstruction stamping `partial.stop_reason = 'error'` as a bare literal, which is the merge obligation recorded on that PR's thread. Merging master into this branch reproduced it: the lint added in cd6c8b3 fails with not ok 18 - the cut-stream marker is only ever stamped through markCutStream GitHub reported the merge CLEAN, because the conflict is semantic rather than textual: no rung sees it and nothing but the lint would have caught it before it reached master. Routing the Responses path through markCutStream also closes the residual round 3 identified: a cut Responses stream whose finished items produce no mapped content emitted a content-free row with a live message_index, which #586's own doneItems.length === 0 guard does not cover. Fixes#591 --------- Co-authored-by: test <test@test.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: test <test@example.com>
Summary
A day of live investigation found OpenClaw's Lane A (gateway live capture) completely non-functional, with three stacked causes, each fixed and re-verified against live traffic:
attach.jswrote only thex-hypaware-upstreamrouting marker, never thex-hypaware-client: openclawheader the openclaw exchange projector'smatch()keys on. Anthropic-shaped exchanges fell through to the Claude projector (misattributed, never settled, and duplicated against the sweep, replaying full conversation history per capture); OpenAI-shaped exchanges matched no projector and vanished silently. Attach now writes both headers; ownership still keys on the marker alone so pre-fix entries detach cleanly and a re-attach upgrades them in place.[Mon 2026-08-03 15:33 PDT]to user messages on the wire but stores bare text in its session file, so user turns content-missed while assistant turns settled.match_key.jsnow normalizes the wire-only stamp out of text identity, symmetrically on both builders./v1/responses. A third parse branch decodes Responses exchanges (input items, tool calls/results, reasoning-as-thinking, streamed reconstruction from the terminalresponse.completedpayload, usage netted through the existing gross-minus-cached path). Decoded turns emit the shared block vocabulary so they settle under the same match keys the session file yields, honoring the fix-ordering constraint (settlement first, decoder second). The dispatcher'sno_projector_matchwarn now names path/method/status/is_sse so an undecoded dialect can never again masquerade as an unattached client.Ships LLP 0175 + 0176 (Draft, round-1 reviews included) recording the full evidence chain.
Verification
npm test: 3302/3303 (the 1 failure is pre-existing on master:usage-policy-fold, confirmed identical with these changes stashed)hyp smoke client_attach_idempotent: greenclient_name: openclaw, correct provider/model, settled onto the native session file identity, and the subsequent sweep pass added zero duplicate rows.Out of scope (documented in the LLPs)
🤖 Generated with Claude Code