Uh oh!
There was an error while loading. Please reload this page.
llp: OpenClaw full capture — plugin-steered shadow providers replace the settings edit (LLP 0142–0149) - #475
Merged
Conversation
…the settings edit (LLP 0142-0149) Eight Draft decisions revising the @hypaware/openclaw adapter (LLP 0109) so capture covers all bearer-token providers instead of one repointed Anthropic primary: - 0142 steering moves from mutating openclaw.json to an OpenClaw-side plugin: catalog-contributed shadow providers + before_model_resolve (verified to re-fire per fallback candidate and cover subagents) - 0143 no attach_probe; detach is an honest no-op; json_path retires (OpenClaw was its last consumer; breaking change accepted, no installed base) - 0144 one shadow provider per API shape (anthropic-messages / openai-completions), not per vendor; session identity stays the prompt-head hash for v1 - 0145 shadow providers borrow the shadowed provider's credential via the public plugin-sdk resolveApiKeyForProvider (OAuth branch verified to return live, refreshed tokens) through prepareRuntimeAuth - 0146 host-signed providers (Bedrock, Vertex) and per-account proxy vendors (Cloudflare/Vercel AI gateways) deferred, not solved - 0147 CLI-backend turns (claude-cli/codex) are permanently transcript-captured by the sibling adapters, never proxied - 0148 wire parity (anthropic-beta set, service_tier) is mirrored in the plugin's own wrapStreamFn; the existing ai-gateway capability is reused unchanged - 0149 uncapturable turns pass through and warn (no_credential / no_preset / deferred), one rule for every cause Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
philcunliffe pushed a commit
that referenced
this pull request
Jul 30, 2026
Two things, both forced by what landed on master while this was in review. ## The conflict: #458 rewrote the file this PR collapses #458 ("hyp session ignore names the session container Codex drops on") and this PR both touch `readRolloutMeta` in `ai-gateway/src/session_command.js`, from opposite directions. #458 grew it: a `type === 'session_meta'` envelope guard, a third field (`payload.session_id`), a blank-after-trim test on that field, and a new resolution path (`resolveFromStatedThread`) built on it. This PR deletes it: issue #465's whole value is that no second copy of the `session_meta` rules survives, so the function becomes a delegation to `src/core/codex/rollout_session_meta.js`. Resolved by keeping both. Every behaviour #458 added is intact - the container is still the answer, `CODEX_THREAD_ID` is still a selector rather than an answer, a legacy or blank `session_id` still refuses rather than falling back to the thread id - and none of the predicates behind it live here any more. The three rules (raw line, envelope type, blank-is-absent) are stated once, in the core reader, which is the point of #465. ## Third-copy check #458's own resolution logic does NOT read `session_meta` fields directly. `resolveFromStatedThread` matches on `meta.threadId` and reads `meta.sessionId`, both from `readRolloutMeta`, so it is a second *resolution path*, not a second *reader*, and routing it through the shared reader needed no change to it. What had become a full second copy is `readRolloutMeta` itself: #458 gave it its own envelope guard and its own blank test on the new field, which is precisely the duplicate this PR removes. `statedEnv` is a blankness test on an environment variable, not on the header, and stays. The only other site that reads `session_meta` fields itself is `codex/src/backfill.js`, which walks whole rollout files (folding `turn_context`) and so cannot call a first-line reader. It shares the one `cwd` predicate (`sessionMetaCwd`) and deliberately does not share rule 3's id refusal: a backfilled row must land in some partition, where the CLI can refuse. Now stated in the LLP rather than only in the code comment. ## One behaviour change the merge required `readRolloutMeta` no longer discards a rollout whose `cwd` is unusable; it passes `cwd: undefined` through. The shared reader refuses a blank or relative `cwd` (LLP 0150 #usable-cwd), and on the cwd-matching path that is what we want. On #458's stated-thread path `cwd` is never consulted, so requiring it would have turned a field-level predicate into a file-level one and refused a session whose container is plainly on disk - a regression of #458 introduced by tightening a field it does not use. Pinned by a new test, mutation-checked: restoring the cwd requirement reddens it. ## Reconciled beyond the conflict markers Four claims that auto-merged cleanly but stopped being true once #458 landed: - LLP 0150 said `sessionId` "has no consumer yet on purpose" and that moving the verb onto the container "is #453's job". #453 is closed and the verb is moved; the section now records that, and that #458 added a resolution path but no second reader. - LLP 0150 said the `hyp session` caller "compares `meta.cwd` against an absolute invocation cwd, so a relative value never matched". True of one of its two paths now. Rewritten to say why the other path must not refuse on `cwd` at all. - LLP 0150's rule 3 read as a blanket "callers refuse", which the backfill does not. Scoped to the reader, with the backfill's different answer explained. - The `readRolloutMeta` doc comment said `meta.sessionId` "is deliberately not consulted here". It is consulted now. Also: LLP 0150's Context bullet no longer implies `CODEX_THREAD_ID` makes the rollout unnecessary, and `resolveSessionIdForCli`'s legacy-rollout note points at `legacyRolloutError`, which is where that refusal now lives. ## The renumber: collision avoidance, NOT a ruling on #469#475 landed `llp/0143-openclaw-registers-no-attach-probe.decision.md` while this PR held `llp/0143-one-reader-for-codex-session-meta.decision.md`. Different filenames, so git flags no conflict, but merging as-is would put two documents at 0143 on master, a fifth duplicate after 0098, 0099, 0111 and 0142. This document moves to 0150 (0149 is the highest on master) and all 12 references follow: `@ref` annotations in `session_command.js`, `backfill.js`, `rollout-cwd.js`, `rollout_session_meta.js`, `types.d.ts` and four test files, plus the heading and one self-reference in the document. **This is mechanical collision avoidance and sets no precedent.** Issue #469 asks whether the later claimant renumbers or whether citations become filename-qualified, and that question is still open and unowned. Renumbering here is only what avoids adding a sixth duplicate today; whichever way #469 is decided, nothing about this commit should be read as having decided it. The human's 0143 is untouched, as are LLP 0142's two references to it. Verified: `npm test` 2915 tests, 2905 pass, 8 fail (the pre-existing `test/core/leave-command.test.js` set, identical on a pristine `origin/master` worktree), +12 tests and no new failures. `npm run typecheck` clean. Smokes `gateway_codex_capture`, `session_optout_capture_drop`, `backfill_codex_fixture` all ok. All `@ref LLP 0150` targets and anchors resolve. Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe pushed a commit
that referenced
this pull request
Jul 30, 2026
PR #475 merged llp/0144-shadow-provider-per-api-shape.decision.md to master as part of the OpenClaw 0142-0149 block, so this branch's llp/0144-codex-lineage-from-body-client-metadata.decision.md would have put two documents at 0144. Because the filenames differ, git reported no conflict and CI stayed green, so nothing on the rung ladder would have caught it before merge. 0151 verified free across master and every remote branch. 0150 is held by fix/issue-465, renumbered there from 0143 for the same reason. Purely a renumber: the document's content is unchanged, and the rename carries every reference with it (12 @ref annotations in exchange-projector.js, 1 in the gateway_codex_capture smoke, 14 in codex-exchange-projector.test.js, 1 in codex-rollout-cwd.test.js, plus the cross-links and Related entries in LLP 0083 and LLP 0141, and the heading). No residual 0144 reference remains on this branch and all six referenced anchors resolve in the renamed document. The human directed this renumber explicitly, accepting that moving the head strips neutral:approved and re-opens the review ladder. It resolves this one collision and sets no precedent for issue #469, where the general renumber-versus-qualified-citation convention is still open. Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe added a commit
that referenced
this pull request
Jul 30, 2026
…s Codex never sends (#464) (#467) * Codex lineage reads the durable body client_metadata (#464) The Codex live projector derived a turn's thread, session and parent thread from request headers, three of whose names Codex has never emitted, while the authoritative ids sat unread in the request body. Read against Codex's source, Codex projects one `CodexResponsesMetadata` snapshot onto three surfaces per HTTP request. The flat body `client_metadata` map (a top-level field of `ResponsesApiRequest`) is built unconditionally and always carries `session_id` and `thread_id`; the `x-codex-turn-metadata` blob only rides along for request kinds that carry turn metadata; and `compatibility_headers` emits exactly four names. `thread-id`, `session-id` and `parent-thread-id`, the three bare names the projector read, are not among them, so they could never supply a right value and could supply a wrong one: any hop setting `thread-id` dictated `conversation_id`, the scope of the row's fallback `message_id`. - Lineage now resolves body-map-first, turn-metadata-blob-second, with the real `x-codex-parent-thread-id` header (previously misspelled) last. - A Codex-owned `client_metadata` map is itself a sufficient signal that an exchange is Codex, so the API-key route's generic `/v1/responses` no longer needs a Codex header to be recognized. - The three fictional header names are gone; the real ones are named constants. - `attributes.codex.lineage_source` records which surface stated the identity, so a future Codex version dropping one is queryable rather than a silent `conversation_id` drift. Nothing re-keys: the blob's `thread_id` and the body map's `thread_id` are the same field of the same snapshot, and the removed header names never matched real traffic, so `conversation_id` (and the `message_id` / `part_id` scoped on it, LLP 0030) is unchanged for every shape already recorded. Already-recorded rows are left alone; LLP 0143 states why no backfill. LLP 0143 is the decision doc. LLP 0083 and LLP 0141 carried the disproved premise that `x-codex-turn-metadata` is Codex Desktop behavior and that the subscription route states its session in a `session-id` header; both are corrected, and the two `codex-rollout-cwd` fixtures that rested on that header now use the shape Codex really sends. Co-Authored-By: Claude <noreply@anthropic.com> * Renumber LLP 0143 to 0144 to clear a duplicate number with PR #466 PR #466 (fix/issue-465) independently minted llp/0143-one-reader-for-codex-session-meta on its own branch at the same time this branch minted llp/0143-codex-lineage-from-body-client-metadata. Two documents cannot share a number, and duplicate LLP numbers are exactly the corpus defect issue #463 tracks, so this branch takes 0144 (deterministic tie-break: the lower PR number keeps the original). Purely a renumber: every @ref anchor, cross-link, and Related entry follows the move, and the document's own content is unchanged. Verified no residual "0143" reference remains on this branch and all six referenced anchors still resolve in the renamed document. Co-Authored-By: Claude <noreply@anthropic.com> * Codex lineage: record a body/blob disagreement instead of silently preferring one The body-map-first precedence rests on Codex projecting one metadata snapshot onto both the flat `client_metadata` map and the `x-codex-turn-metadata` blob, so that the two are equal whenever both are present. That is a claim about another program's internals which HypAware cannot verify, and the body-wins tie-break discarded the counter-evidence without trace: a row whose surfaces disagreed was indistinguishable from a row whose surfaces agreed. - `attributes.codex.lineage_conflict` now names the lineage fields the two surfaces state differently (`thread_id`, `session_id`, `turn_id`, `parent_thread_id`), absent when they agree or only one spoke. The row still keys on the body, so this adds a signal and moves no identity. - `lineage_source` now resolves in the same order as the values it describes (`thread_id` before `session_id`, body before blob). It previously answered "did the body state anything at all", which mislabelled a turn whose `thread_id` came from the blob while only its `session_id` came from the body as `body_client_metadata`, though `conversation_id` keys on `thread_id`. - LLP 0144 gains `#lineage-conflict` and states why the assumption gets a continuously checked signal rather than a one-time assertion. Co-Authored-By: Claude <noreply@anthropic.com> * Codex lineage: an ambiguous flat client_metadata pair is not evidence of Codex `readCodexClientMetadata` accepted a body `client_metadata` map on either of two signals: an `x-codex-*` prefixed key, or the flat `session_id` + `thread_id` pair. Only the first is Codex-exclusive. The second is a shape any agent framework may send, and the projector's matched path set includes the fully generic `/v1/responses` and `/v1/chat/completions`, so an unrelated client that posted that pair was stamped `client_name: 'codex'` and dictated the row's `conversation_id` and `session_id` (the partition key, LLP 0030). That is the same defect class as the fictional `thread-id` header this branch removed, reached through the body instead of a header, and in a capture product a misfiled client is a privacy question. The flat pair is now honoured only when the transport already identified the exchange as Codex independently of the body (`hasCodexTransportSignal`: the `chatgpt` upstream, the `/backend-api/codex/` namespace, an `x-codex-*` compatibility header, or a `codex`-prefixed user-agent product). Real Codex loses nothing: `client_metadata` carries `x-codex-installation-id` and `x-codex-window-id` on every request, so the strict branch alone covers all known Codex traffic, and the corroborated pair still covers a build that stopped writing them. A non-Codex client's row now comes out byte-identical to the same request with no `client_metadata` at all. `isCodexExchange` is replaced by `hasCodexTransportSignal` plus the body check at the single decision point in `resolveCodexContext`, so the corroboration flag cannot drift between the two callers. Also pins the assumption that keeps the outer `match` gate (path and turn-metadata header only) consistent with the body being a Codex signal: a test asserts every route Codex posts to passes the gate, so a body-only Codex request is never dropped before the body is read. LLP 0144#body-is-a-codex-signal and #body-is-authority are amended in the same commit. Co-Authored-By: Claude <noreply@anthropic.com> * Renumber LLP 0144 to 0151 to clear the collision with the OpenClaw block PR #475 merged llp/0144-shadow-provider-per-api-shape.decision.md to master as part of the OpenClaw 0142-0149 block, so this branch's llp/0144-codex-lineage-from-body-client-metadata.decision.md would have put two documents at 0144. Because the filenames differ, git reported no conflict and CI stayed green, so nothing on the rung ladder would have caught it before merge. 0151 verified free across master and every remote branch. 0150 is held by fix/issue-465, renumbered there from 0143 for the same reason. Purely a renumber: the document's content is unchanged, and the rename carries every reference with it (12 @ref annotations in exchange-projector.js, 1 in the gateway_codex_capture smoke, 14 in codex-exchange-projector.test.js, 1 in codex-rollout-cwd.test.js, plus the cross-links and Related entries in LLP 0083 and LLP 0141, and the heading). No residual 0144 reference remains on this branch and all six referenced anchors resolve in the renamed document. The human directed this renumber explicitly, accepting that moving the head strips neutral:approved and re-opens the review ladder. It resolves this one collision and sets no precedent for issue #469, where the general renumber-versus-qualified-citation convention is still open. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: neutral-reconciler <neutral@example.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: neutral-reconciler <neutral@hyparam.com> Co-authored-by: neutral-reconciler <neutral-reconciler@users.noreply.github.com>
This was referenced Jul 30, 2026
philcunliffe added a commit
that referenced
this pull request
Jul 30, 2026
…lear the number collision (#486) Flip LLP 0143-0149 + 0152 (formerly 0142) from Draft to Accepted, following review and merge of #475. The steering decision renumbers from 0142 to 0152: a concurrently authored decision (0142-privacy-surface-and-skill-discoverability, Accepted) took the same number and already has @ref citations in shipped skill files, while ours was cited only within this set - so ours moves. All in-set references updated; 0152 is the next free number after 0150/0151. Also rewords 0147's session-correlation note to drop its [inferred] tag (not-investigated stated plainly; the decision does not depend on it), per the rule that no [inferred] claim survives into Accepted. ref-check: the set contributes zero errors; the pre-existing duplicate numbers 0098/0099/0111 are untouched. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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 freeto 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.
What
Eight Draft decisions revising the
@hypaware/openclawadapter (LLP 0109) so capture covers all bearer-token providers — OpenRouter, OpenAI, Groq, and the rest — instead of one repointed Anthropic primary. Docs only; no code changes in this PR.Why
LLP 0109's mechanism (edit
openclaw.json, repointmodel.primary) captures a silent subset: fallbacks, per-agent models, image/PDF slots, and runtime/modeloverrides all escape uncaptured, with nothing marking the gap. Its${ANTHROPIC_API_KEY}auth also breaks for subscription users — OpenClaw's most common Anthropic auth. Both problems are structural to the settings-edit approach.The decisions
before_model_resolve. The AI gateway proxy stays the capture path — fidelity is unchanged.attach_probe; detach is an honest no-op (Desktop precedent, #445); thejson_pathcore format retires — OpenClaw was its last consumer. Breaking change accepted; no installed base.anthropic-messages/openai-completions), not per vendor. Session identity keeps the prompt-head hash for v1.resolveApiKeyForProvider— fixes subscription-auth users.claude-cli/*, codex) are permanently transcript-captured by the sibling adapters, never proxied.service_tier) is mirrored in the plugin's ownwrapStreamFn. The existinghypaware.ai-gateway ^2.0.0capability is reused unchanged — header-driven routing already exists inmatchUpstream.no_credential/no_preset/deferred) — one rule for every cause; the warnings are the coverage ledger.Verified against OpenClaw source (not guessed)
before_model_resolvere-fires per fallback candidate (model-fallback.tsloop →runAgentAttempt→resolveHookModelSelection) and covers subagent spawns (callGateway({method:"agent"})re-enters the same pipeline)resolveApiKeyForProvider's OAuth branch returns live tokens, refreshing under a lock when expired (auth-profiles/oauth.ts)prepareRuntimeAuth, not via profile orderingmodels.jsonbaseUrl-preservation hazard is neutralized by the fixed gateway port (LLP 0114) plus per-requestbaseUrlfromprepareRuntimeAuth(GitHub Copilot precedent)fetchgated onprovider === "anthropic"and bypasses steering — a low-volume side channel, documented rather than silentNotes for review
[inferred]remains, in 0147's open question (session-correlation follow-up) — legal in a Drafthyp statusattach signal for probe-less clients (0143), vendor-ToS on proxied credentials (0145), whether pi-ai adds default betas itself (0148, implementation-time check)🤖 Generated with Claude Code